connect to devnet contract

This commit is contained in:
Maximilian Schneider 2021-07-05 01:17:40 +02:00
parent 1d58b37d24
commit 816e82b867
8 changed files with 594 additions and 63 deletions

View File

@ -4,13 +4,13 @@ import tw from 'twin.macro'
import { LockClosedIcon, LockOpenIcon } from '@heroicons/react/outline'
import { LinkIcon } from '@heroicons/react/solid'
import useWalletStore from '../stores/useWalletStore'
import { getUsdcBalance } from '../utils'
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;
@ -40,9 +40,15 @@ const StyledModalBorder = styled.div<StyledModalBorderProps>`
`
const ContributionModal = () => {
const actions = useWalletStore((s) => s.actions)
const connected = useWalletStore((s) => s.connected)
const wallet = useWalletStore((s) => s.current)
const usdcBalance = getUsdcBalance()
const largestAccounts = useLargestAccounts()
const usdcBalance = largestAccounts.usdc?.balance || 0
const redeemableBalance = largestAccounts.redeemable?.balance || 0
console.log({ usdcBalance, redeemableBalance })
const [contributionAmount, setContributionAmount] = useState(0)
const [sliderPercentage, setSliderPercentage] = useState(0)
@ -52,6 +58,11 @@ const ContributionModal = () => {
const [loading, setLoading] = useState(true)
const [maxButtonTransition, setMaxButtonTransition] = useState(false)
useEffect(() => {
console.log('setContributionAmount from redeemableBalance')
setContributionAmount(redeemableBalance)
}, [redeemableBalance])
const handleConnectDisconnect = () => {
if (connected) {
setSubmitted(false)
@ -103,12 +114,9 @@ const ContributionModal = () => {
useEffect(() => {
if (submitting) {
// TODO: add submission here
const submitTimer = setTimeout(() => {
setSubmitted(true)
setSubmitting(false)
}, 2000)
return () => clearTimeout(submitTimer)
actions.submitContribution(contributionAmount)
setSubmitted(true)
setSubmitting(false)
}
}, [submitting])

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

@ -103,10 +103,9 @@ export default function useWallet() {
wallet.publicKey.toString().substr(-5),
})
await actions.fetchPool()
await actions.fetchVault()
await Promise.all([
actions.fetchWalletTokenAccounts(),
actions.fetchVaultMint(),
actions.fetchMints(),
])
})
wallet.on('disconnect', () => {
@ -131,7 +130,7 @@ export default function useWallet() {
}, [wallet, setWalletStore])
useInterval(async () => {
await actions.fetchVault()
await actions.fetchUsdcVault()
}, 20 * SECONDS)
return { connected, wallet }

View File

@ -397,6 +397,6 @@
}
],
"metadata": {
"address": "5JTPKQJPkvrMzfDyEHX6EiaEYbR5QxXDkNQ66BoadgUn"
"address": "2oBtRS2AAQfsMxXQfg41fKFY9zjvHwSSD7G5idrCFziV"
}
}

View File

