Merge pull request #1 from blockworks-foundation/contribution-modal
Contribution modal
This commit is contained in:
commit
655bb78305
17
.babelrc
17
.babelrc
|
@ -1,3 +1,18 @@
|
|||
{
|
||||
"presets": ["next/babel"]
|
||||
"presets": [
|
||||
[
|
||||
"next/babel",
|
||||
{
|
||||
"preset-react": {
|
||||
"runtime": "automatic",
|
||||
"importSource": "@emotion/react"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": ["@emotion/babel-plugin", "babel-plugin-macros"]
|
||||
}
|
||||
|
||||
// {
|
||||
// "presets": ["next/babel"]
|
||||
// }
|
||||
|
|
|
@ -25,9 +25,11 @@
|
|||
"react/display-name": 0,
|
||||
"react/prop-types": 0,
|
||||
"@typescript-eslint/explicit-function-return-type": 0,
|
||||
"@typescript-eslint/explicit-module-boundary-types": 0,
|
||||
"@typescript-eslint/explicit-member-accessibility": 0,
|
||||
"@typescript-eslint/indent": 0,
|
||||
"@typescript-eslint/member-delimiter-style": 0,
|
||||
"@typescript-eslint/ban-ts-comment": 0,
|
||||
"@typescript-eslint/no-explicit-any": 0,
|
||||
"@typescript-eslint/no-var-requires": 0,
|
||||
"@typescript-eslint/no-use-before-define": 0,
|
||||
|
@ -36,12 +38,6 @@
|
|||
{
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"no-console": [
|
||||
2,
|
||||
{
|
||||
"allow": ["warn", "error"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ export interface EndpointInfo {
|
|||
name: string
|
||||
url: string
|
||||
websocket: string
|
||||
programId: string
|
||||
poolKey: string
|
||||
}
|
||||
|
||||
export interface WalletContextValues {
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
import BN from 'bn.js'
|
||||
|
||||
import useWalletStore from '../stores/useWalletStore'
|
||||
|
||||
const Balances = () => {
|
||||
const { tokenAccounts, mints } = useWalletStore((state) => state)
|
||||
|
||||
function fixedPointToNumber(value: BN, decimals: number) {
|
||||
const divisor = new BN(10).pow(new BN(decimals))
|
||||
const quotient = value.div(divisor)
|
||||
const remainder = value.mod(divisor)
|
||||
return quotient.toNumber() + remainder.toNumber() / divisor.toNumber()
|
||||
}
|
||||
|
||||
function calculateBalance(a) {
|
||||
const mint = mints[a.account.mint.toBase58()]
|
||||
return mint ? fixedPointToNumber(a.account.amount, mint.decimals) : 0
|
||||
}
|
||||
|
||||
const displayedBalances = tokenAccounts
|
||||
.map((a) => ({ id: a.publicKey.toBase58(), balance: calculateBalance(a) }))
|
||||
.sort((a, b) => (a.id > b.id ? 1 : -1))
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{displayedBalances.map((b) => (
|
||||
<li key={b.id}>
|
||||
{b.id}: {b.balance}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default Balances
|
|
@ -0,0 +1,66 @@
|
|||
import { FunctionComponent } from 'react'
|
||||
import styled from '@emotion/styled'
|
||||
import tw from 'twin.macro'
|
||||
|
||||
type StyledButtonProps = {
|
||||
secondary: boolean
|
||||
}
|
||||
|
||||
const StyledButton = styled.button<StyledButtonProps>`
|
||||
:before {
|
||||
${tw`absolute left-0 top-0 opacity-0 h-full w-full block bg-gradient-to-tl from-secondary-1-light via-secondary-1-dark to-secondary-2-light transition-opacity duration-500`}
|
||||
border-radius: inherit;
|
||||
content: '';
|
||||
z-index: -10;
|
||||
}
|
||||
|
||||
:hover {
|
||||
:before {
|
||||
${tw`opacity-100`};
|
||||
${({ disabled, secondary }) => (disabled || secondary) && tw`hidden`}
|
||||
}
|
||||
}
|
||||
|
||||
:focus {
|
||||
${tw`ring-4 ring-secondary-2-light ring-opacity-40`}
|
||||
${({ secondary }) => secondary && tw`ring-0`}
|
||||
}
|
||||
|
||||
:active {
|
||||
:before {
|
||||
${tw`ring-4 ring-secondary-2-light ring-opacity-40`}
|
||||
}
|
||||
}
|
||||
|
||||
${({ secondary }) => secondary && tw`bg-none`}
|
||||
`
|
||||
|
||||
interface ButtonProps {
|
||||
onClick?: (x?) => void
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
secondary?: boolean
|
||||
}
|
||||
|
||||
const Button: FunctionComponent<ButtonProps> = ({
|
||||
children,
|
||||
onClick,
|
||||
disabled = false,
|
||||
className,
|
||||
secondary = false,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<StyledButton
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={`${className} bg-gradient-to-br from-secondary-1-light via-secondary-1-dark to-secondary-2-light relative z-10 default-transition px-6 py-2 rounded-lg text-fgd-1 hover:bg-bkg-3 focus:outline-none disabled:cursor-not-allowed`}
|
||||
secondary={secondary}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</StyledButton>
|
||||
)
|
||||
}
|
||||
|
||||
export default Button
|
|
@ -4,6 +4,7 @@ import { WALLET_PROVIDERS, DEFAULT_PROVIDER } from '../hooks/useWallet'
|
|||
import useLocalStorageState from '../hooks/useLocalStorageState'
|
||||
import WalletSelect from './WalletSelect'
|
||||
import WalletIcon from './WalletIcon'
|
||||
import Button from './Button'
|
||||
|
||||
const StyledWalletTypeLabel = styled.div`
|
||||
font-size: 0.6rem;
|
||||
|
@ -17,17 +18,17 @@ const ConnectWalletButton = () => {
|
|||
)
|
||||
|
||||
return (
|
||||
<div className="flex justify-between border border-th-primary rounded-md h-11 w-48">
|
||||
<div className="flex justify-between border border-primary-light hover:border-fgd-1 rounded-md h-11 w-48">
|
||||
<button
|
||||
onClick={() => wallet.connect()}
|
||||
disabled={!wallet}
|
||||
className="text-th-primary hover:text-th-fgd-1 focus:outline-none disabled:text-th-fgd-4 disabled:cursor-wait"
|
||||
className="focus:outline-none disabled:text-fgd-4 disabled:cursor-wait"
|
||||
>
|
||||
<div className="flex flex-row items-center px-2 justify-center h-full rounded-l default-transition hover:bg-th-primary hover:text-th-fgd-1">
|
||||
<div className="flex flex-row items-center px-2 justify-center h-full rounded-l default-transition text-primary-light text-sm hover:bg-primary hover:text-fgd-1">
|
||||
<WalletIcon className="w-5 h-5 mr-3 fill-current" />
|
||||
<div>
|
||||
<span className="whitespace-nowrap">Connect Wallet</span>
|
||||
<StyledWalletTypeLabel className="font-normal text-th-fgd-1 text-left leading-3">
|
||||
<StyledWalletTypeLabel className="font-normal text-fgd-1 text-left leading-3">
|
||||
{WALLET_PROVIDERS.filter((p) => p.url === savedProviderUrl).map(
|
||||
({ name }) => name
|
||||
)}
|
||||
|
@ -43,3 +44,15 @@ const ConnectWalletButton = () => {
|
|||
}
|
||||
|
||||
export default ConnectWalletButton
|
||||
|
||||
export const ConnectWalletButtonSmall = ({ children, onClick }) => (
|
||||
<div className="relative">
|
||||
<Button
|
||||
className="rounded-full h-9 w-44 z-30 relative"
|
||||
onClick={() => onClick()}
|
||||
>
|
||||
<div className="flex items-center justify-center text-sm">{children}</div>
|
||||
</Button>
|
||||
<div className="absolute animate-connect-wallet-ping bg-secondary-2-light top-0 rounded-full h-9 w-44 z-20" />
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -0,0 +1,287 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import styled from '@emotion/styled'
|
||||
import tw from 'twin.macro'
|
||||
import { LockClosedIcon, LockOpenIcon } from '@heroicons/react/outline'
|
||||
import { LinkIcon } from '@heroicons/react/solid'
|
||||
import useWalletStore from '../stores/useWalletStore'
|
||||
import Input from './Input'
|
||||
import Button from './Button'
|
||||
import { ConnectWalletButtonSmall } from './ConnectWalletButton'
|
||||
import Slider from './Slider'
|
||||
import Loading from './Loading'
|
||||
import WalletIcon from './WalletIcon'
|
||||
import useLargestAccounts from '../hooks/useLargestAccounts'
|
||||
|
||||
const StyledModalWrapper = styled.div`
|
||||
height: 414px;
|
||||
${tw`bg-bkg-2 flex flex-col items-center relative rounded-lg shadow-lg p-7 w-96`}
|
||||
`
|
||||
type StyledModalBorderProps = {
|
||||
animate: boolean
|
||||
}
|
||||
|
||||
const StyledModalBorder = styled.div<StyledModalBorderProps>`
|
||||
height: 416px;
|
||||
width: 386px;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
z-index: -1;
|
||||
position: absolute;
|
||||
|
||||
:after {
|
||||
content: '';
|
||||
background-size: 300% 300%;
|
||||
${tw`bg-bkg-3 absolute top-0 left-0 right-0 bottom-0 rounded-lg`}
|
||||
|
||||
${({ animate }) =>
|
||||
animate &&
|
||||
tw`animate-gradient opacity-60 bg-gradient-to-br from-secondary-1-light via-secondary-3-light to-primary-light`}
|
||||
}
|
||||
`
|
||||
|
||||
const ContributionModal = () => {
|
||||
const actions = useWalletStore((s) => s.actions)
|
||||
const connected = useWalletStore((s) => s.connected)
|
||||
const wallet = useWalletStore((s) => s.current)
|
||||
const largestAccounts = useLargestAccounts()
|
||||
|
||||
const usdcBalance = largestAccounts.usdc?.balance || 0
|
||||
const redeemableBalance = largestAccounts.redeemable?.balance || 0
|
||||
const totalBalance = usdcBalance + redeemableBalance
|
||||
|
||||
const [walletAmount, setWalletAmount] = useState(0)
|
||||
const [contributionAmount, setContributionAmount] = useState(0)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
const [editContribution, setEditContribution] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [maxButtonTransition, setMaxButtonTransition] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
console.log('refresh modal on balance change')
|
||||
setWalletAmount(usdcBalance)
|
||||
setContributionAmount(redeemableBalance)
|
||||
if (redeemableBalance > 0) {
|
||||
setSubmitted(true)
|
||||
}
|
||||
}, [totalBalance])
|
||||
|
||||
const handleConnectDisconnect = () => {
|
||||
if (connected) {
|
||||
setSubmitted(false)
|
||||
setEditContribution(false)
|
||||
wallet.disconnect()
|
||||
} else {
|
||||
wallet.connect()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSetContribution = () => {
|
||||
setSubmitting(true)
|
||||
setEditContribution(false)
|
||||
}
|
||||
|
||||
const handleEditContribution = () => {
|
||||
setEditContribution(true)
|
||||
setSubmitted(false)
|
||||
}
|
||||
|
||||
const onChangeAmountInput = (amount) => {
|
||||
setWalletAmount(totalBalance - amount)
|
||||
setContributionAmount(amount)
|
||||
}
|
||||
|
||||
const onChangeSlider = (percentage) => {
|
||||
const newContribution = Math.round(percentage * totalBalance) / 100
|
||||
setWalletAmount(totalBalance - newContribution)
|
||||
setContributionAmount(newContribution)
|
||||
}
|
||||
|
||||
const handleMax = () => {
|
||||
setWalletAmount(0)
|
||||
setContributionAmount(totalBalance)
|
||||
setMaxButtonTransition(true)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (maxButtonTransition) {
|
||||
setMaxButtonTransition(false)
|
||||
}
|
||||
}, [maxButtonTransition])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
if (largestAccounts.usdc) {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [largestAccounts])
|
||||
|
||||
useEffect(() => {
|
||||
if (submitting) {
|
||||
(async () => {
|
||||
await actions.submitContribution(contributionAmount)
|
||||
setSubmitted(true)
|
||||
setSubmitting(false)
|
||||
})()
|
||||
}
|
||||
}, [submitting])
|
||||
|
||||
const disableFormInputs = submitted || !connected || loading
|
||||
|
||||
return (
|
||||
<div className="relative z-10">
|
||||
<StyledModalWrapper>
|
||||
<div className="pb-4 text-center">
|
||||
{!submitted && !submitting && !editContribution ? (
|
||||
<>
|
||||
<h2>Plant your seed</h2>
|
||||
<p>This is the start of something big.</p>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{!submitted && submitting ? (
|
||||
<>
|
||||
<h2>Approve the transaction</h2>
|
||||
<p>Almost there...</p>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{submitted && !submitting ? (
|
||||
<>
|
||||
<h2>Your contribution amount</h2>
|
||||
<p>A new seed planted...</p>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{editContribution && !submitting ? (
|
||||
<>
|
||||
<h2>Funds unlocked</h2>
|
||||
<p>Increase or reduce your contribution...</p>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
{submitting ? (
|
||||
<div className="flex items-center h-full">
|
||||
<Loading className="h-6 w-6 mb-3 text-primary-light" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={`${
|
||||
connected ? 'opacity-100' : 'opacity-30'
|
||||
} pb-6 transiton-all duration-1000 w-full`}
|
||||
>
|
||||
<div className="flex justify-between pb-2">
|
||||
<div className="flex items-center text-xs text-fgd-4">
|
||||
<WalletIcon className="w-4 h-4 mr-1 text-fgd-3 fill-current" />
|
||||
{connected ? (
|
||||
loading ? (
|
||||
<div className="bg-bkg-4 rounded w-10 h-4 animate-pulse" />
|
||||
) : (
|
||||
<span className="font-display text-fgd-1 ml-1">
|
||||
{walletAmount}
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
'----'
|
||||
)}
|
||||
<img
|
||||
alt=""
|
||||
width="16"
|
||||
height="16"
|
||||
src="/icons/usdc.svg"
|
||||
className={`ml-1`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
{submitted ? (
|
||||
<Button
|
||||
className="ring-1 ring-secondary-1-light ring-inset hover:ring-secondary-1-dark hover:bg-transparent hover:text-secondary-1-dark font-normal rounded text-secondary-1-light text-xs py-0.5 px-1.5 mr-2"
|
||||
disabled={!connected}
|
||||
onClick={() => handleEditContribution()}
|
||||
secondary
|
||||
>
|
||||
Unlock
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
className={`${
|
||||
submitted && 'opacity-30'
|
||||
} bg-bkg-4 font-normal rounded text-fgd-3 text-xs py-0.5 px-1.5`}
|
||||
disabled={disableFormInputs}
|
||||
onClick={() => handleMax()}
|
||||
secondary
|
||||
>
|
||||
Max
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center pb-4 relative">
|
||||
{submitted ? (
|
||||
<LockClosedIcon className="absolute text-secondary-2-light h-4 w-4 mb-0.5 left-2 z-10" />
|
||||
) : null}
|
||||
{editContribution ? (
|
||||
<LockOpenIcon className="absolute text-secondary-1-light h-4 w-4 mb-0.5 left-2 z-10" />
|
||||
) : null}
|
||||
<Input
|
||||
className={(submitted || editContribution) && 'pl-7'}
|
||||
disabled={disableFormInputs}
|
||||
type="text"
|
||||
onChange={(e) => onChangeAmountInput(e.target.value)}
|
||||
value={loading ? '' : contributionAmount}
|
||||
suffix="USDC"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
!submitted ? 'opacity-100' : 'opacity-30'
|
||||
} transiton-all duration-1000`}
|
||||
>
|
||||
<div className="pb-20">
|
||||
<Slider
|
||||
disabled={disableFormInputs}
|
||||
value={(100 * contributionAmount) / totalBalance}
|
||||
onChange={(v) => onChangeSlider(v)}
|
||||
step={1}
|
||||
maxButtonTransition={maxButtonTransition}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => handleSetContribution()}
|
||||
className="w-full py-2.5"
|
||||
disabled={disableFormInputs || walletAmount < 0}
|
||||
>
|
||||
<div className={`flex items-center justify-center`}>
|
||||
Set Contribution
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{connected ? (
|
||||
<Button
|
||||
className="rounded-full bg-bkg-4 text-fgd-3 font-normal"
|
||||
onClick={() => handleConnectDisconnect()}
|
||||
secondary
|
||||
>
|
||||
<div className="flex items-center text-sm">
|
||||
<LinkIcon className="h-4 w-4 mr-1" />
|
||||
Disconnect
|
||||
</div>
|
||||
</Button>
|
||||
) : (
|
||||
<ConnectWalletButtonSmall onClick={handleConnectDisconnect}>
|
||||
<div className="flex items-center justify-center text-sm">
|
||||
<LinkIcon className="h-4 w-4 mr-1" />
|
||||
Connect Wallet
|
||||
</div>
|
||||
</ConnectWalletButtonSmall>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</StyledModalWrapper>
|
||||
<StyledModalBorder animate={submitted && connected} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContributionModal
|
|
@ -0,0 +1,52 @@
|
|||
interface InputProps {
|
||||
type: string
|
||||
value: any
|
||||
onChange: (e) => void
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
[x: string]: any
|
||||
}
|
||||
|
||||
const Input = ({
|
||||
type,
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
wrapperClassName = 'w-full',
|
||||
disabled,
|
||||
prefix,
|
||||
suffix,
|
||||
...props
|
||||
}: InputProps) => {
|
||||
return (
|
||||
<div className={`flex relative ${wrapperClassName}`}>
|
||||
{prefix ? (
|
||||
<div
|
||||
className="flex items-center justify-end p-2 border border-r-0
|
||||
border-fgd-4 bg-bkg-2 h-full text-xs rounded rounded-r-none w-14 text-right"
|
||||
>
|
||||
{prefix}
|
||||
</div>
|
||||
) : null}
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className={`${className} font-display px-2 py-2 w-full bg-bkg-1 rounded text-fgd-1
|
||||
border border-fgd-4 default-transition hover:border-primary-dark
|
||||
focus:border-primary-light focus:outline-none
|
||||
${disabled ? 'cursor-not-allowed hover:border-fgd-4 text-fgd-3' : ''}
|
||||
${prefix ? 'rounded-l-none' : ''}`}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
/>
|
||||
{suffix ? (
|
||||
<span className="absolute right-0 text-xs flex items-center pr-2 h-full bg-transparent text-fgd-4">
|
||||
{suffix}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Input
|
|
@ -0,0 +1,32 @@
|
|||
import { FunctionComponent } from 'react'
|
||||
|
||||
interface LoadingProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Loading: FunctionComponent<LoadingProps> = ({ className }) => {
|
||||
return (
|
||||
<svg
|
||||
className={`${className} animate-spin h-5 w-5`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className={`opacity-25`}
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className={`opacity-90`}
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading
|
|
@ -53,30 +53,30 @@ const Notification = ({ type, message, description, txid }) => {
|
|||
|
||||
return (
|
||||
<div
|
||||
className={`max-w-sm w-full bg-th-bkg-3 shadow-lg rounded-md mt-2 pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden`}
|
||||
className={`max-w-sm w-full bg-bkg-3 shadow-lg rounded-md mt-2 pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden`}
|
||||
>
|
||||
<div className={`p-4`}>
|
||||
<div className={`flex items-center`}>
|
||||
<div className={`flex-shrink-0`}>
|
||||
{type === 'success' ? (
|
||||
<CheckCircleIcon className={`text-th-green h-9 w-9 mr-1`} />
|
||||
<CheckCircleIcon className={`text-green h-9 w-9 mr-1`} />
|
||||
) : null}
|
||||
{type === 'info' && (
|
||||
<XCircleIcon className={`text-th-primary h-9 w-9 mr-1`} />
|
||||
<XCircleIcon className={`text-primary h-9 w-9 mr-1`} />
|
||||
)}
|
||||
{type === 'error' && (
|
||||
<InformationCircleIcon className={`text-th-red h-9 w-9 mr-1`} />
|
||||
<InformationCircleIcon className={`text-red h-9 w-9 mr-1`} />
|
||||
)}
|
||||
</div>
|
||||
<div className={`ml-2 w-0 flex-1`}>
|
||||
<div className={`text-lg text-th-fgd-1`}>{message}</div>
|
||||
<div className={`text-lg text-fgd-1`}>{message}</div>
|
||||
{description ? (
|
||||
<p className={`mt-0.5 text-base text-th-fgd-2`}>{description}</p>
|
||||
<p className={`mt-0.5 text-base text-fgd-2`}>{description}</p>
|
||||
) : null}
|
||||
{txid ? (
|
||||
<a
|
||||
href={'https://explorer.solana.com/tx/' + txid}
|
||||
className="text-th-primary"
|
||||
className="text-primary"
|
||||
>
|
||||
View transaction {txid.slice(0, 8)}...
|
||||
{txid.slice(txid.length - 8)}
|
||||
|
@ -86,7 +86,7 @@ const Notification = ({ type, message, description, txid }) => {
|
|||
<div className={`ml-4 flex-shrink-0 self-start flex`}>
|
||||
<button
|
||||
onClick={() => setShowNotification(false)}
|
||||
className={`bg-th-bkg-3 rounded-md inline-flex text-fgd-3 hover:text-th-fgd-4 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-th-primary`}
|
||||
className={`bg-bkg-3 rounded-md inline-flex text-fgd-3 hover:text-fgd-4 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary`}
|
||||
>
|
||||
<span className={`sr-only`}>Close</span>
|
||||
<svg
|
||||
|
|
|
@ -0,0 +1,192 @@
|
|||
import { FunctionComponent, useEffect, useState } from 'react'
|
||||
import tw from 'twin.macro'
|
||||
import styled from '@emotion/styled'
|
||||
import Slider from 'rc-slider'
|
||||
import 'rc-slider/assets/index.css'
|
||||
|
||||
type StyledSliderProps = {
|
||||
enableTransition?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const StyledSlider = styled(Slider)<StyledSliderProps>`
|
||||
.rc-slider-rail {
|
||||
${tw`bg-gradient-to-r from-secondary-1-light via-primary-light to-secondary-2-light h-2.5 rounded-full`}
|
||||
}
|
||||
|
||||
.rc-slider-track {
|
||||
${tw`bg-gradient-to-r from-secondary-1-dark via-primary-light to-secondary-2-light h-2.5 rounded-full ring-1 ring-primary-light ring-inset`}
|
||||
|
||||
${({ enableTransition }) =>
|
||||
enableTransition && tw`transition-all duration-500`}
|
||||
}
|
||||
|
||||
.rc-slider-step {
|
||||
${tw`hidden`}
|
||||
}
|
||||
|
||||
.rc-slider-handle {
|
||||
${tw`bg-fgd-1 border-4 border-primary-dark h-4 w-4`}
|
||||
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.3);
|
||||
margin-top: -3px;
|
||||
|
||||
${({ enableTransition }) =>
|
||||
enableTransition && tw`transition-all duration-500`}
|
||||
}
|
||||
|
||||
.rc-slider-mark-text {
|
||||
${tw`font-display transition-all duration-300 text-fgd-2 hover:text-primary-light`}
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.rc-slider-mark-text-active {
|
||||
${tw`opacity-60 hover:opacity-100`}
|
||||
}
|
||||
|
||||
.rc-slider-mark-text:first-of-type {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.rc-slider-mark-text:last-of-type {
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
${({ disabled }) => disabled && 'background-color: transparent'}
|
||||
`
|
||||
|
||||
const StyledSliderButtonWrapper = styled.div`
|
||||
${tw`absolute left-0 top-4 w-full`}
|
||||
`
|
||||
|
||||
type StyledSliderButtonProps = {
|
||||
styleValue: number
|
||||
sliderValue: number
|
||||
}
|
||||
|
||||
const StyledSliderButton = styled.button<StyledSliderButtonProps>`
|
||||
${tw`bg-none font-display transition-all duration-300 hover:text-primary-light focus:outline-none`}
|
||||
font-size: 0.6rem;
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
left: 0%;
|
||||
|
||||
:nth-of-type(2) {
|
||||
left: 23%;
|
||||
transform: translateX(-23%);
|
||||
}
|
||||
|
||||
:nth-of-type(3) {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
:nth-of-type(4) {
|
||||
left: 76%;
|
||||
transform: translateX(-76%);
|
||||
}
|
||||
|
||||
:nth-of-type(5) {
|
||||
left: 100%;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
${({ styleValue, sliderValue }) => styleValue < sliderValue && tw`opacity-40`}
|
||||
${({ styleValue, sliderValue }) =>
|
||||
styleValue === sliderValue && tw`animate-pulse text-primary-light`}
|
||||
`
|
||||
|
||||
type SliderProps = {
|
||||
onChange: (...args: any[]) => any
|
||||
step: number
|
||||
value: number
|
||||
disabled: boolean
|
||||
max?: number
|
||||
maxButtonTransition?: boolean
|
||||
}
|
||||
|
||||
const AmountSlider: FunctionComponent<SliderProps> = ({
|
||||
onChange,
|
||||
step,
|
||||
value,
|
||||
disabled,
|
||||
max,
|
||||
maxButtonTransition,
|
||||
}) => {
|
||||
const [enableTransition, setEnableTransition] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (maxButtonTransition) {
|
||||
setEnableTransition(true)
|
||||
}
|
||||
}, [maxButtonTransition])
|
||||
|
||||
useEffect(() => {
|
||||
if (enableTransition) {
|
||||
const transitionTimer = setTimeout(() => {
|
||||
setEnableTransition(false)
|
||||
}, 500)
|
||||
return () => clearTimeout(transitionTimer)
|
||||
}
|
||||
}, [enableTransition])
|
||||
|
||||
const handleSliderButtonClick = (value) => {
|
||||
onChange(value)
|
||||
setEnableTransition(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<StyledSlider
|
||||
min={0}
|
||||
max={max}
|
||||
value={value || 0}
|
||||
onChange={onChange}
|
||||
step={step}
|
||||
enableTransition={enableTransition}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<StyledSliderButtonWrapper>
|
||||
<StyledSliderButton
|
||||
onClick={() => handleSliderButtonClick(0)}
|
||||
styleValue={0}
|
||||
sliderValue={value}
|
||||
>
|
||||
0%
|
||||
</StyledSliderButton>
|
||||
<StyledSliderButton
|
||||
onClick={() => handleSliderButtonClick(25)}
|
||||
styleValue={25}
|
||||
sliderValue={value}
|
||||
>
|
||||
25%
|
||||
</StyledSliderButton>
|
||||
<StyledSliderButton
|
||||
onClick={() => handleSliderButtonClick(50)}
|
||||
styleValue={50}
|
||||
sliderValue={value}
|
||||
>
|
||||
50%
|
||||
</StyledSliderButton>
|
||||
<StyledSliderButton
|
||||
onClick={() => handleSliderButtonClick(75)}
|
||||
styleValue={75}
|
||||
sliderValue={value}
|
||||
>
|
||||
75%
|
||||
</StyledSliderButton>
|
||||
<StyledSliderButton
|
||||
onClick={() => handleSliderButtonClick(100)}
|
||||
styleValue={100}
|
||||
sliderValue={value}
|
||||
>
|
||||
100%
|
||||
</StyledSliderButton>
|
||||
</StyledSliderButtonWrapper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AmountSlider
|
|
@ -51,7 +51,7 @@ const TopBar = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<nav className={`bg-th-bkg-2`}>
|
||||
<nav className={`bg-bkg-1`}>
|
||||
<div className={`px-4 sm:px-6 lg:px-8`}>
|
||||
<div className={`flex justify-between h-16`}>
|
||||
<div className={`flex`}>
|
||||
|
@ -66,11 +66,11 @@ const TopBar = () => {
|
|||
<Menu>
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<Menu.Button className="w-48 h-11 pl-2 pr-2.5 border border-th-green hover:border-th-fgd-1 focus:outline-none rounded-md text-th-fgd-4 hover:text-th-fgd-1">
|
||||
<Menu.Button className="w-48 h-11 pl-2 pr-2.5 border border-green hover:border-fgd-1 focus:outline-none rounded-md text-fgd-4 hover:text-fgd-1">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<WalletIcon className="w-5 h-5 mr-2 fill-current text-th-green" />
|
||||
<Code className="p-1 text-th-fgd-3 font-light">
|
||||
<WalletIcon className="w-5 h-5 mr-2 fill-current text-green" />
|
||||
<Code className="p-1 text-fgd-3 font-light">
|
||||
{isCopied
|
||||
? 'Copied!'
|
||||
: wallet.publicKey.toString().substr(0, 5) +
|
||||
|
@ -85,11 +85,11 @@ const TopBar = () => {
|
|||
)}
|
||||
</div>
|
||||
</Menu.Button>
|
||||
<Menu.Items className="z-20 p-1 absolute right-0 top-11 bg-th-bkg-1 divide-y divide-th-bkg-3 shadow-lg outline-none rounded-md w-48">
|
||||
<Menu.Items className="z-20 p-1 absolute right-0 top-11 bg-bkg-1 divide-y divide-bkg-3 shadow-lg outline-none rounded-md w-48">
|
||||
{WALLET_OPTIONS.map(({ name, icon }) => (
|
||||
<Menu.Item key={name}>
|
||||
<button
|
||||
className="flex flex-row items-center w-full p-2 hover:bg-th-bkg-2 hover:cursor-pointer font-normal"
|
||||
className="flex flex-row items-center w-full p-2 hover:bg-bkg-2 hover:cursor-pointer font-normal"
|
||||
onClick={() => handleWalletMenu(name)}
|
||||
>
|
||||
<div className="w-5 h-5 mr-2">{icon}</div>
|
||||
|
|
|
@ -27,10 +27,10 @@ export default function WalletSelect({ isPrimary = false }) {
|
|||
{({ open }) => (
|
||||
<>
|
||||
<Menu.Button
|
||||
className={`flex justify-center items-center h-full rounded-r rounded-l-none focus:outline-none text-th-primary hover:text-th-fgd-1 ${
|
||||
className={`flex justify-center items-center h-full rounded-r rounded-l-none focus:outline-none text-primary hover:text-fgd-1 ${
|
||||
isPrimary
|
||||
? 'px-3 hover:bg-th-primary'
|
||||
: 'px-2 hover:bg-th-bkg-3 border-l border-th-fgd-4'
|
||||
? 'px-3 hover:bg-primary'
|
||||
: 'px-2 hover:bg-bkg-3 border-l border-fgd-4'
|
||||
} cursor-pointer`}
|
||||
>
|
||||
{open ? (
|
||||
|
@ -39,11 +39,11 @@ export default function WalletSelect({ isPrimary = false }) {
|
|||
<ChevronDownIcon className="h-5 w-5" />
|
||||
)}
|
||||
</Menu.Button>
|
||||
<Menu.Items className="z-20 p-1 absolute right-0 top-11 bg-th-bkg-1 divide-y divide-th-bkg-3 shadow-lg outline-none rounded-md w-48">
|
||||
<Menu.Items className="z-20 p-1 absolute right-0 top-11 bg-bkg-1 divide-y divide-bkg-3 shadow-lg outline-none rounded-md w-48">
|
||||
{WALLET_PROVIDERS.map(({ name, url, icon }) => (
|
||||
<Menu.Item key={name}>
|
||||
<button
|
||||
className="flex flex-row items-center justify-between w-full p-2 hover:bg-th-bkg-2 hover:cursor-pointer font-normal focus:outline-none"
|
||||
className="flex flex-row items-center justify-between w-full p-2 hover:bg-bkg-2 hover:cursor-pointer font-normal focus:outline-none"
|
||||
onClick={() => handleSelectProvider(url)}
|
||||
>
|
||||
<div className="flex">
|
||||
|
@ -51,7 +51,7 @@ export default function WalletSelect({ isPrimary = false }) {
|
|||
{name}
|
||||
</div>
|
||||
{savedProviderUrl === url ? (
|
||||
<CheckCircleIcon className="h-4 w-4 text-th-green" />
|
||||
<CheckCircleIcon className="h-4 w-4 text-green" />
|
||||
) : null}{' '}
|
||||
</button>
|
||||
</Menu.Item>
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import BN from 'bn.js'
|
||||
import useWalletStore from '../stores/useWalletStore'
|
||||
import { ProgramAccount, TokenAccount } from '../utils/tokens'
|
||||
|
||||
function fixedPointToNumber(value: BN, decimals: number) {
|
||||
const divisor = new BN(10).pow(new BN(decimals))
|
||||
const quotient = value.div(divisor)
|
||||
const remainder = value.mod(divisor)
|
||||
return quotient.toNumber() + remainder.toNumber() / divisor.toNumber()
|
||||
}
|
||||
|
||||
function calculateBalance(mints, account: TokenAccount): number {
|
||||
const mint = mints[account.mint.toBase58()]
|
||||
return mint ? fixedPointToNumber(account.amount, mint.decimals) : 0
|
||||
}
|
||||
|
||||
export function findLargestBalanceAccountForMint(
|
||||
mints,
|
||||
tokenAccounts: ProgramAccount<TokenAccount>[],
|
||||
mintPk
|
||||
) {
|
||||
const accounts = tokenAccounts.filter((a) => a.account.mint.equals(mintPk))
|
||||
if (!accounts.length) return undefined
|
||||
|
||||
const balances = accounts.map((a) => calculateBalance(mints, a.account))
|
||||
const maxBalanceAccountIndex = balances.reduce(
|
||||
(iMax, bal, iBal) => (bal > balances[iMax] ? iBal : iMax),
|
||||
0
|
||||
)
|
||||
const account = accounts[maxBalanceAccountIndex]
|
||||
const balance = balances[maxBalanceAccountIndex]
|
||||
|
||||
console.log(
|
||||
'findLargestBalanceAccountForMint',
|
||||
maxBalanceAccountIndex,
|
||||
account,
|
||||
balance
|
||||
)
|
||||
|
||||
return { account, balance }
|
||||
}
|
||||
|
||||
export default function useLargestAccounts() {
|
||||
const { pool, tokenAccounts, mints, usdcVault } = useWalletStore(
|
||||
(state) => state
|
||||
)
|
||||
const usdc = usdcVault
|
||||
? findLargestBalanceAccountForMint(mints, tokenAccounts, usdcVault.mint)
|
||||
: undefined
|
||||
const redeemable = pool
|
||||
? findLargestBalanceAccountForMint(
|
||||
mints,
|
||||
tokenAccounts,
|
||||
pool.redeemableMint
|
||||
)
|
||||
: undefined
|
||||
return { usdc, redeemable }
|
||||
}
|
|
@ -102,8 +102,11 @@ export default function useWallet() {
|
|||
'...' +
|
||||
wallet.publicKey.toString().substr(-5),
|
||||
})
|
||||
await actions.fetchWalletTokenAccounts()
|
||||
await actions.fetchWalletMints()
|
||||
await actions.fetchPool()
|
||||
await Promise.all([
|
||||
actions.fetchWalletTokenAccounts(),
|
||||
actions.fetchMints(),
|
||||
])
|
||||
})
|
||||
wallet.on('disconnect', () => {
|
||||
setWalletStore((state) => {
|
||||
|
@ -127,8 +130,7 @@ export default function useWallet() {
|
|||
}, [wallet, setWalletStore])
|
||||
|
||||
useInterval(async () => {
|
||||
await actions.fetchWalletTokenAccounts()
|
||||
await actions.fetchWalletMints()
|
||||
await actions.fetchUsdcVault()
|
||||
}, 20 * SECONDS)
|
||||
|
||||
return { connected, wallet }
|
||||
|
|
|
@ -0,0 +1,402 @@
|
|||
{
|
||||
"version": "0.0.0",
|
||||
"name": "ido_pool",
|
||||
"instructions": [
|
||||
{
|
||||
"name": "initializePool",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "poolAccount",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "poolSigner",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "redeemableMint",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "usdcMint",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "poolWatermelon",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "poolUsdc",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "distributionAuthority",
|
||||
"isMut": false,
|
||||
"isSigner": true
|
||||
},
|
||||
{
|
||||
"name": "creatorWatermelon",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "tokenProgram",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "rent",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "numIdoTokens",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "startIdoTs",
|
||||
"type": "i64"
|
||||
},
|
||||
{
|
||||
"name": "endDepositsTs",
|
||||
"type": "i64"
|
||||
},
|
||||
{
|
||||
"name": "endIdoTs",
|
||||
"type": "i64"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "exchangeUsdcForRedeemable",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "poolAccount",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "poolSigner",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "redeemableMint",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "poolUsdc",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "userAuthority",
|
||||
"isMut": false,
|
||||
"isSigner": true
|
||||
},
|
||||
{
|
||||
"name": "userUsdc",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "userRedeemable",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "tokenProgram",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "exchangeRedeemableForUsdc",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "poolAccount",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "poolSigner",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "redeemableMint",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "poolUsdc",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "userAuthority",
|
||||
"isMut": false,
|
||||
"isSigner": true
|
||||
},
|
||||
{
|
||||
"name": "userUsdc",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "userRedeemable",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "tokenProgram",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "exchangeRedeemableForWatermelon",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "poolAccount",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "poolSigner",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "redeemableMint",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "poolWatermelon",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "userAuthority",
|
||||
"isMut": false,
|
||||
"isSigner": true
|
||||
},
|
||||
{
|
||||
"name": "userWatermelon",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "userRedeemable",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "tokenProgram",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "withdrawPoolUsdc",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "poolAccount",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "poolSigner",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "poolUsdc",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "distributionAuthority",
|
||||
"isMut": false,
|
||||
"isSigner": true
|
||||
},
|
||||
{
|
||||
"name": "creatorUsdc",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "tokenProgram",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
}
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "PoolAccount",
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{
|
||||
"name": "redeemableMint",
|
||||
"type": "publicKey"
|
||||
},
|
||||
{
|
||||
"name": "poolWatermelon",
|
||||
"type": "publicKey"
|
||||
},
|
||||
{
|
||||
"name": "watermelonMint",
|
||||
"type": "publicKey"
|
||||
},
|
||||
{
|
||||
"name": "poolUsdc",
|
||||
"type": "publicKey"
|
||||
},
|
||||
{
|
||||
"name": "distributionAuthority",
|
||||
"type": "publicKey"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "numIdoTokens",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "startIdoTs",
|
||||
"type": "i64"
|
||||
},
|
||||
{
|
||||
"name": "endDepositsTs",
|
||||
"type": "i64"
|
||||
},
|
||||
{
|
||||
"name": "endIdoTs",
|
||||
"type": "i64"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": 300,
|
||||
"name": "IdoFuture",
|
||||
"msg": "IDO must start in the future"
|
||||
},
|
||||
{
|
||||
"code": 301,
|
||||
"name": "SeqTimes",
|
||||
"msg": "IDO times are non-sequential"
|
||||
},
|
||||
{
|
||||
"code": 302,
|
||||
"name": "StartIdoTime",
|
||||
"msg": "IDO has not started"
|
||||
},
|
||||
{
|
||||
"code": 303,
|
||||
"name": "EndDepositsTime",
|
||||
"msg": "Deposits period has ended"
|
||||
},
|
||||
{
|
||||
"code": 304,
|
||||
"name": "EndIdoTime",
|
||||
"msg": "IDO has ended"
|
||||
},
|
||||
{
|
||||
"code": 305,
|
||||
"name": "IdoNotOver",
|
||||
"msg": "IDO has not finished yet"
|
||||
},
|
||||
{
|
||||
"code": 306,
|
||||
"name": "LowUsdc",
|
||||
"msg": "Insufficient USDC"
|
||||
},
|
||||
{
|
||||
"code": 307,
|
||||
"name": "LowRedeemable",
|
||||
"msg": "Insufficient redeemable tokens"
|
||||
},
|
||||
{
|
||||
"code": 308,
|
||||
"name": "UsdcNotEqRedeem",
|
||||
"msg": "USDC total and redeemable total don't match"
|
||||
},
|
||||
{
|
||||
"code": 309,
|
||||
"name": "InvalidNonce",
|
||||
"msg": "Given nonce is invalid"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"address": "2oBtRS2AAQfsMxXQfg41fKFY9zjvHwSSD7G5idrCFziV"
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
[build]
|
||||
command = "npm run build"
|
||||
command = "yarn build"
|
||||
publish = "out"
|
||||
|
||||
[[plugins]]
|
||||
package = "@netlify/plugin-nextjs"
|
||||
|
||||
|
|
|
@ -5,6 +5,9 @@ module.exports = {
|
|||
test: /\.svg$/,
|
||||
use: ['@svgr/webpack'],
|
||||
})
|
||||
config.node = {
|
||||
fs: 'empty',
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
|
|
|
@ -25,17 +25,20 @@
|
|||
"@emotion/styled": "^11.3.0",
|
||||
"@headlessui/react": "^1.0.0",
|
||||
"@heroicons/react": "^1.0.1",
|
||||
"@project-serum/anchor": "^0.10.0",
|
||||
"@project-serum/sol-wallet-adapter": "^0.2.0",
|
||||
"@solana/spl-token": "^0.1.3",
|
||||
"@solana/web3.js": "^1.5.0",
|
||||
"immer": "^9.0.1",
|
||||
"next": "latest",
|
||||
"next-themes": "^0.0.14",
|
||||
"rc-slider": "^9.7.2",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"zustand": "^3.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@emotion/babel-preset-css-prop": "^11.2.0",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "^14.14.25",
|
||||
|
@ -56,6 +59,12 @@
|
|||
"postcss-preset-env": "^6.7.0",
|
||||
"prettier": "^2.0.2",
|
||||
"tailwindcss": "^2.1.2",
|
||||
"twin.macro": "^2.4.0",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"babelMacros": {
|
||||
"twin": {
|
||||
"preset": "emotion"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ function App({ Component, pageProps }) {
|
|||
<title>{title}</title>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap"
|
||||
href="https://fonts.googleapis.com/css2?family=Lato:wght@400;700&family=PT+Mono&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import Notifications from '../components/Notification'
|
||||
import Balances from '../components/Balances'
|
||||
import TopBar from '../components/TopBar'
|
||||
import ContributionModal from '../components/ContributionModal'
|
||||
|
||||
const Index = () => {
|
||||
return (
|
||||
<div className={`bg-th-bkg-1 text-th-fgd-1 transition-all `}>
|
||||
<div className={`bg-bkg-1 text-fgd-1 transition-all`}>
|
||||
<TopBar />
|
||||
<Balances />
|
||||
<Notifications />
|
||||
<div className="grid grid-cols-1 place-items-center">
|
||||
<ContributionModal />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#2775CA;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g>
|
||||
<circle class="st0" cx="12" cy="12" r="12"/>
|
||||
<path class="st1" d="M9.8,20.3c0,0.3-0.2,0.4-0.5,0.4C5.6,19.5,3,16.1,3,12.1c0-4,2.6-7.4,6.3-8.6c0.3-0.1,0.5,0.1,0.5,0.4v0.7
|
||||
c0,0.2-0.1,0.4-0.3,0.5c-2.9,1.1-4.9,3.8-4.9,7c0,3.2,2.1,6,4.9,7c0.2,0.1,0.3,0.3,0.3,0.5V20.3z"/>
|
||||
<path class="st1" d="M12.8,17.8c0,0.2-0.2,0.4-0.4,0.4h-0.8c-0.2,0-0.4-0.2-0.4-0.4v-1.2c-1.6-0.2-2.4-1.1-2.7-2.4
|
||||
c0-0.2,0.1-0.4,0.3-0.4h0.9c0.2,0,0.3,0.1,0.4,0.3c0.2,0.7,0.6,1.3,1.9,1.3c1,0,1.7-0.5,1.7-1.3c0-0.8-0.4-1.1-1.8-1.3
|
||||
c-2.1-0.3-3.1-0.9-3.1-2.6c0-1.3,1-2.3,2.4-2.5V6.5c0-0.2,0.2-0.4,0.4-0.4h0.8c0.2,0,0.4,0.2,0.4,0.4v1.2c1.2,0.2,2,0.9,2.2,2
|
||||
c0,0.2-0.1,0.4-0.3,0.4h-0.8c-0.2,0-0.3-0.1-0.4-0.3c-0.2-0.7-0.7-1-1.6-1c-1,0-1.5,0.5-1.5,1.2c0,0.7,0.3,1.1,1.8,1.3
|
||||
c2.1,0.3,3.1,0.9,3.1,2.6c0,1.3-1,2.4-2.5,2.7V17.8z"/>
|
||||
<path class="st1" d="M14.7,20.7c-0.3,0.1-0.5-0.1-0.5-0.4v-0.7c0-0.2,0.1-0.4,0.3-0.5c2.9-1.1,4.9-3.8,4.9-7c0-3.2-2.1-6-4.9-7
|
||||
c-0.2-0.1-0.3-0.3-0.3-0.5V3.9c0-0.3,0.2-0.4,0.5-0.4c3.6,1.2,6.3,4.6,6.3,8.6C21,16.1,18.4,19.5,14.7,20.7z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -1,33 +1,69 @@
|
|||
import create, { State } from 'zustand'
|
||||
import produce from 'immer'
|
||||
import { Connection } from '@solana/web3.js'
|
||||
import { Connection, PublicKey, Transaction } from '@solana/web3.js'
|
||||
import * as anchor from '@project-serum/anchor'
|
||||
|
||||
import { EndpointInfo, WalletAdapter } from '../@types/types'
|
||||
|
||||
// @ts-ignore
|
||||
import poolIdl from '../idls/ido_pool'
|
||||
|
||||
import {
|
||||
getOwnedTokenAccounts,
|
||||
getMint,
|
||||
ProgramAccount,
|
||||
TokenAccount,
|
||||
MintAccount,
|
||||
getTokenAccount,
|
||||
} from '../utils/tokens'
|
||||
import { findLargestBalanceAccountForMint } from '../hooks/useLargestAccounts'
|
||||
import { TOKEN_PROGRAM_ID } from '@solana/spl-token'
|
||||
import { createAssociatedTokenAccount } from '../utils/associated'
|
||||
import { sendTransaction } from '../utils/send'
|
||||
|
||||
export const ENDPOINTS: EndpointInfo[] = [
|
||||
{
|
||||
name: 'mainnet-beta',
|
||||
url: 'https://solana-api.projectserum.com/',
|
||||
url: 'https://api.mainnet-beta.solana.com/',
|
||||
websocket: 'https://api.mainnet-beta.solana.com/',
|
||||
programId: '',
|
||||
poolKey: '',
|
||||
},
|
||||
{
|
||||
name: 'devnet',
|
||||
url: 'https://devnet.solana.com',
|
||||
websocket: 'https://devnet.solana.com',
|
||||
url: 'https://api.devnet.solana.com',
|
||||
websocket: 'https://api.devnet.solana.com',
|
||||
programId: '2oBtRS2AAQfsMxXQfg41fKFY9zjvHwSSD7G5idrCFziV',
|
||||
poolKey: 'ZfSZf2xrNLBrfY37TwJLoHv9qdKBQpkbZuPrq5FT8U9',
|
||||
},
|
||||
{
|
||||
name: 'localnet',
|
||||
url: 'http://localhost:8899',
|
||||
websocket: 'http://localhost:8899',
|
||||
programId: 'FF8zcQ1aEmyXeBt99hohoyYprgpEVmWsRK44qta3emno',
|
||||
poolKey: '8gswb9g1JdYEVj662KXr9p6p9SMgR77NryyqvWn9GPXJ',
|
||||
},
|
||||
]
|
||||
|
||||
const CLUSTER = 'mainnet-beta'
|
||||
const CLUSTER = 'devnet'
|
||||
const ENDPOINT = ENDPOINTS.find((e) => e.name === CLUSTER)
|
||||
const DEFAULT_CONNECTION = new Connection(ENDPOINT.url, 'recent')
|
||||
const WEBSOCKET_CONNECTION = new Connection(ENDPOINT.websocket, 'recent')
|
||||
const PROGRAM_ID = new PublicKey(ENDPOINT.programId)
|
||||
const POOL_PK = new PublicKey(ENDPOINT.poolKey)
|
||||
|
||||
interface PoolAccount {
|
||||
distributionAuthority: PublicKey
|
||||
endDepositsTs: anchor.BN
|
||||
endIdoTs: anchor.BN
|
||||
nonce: number
|
||||
numIdoTokens: anchor.BN
|
||||
poolUsdc: PublicKey
|
||||
poolWatermelon: PublicKey
|
||||
redeemableMint: PublicKey
|
||||
startIdoTs: anchor.BN
|
||||
watermelonMint: PublicKey
|
||||
}
|
||||
|
||||
interface WalletStore extends State {
|
||||
connected: boolean
|
||||
|
@ -36,9 +72,15 @@ interface WalletStore extends State {
|
|||
current: Connection
|
||||
websocket: Connection
|
||||
endpoint: string
|
||||
programId: PublicKey
|
||||
}
|
||||
current: WalletAdapter | undefined
|
||||
providerUrl: string
|
||||
provider: anchor.Provider | undefined
|
||||
program: anchor.Program | undefined
|
||||
pool: PoolAccount | undefined
|
||||
mangoVault: TokenAccount | undefined
|
||||
usdcVault: TokenAccount | undefined
|
||||
tokenAccounts: ProgramAccount<TokenAccount>[]
|
||||
mints: { [pubkey: string]: MintAccount }
|
||||
set: (x: any) => void
|
||||
|
@ -52,12 +94,52 @@ const useWalletStore = create<WalletStore>((set, get) => ({
|
|||
current: DEFAULT_CONNECTION,
|
||||
websocket: WEBSOCKET_CONNECTION,
|
||||
endpoint: ENDPOINT.url,
|
||||
programId: PROGRAM_ID,
|
||||
},
|
||||
current: null,
|
||||
providerUrl: null,
|
||||
provider: undefined,
|
||||
program: undefined,
|
||||
pool: undefined,
|
||||
mangoVault: undefined,
|
||||
usdcVault: undefined,
|
||||
tokenAccounts: [],
|
||||
mints: {},
|
||||
actions: {
|
||||
async fetchPool() {
|
||||
const connection = get().connection.current
|
||||
const connected = get().connected
|
||||
const wallet = get().current
|
||||
const programId = get().connection.programId
|
||||
|
||||
console.log('fetchPool', connected, poolIdl)
|
||||
if (connection && connected) {
|
||||
const provider = new anchor.Provider(
|
||||
connection,
|
||||
wallet,
|
||||
anchor.Provider.defaultOptions()
|
||||
)
|
||||
const program = new anchor.Program(poolIdl, programId, provider)
|
||||
const pool = (await program.account.poolAccount.fetch(
|
||||
POOL_PK
|
||||
)) as PoolAccount
|
||||
|
||||
const [usdcVault, mangoVault] = await Promise.all([
|
||||
getTokenAccount(connection, pool.poolUsdc),
|
||||
getTokenAccount(connection, pool.poolWatermelon),
|
||||
])
|
||||
|
||||
console.log({ program, pool, usdcVault, mangoVault })
|
||||
|
||||
set((state) => {
|
||||
state.provider = provider
|
||||
state.program = program
|
||||
state.pool = pool
|
||||
state.usdcVault = usdcVault.account
|
||||
state.mangoVault = mangoVault.account
|
||||
})
|
||||
}
|
||||
},
|
||||
async fetchWalletTokenAccounts() {
|
||||
const connection = get().connection.current
|
||||
const connected = get().connected
|
||||
|
@ -71,6 +153,8 @@ const useWalletStore = create<WalletStore>((set, get) => ({
|
|||
walletOwner
|
||||
)
|
||||
|
||||
console.log('fetchWalletTokenAccounts', ownedTokenAccounts)
|
||||
|
||||
set((state) => {
|
||||
state.tokenAccounts = ownedTokenAccounts
|
||||
})
|
||||
|
@ -80,31 +164,134 @@ const useWalletStore = create<WalletStore>((set, get) => ({
|
|||
})
|
||||
}
|
||||
},
|
||||
async fetchWalletMints() {
|
||||
async fetchUsdcVault() {
|
||||
const connection = get().connection.current
|
||||
const connected = get().connected
|
||||
const tokenAccounts = get().tokenAccounts
|
||||
const pool = get().pool
|
||||
const set = get().set
|
||||
|
||||
if (connected) {
|
||||
const fetchMints = tokenAccounts.map((a) =>
|
||||
getMint(connection, a.account.mint)
|
||||
)
|
||||
const mintResults = await Promise.all(fetchMints)
|
||||
if (!pool) return
|
||||
|
||||
const newMints: { [pubkey: string]: MintAccount } = {}
|
||||
mintResults.forEach(
|
||||
(m) => (newMints[m.publicKey.toBase58()] = m.account)
|
||||
)
|
||||
const { account: vault } = await getTokenAccount(
|
||||
connection,
|
||||
pool.poolUsdc
|
||||
)
|
||||
console.log('fetchUsdcVault', vault)
|
||||
|
||||
set((state) => {
|
||||
state.mints = newMints
|
||||
set((state) => {
|
||||
state.usdcVault = vault
|
||||
})
|
||||
},
|
||||
async fetchMints() {
|
||||
const connection = get().connection.current
|
||||
const pool = get().pool
|
||||
const mangoVault = get().mangoVault
|
||||
const usdcVault = get().usdcVault
|
||||
const set = get().set
|
||||
|
||||
const mintKeys = [mangoVault.mint, usdcVault.mint, pool.redeemableMint]
|
||||
const mints = await Promise.all(
|
||||
mintKeys.map((pk) => getMint(connection, pk))
|
||||
)
|
||||
console.log('fetchMints', mints)
|
||||
|
||||
set((state) => {
|
||||
for (const pa of mints) {
|
||||
state.mints[pa.publicKey.toBase58()] = pa.account
|
||||
console.log('mint', pa.publicKey.toBase58(), pa.account)
|
||||
}
|
||||
})
|
||||
},
|
||||
async submitContribution(amount: number) {
|
||||
console.log('submitContribution', amount)
|
||||
|
||||
const actions = get().actions
|
||||
await actions.fetchWalletTokenAccounts()
|
||||
|
||||
const {
|
||||
program,
|
||||
provider,
|
||||
pool,
|
||||
tokenAccounts,
|
||||
mints,
|
||||
usdcVault,
|
||||
current: wallet,
|
||||
connection: { current: connection },
|
||||
} = get()
|
||||
const usdcDecimals = mints[usdcVault.mint.toBase58()].decimals
|
||||
const redeemable = findLargestBalanceAccountForMint(
|
||||
mints,
|
||||
tokenAccounts,
|
||||
pool.redeemableMint
|
||||
)
|
||||
const usdc = findLargestBalanceAccountForMint(
|
||||
mints,
|
||||
tokenAccounts,
|
||||
usdcVault.mint
|
||||
)
|
||||
|
||||
const difference = amount - (redeemable?.balance || 0)
|
||||
const [poolSigner] = await anchor.web3.PublicKey.findProgramAddress(
|
||||
[pool.watermelonMint.toBuffer()],
|
||||
program.programId
|
||||
)
|
||||
|
||||
if (difference > 0) {
|
||||
const depositAmount = new anchor.BN(
|
||||
difference * Math.pow(10, usdcDecimals)
|
||||
)
|
||||
console.log(depositAmount.toString(), 'exchangeUsdcForReemable')
|
||||
|
||||
let redeemableAccPk = redeemable?.account?.publicKey
|
||||
const transaction = new Transaction()
|
||||
if (!redeemable) {
|
||||
const [ins, pk] = await createAssociatedTokenAccount(
|
||||
wallet.publicKey,
|
||||
wallet.publicKey,
|
||||
pool.redeemableMint
|
||||
)
|
||||
transaction.add(ins)
|
||||
redeemableAccPk = pk
|
||||
}
|
||||
transaction.add(
|
||||
program.instruction.exchangeUsdcForRedeemable(depositAmount, {
|
||||
accounts: {
|
||||
poolAccount: POOL_PK,
|
||||
poolSigner: poolSigner,
|
||||
redeemableMint: pool.redeemableMint,
|
||||
poolUsdc: pool.poolUsdc,
|
||||
userAuthority: provider.wallet.publicKey,
|
||||
userUsdc: usdc.account.publicKey,
|
||||
userRedeemable: redeemableAccPk,
|
||||
tokenProgram: TOKEN_PROGRAM_ID,
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
},
|
||||
})
|
||||
)
|
||||
await sendTransaction({ transaction, wallet, connection })
|
||||
} else if (difference < 0) {
|
||||
const withdrawAmount = new anchor.BN(
|
||||
difference * -1 * Math.pow(10, usdcDecimals)
|
||||
)
|
||||
console.log(withdrawAmount.toString(), 'exchangeRedeemableForUsdc')
|
||||
await program.rpc.exchangeRedeemableForUsdc(withdrawAmount, {
|
||||
accounts: {
|
||||
poolAccount: POOL_PK,
|
||||
poolSigner: poolSigner,
|
||||
redeemableMint: pool.redeemableMint,
|
||||
poolUsdc: pool.poolUsdc,
|
||||
userAuthority: provider.wallet.publicKey,
|
||||
userUsdc: usdc.account.publicKey,
|
||||
userRedeemable: redeemable.account.publicKey,
|
||||
tokenProgram: TOKEN_PROGRAM_ID,
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
set((state) => {
|
||||
state.mints = {}
|
||||
})
|
||||
console.log('difference = 0 no submission needed', difference)
|
||||
return
|
||||
}
|
||||
|
||||
await actions.fetchWalletTokenAccounts()
|
||||
},
|
||||
},
|
||||
set: (fn) => set(produce(fn)),
|
||||
|
|
|
@ -1,3 +1,33 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--primary: theme('colors.primary.light');
|
||||
}
|
||||
|
||||
/* base */
|
||||
|
||||
* {
|
||||
@apply m-0 p-0;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-bkg-1 text-fgd-1 text-base font-body tracking-wide;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-xl mb-1 font-bold;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply text-fgd-4 text-sm;
|
||||
}
|
||||
|
||||
button {
|
||||
@apply font-bold;
|
||||
}
|
||||
|
||||
.default-transition {
|
||||
@apply transition-all duration-300;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
// const colors = require('tailwindcss/colors')
|
||||
// const defaultTheme = require('tailwindcss/defaultTheme')
|
||||
|
||||
module.exports = {
|
||||
mode: 'jit',
|
||||
purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
|
||||
|
@ -11,7 +8,7 @@ module.exports = {
|
|||
darkMode: false,
|
||||
theme: {
|
||||
fontFamily: {
|
||||
display: ['Lato, sans-serif'],
|
||||
display: ['PT Mono, monospace'],
|
||||
body: ['Lato, sans-serif'],
|
||||
},
|
||||
extend: {
|
||||
|
@ -19,59 +16,42 @@ module.exports = {
|
|||
help: 'help',
|
||||
},
|
||||
colors: {
|
||||
'mango-orange': {
|
||||
DEFAULT: '#DFAB01',
|
||||
dark: '#CB9C01',
|
||||
primary: { light: '#F2C94C', dark: '#EEB91B' },
|
||||
'secondary-1': { light: '#AFD803', dark: '#6CBF00' },
|
||||
'secondary-2': { light: '#E54033', dark: '#C7251A' },
|
||||
'secondary-3': { light: '#026DF7', dark: '#0259CA' },
|
||||
'bkg-1': '#141125',
|
||||
'bkg-2': '#242132',
|
||||
'bkg-3': '#393549',
|
||||
'bkg-4': '#4F4B63',
|
||||
'fgd-1': '#F0EDFF',
|
||||
'fgd-2': '#FCFCFF',
|
||||
'fgd-3': '#B9B5CE',
|
||||
'fgd-4': '#706C81',
|
||||
},
|
||||
animation: {
|
||||
'connect-wallet-ping':
|
||||
'connect-wallet-ping 1.5s cubic-bezier(0, 0, 0.2, 1) infinite',
|
||||
gradient: 'gradient 4s ease-in-out infinite',
|
||||
},
|
||||
keyframes: {
|
||||
'connect-wallet-ping': {
|
||||
'75%, 100%': {
|
||||
transform: 'scale(1.06, 1.3)',
|
||||
opacity: '10%',
|
||||
},
|
||||
},
|
||||
'mango-yellow': '#F2C94C',
|
||||
'mango-red': '#E54033',
|
||||
'mango-green': '#AFD803',
|
||||
'mango-dark': {
|
||||
lighter: '#332F46',
|
||||
light: '#262337',
|
||||
DEFAULT: '#141026',
|
||||
gradient: {
|
||||
'0%': {
|
||||
'background-position': '15% 0%',
|
||||
},
|
||||
'50%': {
|
||||
'background-position': '85% 100%',
|
||||
},
|
||||
'100%': {
|
||||
'background-position': '15% 0%',
|
||||
},
|
||||
},
|
||||
'mango-med': {
|
||||
light: '#C2BDD9',
|
||||
DEFAULT: '#9490A6',
|
||||
dark: '#706C81',
|
||||
},
|
||||
'mango-light': {
|
||||
light: '#FCFCFF',
|
||||
DEFAULT: '#F0EDFF',
|
||||
dark: '#B9B5CE',
|
||||
},
|
||||
'mango-grey': {
|
||||
lighter: '#f7f7f7',
|
||||
light: '#e6e6e6',
|
||||
dark: '#092e34',
|
||||
darker: '#072428',
|
||||
darkest: '#061f23',
|
||||
},
|
||||
'mango-theme': {
|
||||
yellow: '#F2C94C',
|
||||
red: { DEFAULT: '#E54033', dark: '#C7251A' },
|
||||
green: { DEFAULT: '#AFD803', dark: '#91B503' },
|
||||
'bkg-1': '#141026',
|
||||
'bkg-2': '#1D1832',
|
||||
'bkg-3': '#322E47',
|
||||
'fgd-1': '#F0EDFF',
|
||||
'fgd-2': '#FCFCFF',
|
||||
'fgd-3': '#B9B5CE',
|
||||
'fgd-4': '#706C81',
|
||||
},
|
||||
'th-bkg-1': 'var(--bkg-1)',
|
||||
'th-bkg-2': 'var(--bkg-2)',
|
||||
'th-bkg-3': 'var(--bkg-3)',
|
||||
'th-fgd-1': 'var(--fgd-1)',
|
||||
'th-fgd-2': 'var(--fgd-2)',
|
||||
'th-fgd-3': 'var(--fgd-3)',
|
||||
'th-fgd-4': 'var(--fgd-4)',
|
||||
'th-primary': 'var(--primary)',
|
||||
'th-red': 'var(--red)',
|
||||
'th-red-dark': 'var(--red-dark)',
|
||||
'th-green': 'var(--green)',
|
||||
'th-green-dark': 'var(--green-dark)',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
import {
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
SYSVAR_RENT_PUBKEY,
|
||||
TransactionInstruction,
|
||||
} from '@solana/web3.js'
|
||||
import { TOKEN_PROGRAM_ID } from '@solana/spl-token'
|
||||
|
||||
const SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID: PublicKey = new PublicKey(
|
||||
'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'
|
||||
)
|
||||
|
||||
export async function findAssociatedTokenAddress(
|
||||
walletAddress: PublicKey,
|
||||
tokenMintAddress: PublicKey
|
||||
): Promise<PublicKey> {
|
||||
return (
|
||||
await PublicKey.findProgramAddress(
|
||||
[
|
||||
walletAddress.toBuffer(),
|
||||
TOKEN_PROGRAM_ID.toBuffer(),
|
||||
tokenMintAddress.toBuffer(),
|
||||
],
|
||||
SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID
|
||||
)
|
||||
)[0]
|
||||
}
|
||||
|
||||
export async function createAssociatedTokenAccount(
|
||||
fundingAddress: PublicKey,
|
||||
walletAddress: PublicKey,
|
||||
splTokenMintAddress: PublicKey
|
||||
): Promise<[TransactionInstruction, PublicKey]> {
|
||||
const associatedTokenAddress = await findAssociatedTokenAddress(
|
||||
walletAddress,
|
||||
splTokenMintAddress
|
||||
)
|
||||
const keys = [
|
||||
{
|
||||
pubkey: fundingAddress,
|
||||
isSigner: true,
|
||||
isWritable: true,
|
||||
},
|
||||
{
|
||||
pubkey: associatedTokenAddress,
|
||||
isSigner: false,
|
||||
isWritable: true,
|
||||
},
|
||||
{
|
||||
pubkey: walletAddress,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: splTokenMintAddress,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: SystemProgram.programId,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: TOKEN_PROGRAM_ID,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: SYSVAR_RENT_PUBKEY,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
]
|
||||
return [
|
||||
new TransactionInstruction({
|
||||
keys,
|
||||
programId: SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID,
|
||||
data: Buffer.from([]),
|
||||
}),
|
||||
associatedTokenAddress,
|
||||
]
|
||||
}
|
|
@ -0,0 +1,287 @@
|
|||
import { notify } from './notifications'
|
||||
import {
|
||||
Account,
|
||||
Commitment,
|
||||
Connection,
|
||||
RpcResponseAndContext,
|
||||
SimulatedTransactionResponse,
|
||||
Transaction,
|
||||
TransactionSignature,
|
||||
} from '@solana/web3.js'
|
||||
import Wallet from '@project-serum/sol-wallet-adapter'
|
||||
|
||||
class TransactionError extends Error {
|
||||
public txid: string
|
||||
constructor(message: string, txid?: string) {
|
||||
super(message)
|
||||
this.txid = txid
|
||||
}
|
||||
}
|
||||
|
||||
export async function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
export function getUnixTs() {
|
||||
return new Date().getTime() / 1000
|
||||
}
|
||||
|
||||
const DEFAULT_TIMEOUT = 30000
|
||||
|
||||
export async function sendTransaction({
|
||||
transaction,
|
||||
wallet,
|
||||
signers = [],
|
||||
connection,
|
||||
sendingMessage = 'Sending transaction...',
|
||||
successMessage = 'Transaction confirmed',
|
||||
timeout = DEFAULT_TIMEOUT,
|
||||
}: {
|
||||
transaction: Transaction
|
||||
wallet: Wallet
|
||||
signers?: Array<Account>
|
||||
connection: Connection
|
||||
sendingMessage?: string
|
||||
successMessage?: string
|
||||
timeout?: number
|
||||
}) {
|
||||
const signedTransaction = await signTransaction({
|
||||
transaction,
|
||||
wallet,
|
||||
signers,
|
||||
connection,
|
||||
})
|
||||
return await sendSignedTransaction({
|
||||
signedTransaction,
|
||||
connection,
|
||||
sendingMessage,
|
||||
successMessage,
|
||||
timeout,
|
||||
})
|
||||
}
|
||||
|
||||
export async function signTransaction({
|
||||
transaction,
|
||||
wallet,
|
||||
signers = [],
|
||||
connection,
|
||||
}: {
|
||||
transaction: Transaction
|
||||
wallet: Wallet
|
||||
signers?: Array<Account>
|
||||
connection: Connection
|
||||
}) {
|
||||
transaction.recentBlockhash = (
|
||||
await connection.getRecentBlockhash('max')
|
||||
).blockhash
|
||||
transaction.setSigners(wallet.publicKey, ...signers.map((s) => s.publicKey))
|
||||
if (signers.length > 0) {
|
||||
transaction.partialSign(...signers)
|
||||
}
|
||||
return await wallet.signTransaction(transaction)
|
||||
}
|
||||
|
||||
export async function signTransactions({
|
||||
transactionsAndSigners,
|
||||
wallet,
|
||||
connection,
|
||||
}: {
|
||||
transactionsAndSigners: {
|
||||
transaction: Transaction
|
||||
signers?: Array<Account>
|
||||
}[]
|
||||
wallet: Wallet
|
||||
connection: Connection
|
||||
}) {
|
||||
const blockhash = (await connection.getRecentBlockhash('max')).blockhash
|
||||
transactionsAndSigners.forEach(({ transaction, signers = [] }) => {
|
||||
transaction.recentBlockhash = blockhash
|
||||
transaction.setSigners(wallet.publicKey, ...signers.map((s) => s.publicKey))
|
||||
if (signers?.length > 0) {
|
||||
transaction.partialSign(...signers)
|
||||
}
|
||||
})
|
||||
return await wallet.signAllTransactions(
|
||||
transactionsAndSigners.map(({ transaction }) => transaction)
|
||||
)
|
||||
}
|
||||
|
||||
export async function sendSignedTransaction({
|
||||
signedTransaction,
|
||||
connection,
|
||||
sendingMessage = 'Sending transaction...',
|
||||
successMessage = 'Transaction confirmed',
|
||||
timeout = DEFAULT_TIMEOUT,
|
||||
}: {
|
||||
signedTransaction: Transaction
|
||||
connection: Connection
|
||||
sendingMessage?: string
|
||||
successMessage?: string
|
||||
timeout?: number
|
||||
}): Promise<string> {
|
||||
const rawTransaction = signedTransaction.serialize()
|
||||
const startTime = getUnixTs()
|
||||
notify({ message: sendingMessage })
|
||||
const txid: TransactionSignature = await connection.sendRawTransaction(
|
||||
rawTransaction,
|
||||
{
|
||||
skipPreflight: true,
|
||||
}
|
||||
)
|
||||
|
||||
console.log('Started awaiting confirmation for', txid)
|
||||
|
||||
let done = false
|
||||
;(async () => {
|
||||
while (!done && getUnixTs() - startTime < timeout) {
|
||||
connection.sendRawTransaction(rawTransaction, {
|
||||
skipPreflight: true,
|
||||
})
|
||||
await sleep(300)
|
||||
}
|
||||
})()
|
||||
try {
|
||||
await awaitTransactionSignatureConfirmation(txid, timeout, connection)
|
||||
} catch (err) {
|
||||
if (err.timeout) {
|
||||
throw new Error('Timed out awaiting confirmation on transaction')
|
||||
}
|
||||
let simulateResult: SimulatedTransactionResponse | null = null
|
||||
try {
|
||||
simulateResult = (
|
||||
await simulateTransaction(connection, signedTransaction, 'single')
|
||||
).value
|
||||
} catch (e) {
|
||||
console.log('Error: ', e)
|
||||
}
|
||||
if (simulateResult && simulateResult.err) {
|
||||
if (simulateResult.logs) {
|
||||
for (let i = simulateResult.logs.length - 1; i >= 0; --i) {
|
||||
const line = simulateResult.logs[i]
|
||||
if (line.startsWith('Program log: ')) {
|
||||
throw new TransactionError(
|
||||
'Transaction failed: ' + line.slice('Program log: '.length),
|
||||
txid
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new TransactionError(JSON.stringify(simulateResult.err), txid)
|
||||
}
|
||||
throw new TransactionError('Transaction failed', txid)
|
||||
} finally {
|
||||
done = true
|
||||
}
|
||||
notify({ message: successMessage, type: 'success', txid })
|
||||
|
||||
console.log('Latency', txid, getUnixTs() - startTime)
|
||||
return txid
|
||||
}
|
||||
|
||||
async function awaitTransactionSignatureConfirmation(
|
||||
txid: TransactionSignature,
|
||||
timeout: number,
|
||||
connection: Connection
|
||||
) {
|
||||
let done = false
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
// eslint-disable-next-line
|
||||
;(async () => {
|
||||
setTimeout(() => {
|
||||
if (done) {
|
||||
return
|
||||
}
|
||||
done = true
|
||||
console.log('Timed out for txid', txid)
|
||||
reject({ timeout: true })
|
||||
}, timeout)
|
||||
try {
|
||||
connection.onSignature(
|
||||
txid,
|
||||
(result) => {
|
||||
console.log('WS confirmed', txid, result)
|
||||
done = true
|
||||
if (result.err) {
|
||||
reject(result.err)
|
||||
} else {
|
||||
resolve(result)
|
||||
}
|
||||
},
|
||||
connection.commitment
|
||||
)
|
||||
console.log('Set up WS connection', txid)
|
||||
} catch (e) {
|
||||
done = true
|
||||
console.log('WS error in setup', txid, e)
|
||||
}
|
||||
while (!done) {
|
||||
// eslint-disable-next-line
|
||||
;(async () => {
|
||||
try {
|
||||
const signatureStatuses = await connection.getSignatureStatuses([
|
||||
txid,
|
||||
])
|
||||
const result = signatureStatuses && signatureStatuses.value[0]
|
||||
if (!done) {
|
||||
if (!result) {
|
||||
// console.log('REST null result for', txid, result);
|
||||
} else if (result.err) {
|
||||
console.log('REST error for', txid, result)
|
||||
done = true
|
||||
reject(result.err)
|
||||
}
|
||||
// @ts-ignore
|
||||
else if (
|
||||
!(
|
||||
result.confirmations ||
|
||||
result.confirmationStatus === 'confirmed' ||
|
||||
result.confirmationStatus === 'finalized'
|
||||
)
|
||||
) {
|
||||
console.log('REST not confirmed', txid, result)
|
||||
} else {
|
||||
console.log('REST confirmed', txid, result)
|
||||
done = true
|
||||
resolve(result)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (!done) {
|
||||
console.log('REST connection error: txid', txid, e)
|
||||
}
|
||||
}
|
||||
})()
|
||||
await sleep(300)
|
||||
}
|
||||
})()
|
||||
})
|
||||
done = true
|
||||
return result
|
||||
}
|
||||
|
||||
/** Copy of Connection.simulateTransaction that takes a commitment parameter. */
|
||||
async function simulateTransaction(
|
||||
connection: Connection,
|
||||
transaction: Transaction,
|
||||
commitment: Commitment
|
||||
): Promise<RpcResponseAndContext<SimulatedTransactionResponse>> {
|
||||
// @ts-ignore
|
||||
transaction.recentBlockhash = await connection._recentBlockhash(
|
||||
// @ts-ignore
|
||||
connection._disableBlockhashCaching
|
||||
)
|
||||
|
||||
const signData = transaction.serializeMessage()
|
||||
// @ts-ignore
|
||||
const wireTransaction = transaction._serialize(signData)
|
||||
const encodedTransaction = wireTransaction.toString('base64')
|
||||
const config: any = { encoding: 'base64', commitment }
|
||||
const args = [encodedTransaction, config]
|
||||
|
||||
// @ts-ignore
|
||||
const res = await connection._rpcRequest('simulateTransaction', args)
|
||||
if (res.error) {
|
||||
throw new Error('failed to simulate transaction: ' + res.error.message)
|
||||
}
|
||||
return res.result
|
||||
}
|
|
@ -42,6 +42,19 @@ export async function getMint(
|
|||
}
|
||||
}
|
||||
|
||||
export async function getTokenAccount(
|
||||
connection: Connection,
|
||||
publicKey: PublicKey
|
||||
): Promise<ProgramAccount<TokenAccount>> {
|
||||
const result = await connection.getAccountInfo(publicKey)
|
||||
const data = Buffer.from(result.data)
|
||||
const account = parseTokenAccountData(publicKey, data)
|
||||
return {
|
||||
publicKey,
|
||||
account,
|
||||
}
|
||||
}
|
||||
|
||||
// copied from @solana/spl-token
|
||||
|
||||
const TOKEN_PROGRAM_ID = new PublicKey(
|
||||
|
@ -57,7 +70,7 @@ function parseTokenAccountData(account: PublicKey, data: Buffer): TokenAccount {
|
|||
|
||||
if (accountInfo.delegateOption === 0) {
|
||||
accountInfo.delegate = null
|
||||
accountInfo.delegatedAmount = new u64()
|
||||
accountInfo.delegatedAmount = new u64(0)
|
||||
} else {
|
||||
accountInfo.delegate = new PublicKey(accountInfo.delegate)
|
||||
accountInfo.delegatedAmount = u64.fromBuffer(accountInfo.delegatedAmount)
|
||||
|
|
Loading…
Reference in New Issue