Merge pull request #1 from blockworks-foundation/contribution-modal

Contribution modal
This commit is contained in:
Maximilian Schneider 2021-07-05 15:39:35 +02:00 committed by GitHub
commit 655bb78305
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 2171 additions and 1144 deletions

View File

@ -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"]
// }

View File

@ -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"]
}
]
}
}

View File

@ -19,6 +19,8 @@ export interface EndpointInfo {
name: string
url: string
websocket: string
programId: string
poolKey: string
}
export interface WalletContextValues {

View File

@ -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

66
components/Button.tsx Normal file
View File

@ -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

View File

@ -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>
)

View File

@ -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

52
components/Input.tsx Normal file
View File

@ -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

32
components/Loading.tsx Normal file
View File

@ -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

View File

@ -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

192
components/Slider.tsx Normal file
View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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 }
}

View File

@ -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 }

402
idls/ido_pool.json Normal file
View File

@ -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"
}
}

View File

@ -1,7 +1,6 @@
[build]
command = "npm run build"
command = "yarn build"
publish = "out"
[[plugins]]
package = "@netlify/plugin-nextjs"
package = "@netlify/plugin-nextjs"

View File

@ -5,6 +5,9 @@ module.exports = {
test: /\.svg$/,
use: ['@svgr/webpack'],
})
config.node = {
fs: 'empty',
}
return config
},

View File

@ -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"
}
}
}

View File

@ -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" />

View File

@ -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>
)
}

21
public/icons/usdc.svg Normal file
View File

@ -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

View File

@ -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)),

View File

@ -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;
}

View File

@ -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)',
},
},
},

83
utils/associated.tsx Normal file
View File

@ -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,
]
}

287
utils/send.tsx Normal file
View File

@ -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
}

View File

@ -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)

1309
yarn.lock

File diff suppressed because it is too large Load Diff