@ -1,6 +1,6 @@
import create, { State } from 'zustand'
import produce from 'immer'
import { Connection, PublicKey } 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'
@ -16,6 +16,11 @@ import {
MintAccount,
getTokenAccount,
} from '../utils/tokens'
import { findLargestBalanceAccountForMint } from '../hooks/useLargestAccounts'
import { TOKEN_PROGRAM_ID } from '@solana/spl-token'
import { token } from '@project-serum/anchor/dist/utils'
import { createAssociatedTokenAccount } from '../utils/associated'
import { sendTransaction } from '../utils/send'
export const ENDPOINTS: EndpointInfo[] = [
{
@ -27,10 +32,10 @@ export const ENDPOINTS: EndpointInfo[] = [
},
{
name: 'devnet',
url: 'https://devnet.solana.com',
websocket: 'https://devnet.solana.com',
programId: 'E5s3D6B3PJinuB9kb3dicxfi3qUNLUGX6hoPawhbqagt',
poolKey: '',
url: 'https://api.devnet.solana.com',
websocket: 'https://api.devnet.solana.com',
programId: '2oBtRS2AAQfsMxXQfg41fKFY9zjvHwSSD7G5idrCFziV',
poolKey: 'ZfSZf2xrNLBrfY37TwJLoHv9qdKBQpkbZuPrq5FT8U9',
},
{
name: 'localnet',
@ -41,7 +46,7 @@ export const ENDPOINTS: EndpointInfo[] = [
},
]
const CLUSTER = 'localnet'
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')
@ -72,10 +77,13 @@ interface WalletStore extends State {
}
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 }
pool: PoolAccount | undefined
vault: TokenAccount | undefined
set: (x: any) => void
actions: any
}
@ -91,10 +99,13 @@ const useWalletStore = create<WalletStore>((set, get) => ({
},
current: null,
providerUrl: null,
provider: undefined,
program: undefined,
pool: undefined,
mangoVault: undefined,
usdcVault: undefined,
tokenAccounts: [],
mints: {},
pool: undefined,
vault: undefined,
actions: {
async fetchPool() {
const connection = get().connection.current
@ -110,15 +121,23 @@ const useWalletStore = create<WalletStore>((set, get) => ({
anchor.Provider.defaultOptions()
)
const program = new anchor.Program(poolIdl, programId, provider)
console.log(program)
const pool = (await program.account.poolAccount.fetch(
POOL_PK
)) as PoolAccount
console.log(pool)
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
})
}
},
@ -135,6 +154,8 @@ const useWalletStore = create<WalletStore>((set, get) => ({
walletOwner
)
console.log('fetchWalletTokenAccounts', ownedTokenAccounts)
set((state) => {
state.tokenAccounts = ownedTokenAccounts
})
@ -144,7 +165,7 @@ const useWalletStore = create<WalletStore>((set, get) => ({
})
}
},
async fetchVault() {
async fetchUsdcVault() {
const connection = get().connection.current
const pool = get().pool
const set = get().set
@ -155,24 +176,124 @@ const useWalletStore = create<WalletStore>((set, get) => ({
connection,
pool.poolUsdc
)
console.log('fetchVault', vault)
console.log('fetchUsdcVault', vault)
set((state) => {
state.vault = vault
state.usdcVault = vault
})
},
async fetchVaultMint() {
async fetchMints() {
const connection = get().connection.current
const vault = get().vault
const pool = get().pool
const mangoVault = get().mangoVault
const usdcVault = get().usdcVault
const set = get().set
const { account: mint } = await getMint(connection, vault.mint)
console.log('fetchVaultMint', mint)
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) => {
state.mints[vault.mint.toBase58()] = mint
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 {
console.log('difference = 0 no submission needed', difference)
return
}
await actions.fetchWalletTokenAccounts()
},
},
set: (fn) => set(produce(fn)),
}))

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

View File

@ -1,30 +0,0 @@
import BN from 'bn.js'
import useWalletStore from '../stores/useWalletStore'
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()
}
export function getUsdcBalance() {
const { tokenAccounts, mints, vault } = useWalletStore((state) => state)
if (!vault) return 0
const calculateBalance = (a) => {
const mint = mints[a.account?.mint?.toBase58()]
return mint ? fixedPointToNumber(a.account.amount, mint.decimals) : 0
}
const usdcAddress = vault.mint.toBase58()
const usdcAccount = tokenAccounts.filter(
(a) => a.account.mint.toBase58() === usdcAddress
)
const usdcBalance = usdcAccount.map((a) => calculateBalance(a))
return usdcBalance.length ? usdcBalance[0] : 0
}

292
utils/send.tsx Normal file
View File

@ -0,0 +1,292 @@
import { notify } from './notifications'
import {
Account,
AccountInfo,
Commitment,
Connection,
PublicKey,
RpcResponseAndContext,
SimulatedTransactionResponse,
Transaction,
TransactionSignature,
} from '@solana/web3.js'
import Wallet from '@project-serum/sol-wallet-adapter'
import { Buffer } from 'buffer'
import assert from 'assert'
import { struct } from 'superstruct'
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
}