From 04a7d9da53a82da3d0206565c0fe62af89a4e059 Mon Sep 17 00:00:00 2001 From: tmskrtsz Date: Sun, 30 Oct 2022 14:58:57 +0200 Subject: [PATCH 1/6] Add stake functionality (WIP) --- package.json | 11 +- src/handlers/useHandleStake.ts | 659 +--------------------------- src/hooks/useAllowedTokenDatas.tsx | 10 +- src/screens/Stake.tsx | 36 +- src/typings/tokenMetadata.ts | 6 +- src/utils/constants.ts | 15 + src/utils/tokenOps.ts | 4 +- src/utils/transactions.ts | 672 ++++++++++++++++++++++++++++- yarn.lock | 13 - 9 files changed, 722 insertions(+), 704 deletions(-) diff --git a/package.json b/package.json index 3a9b109..9547c18 100644 --- a/package.json +++ b/package.json @@ -16,29 +16,28 @@ "@solana/web3.js": "1.54.0" }, "devDependencies": { + "@coral-xyz/xnft-cli": "^0.2.0-latest.300", + "@tanstack/react-query-devtools": "^4.2.3", "husky": "^8.0.1", "lint-staged": "^13.0.3", "prettier": "2.7.1", - "typescript": "^4.8.4", - "@coral-xyz/xnft-cli": "^0.2.0-latest.300", - "@tanstack/react-query-devtools": "^4.2.3" + "typescript": "^4.8.4" }, "dependencies": { "@cardinal/common": "^2.0.11", + "@cardinal/payment-manager": "^1.7.9", "@cardinal/staking": "^1.7.4", "@cardinal/stats": "^1.0.3", - "@cardinal/payment-manager": "^1.7.9", "@cardinal/token-manager": "^1.7.9", "@metaplex-foundation/mpl-token-metadata": "1.2.5", "@project-serum/anchor": "0.24.2", "@solana/web3.js": "1.54.0", - "@tanstack/react-query": "^4.2.3", "assert": "^2.0.0", "bignumber.js": "^9.1.0", "process": "^0.11.10", "react": "^17.0.2", - "react-icons": "^4.6.0", "react-dom": "^18.2.0", + "react-icons": "^4.6.0", "react-xnft": "^0.2.0-latest.300", "swr": "^1.3.0" }, diff --git a/src/handlers/useHandleStake.ts b/src/handlers/useHandleStake.ts index ca44878..9ddc4d2 100644 --- a/src/handlers/useHandleStake.ts +++ b/src/handlers/useHandleStake.ts @@ -1,662 +1,17 @@ // Credit for https://github.com/cardinal-labs/cardinal-staking-xnft/blob/main/src/handlers/useHandleStake.ts -import { AccountData } from "@cardinal/common"; -import { - PublicKey, - Keypair, - Signer, - Transaction, - TransactionInstruction, - SystemProgram, -} from "@solana/web3.js"; -import { executeAllTransactions } from "../utils/transactions"; -import * as metaplex from "@metaplex-foundation/mpl-token-metadata"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { updateStakeStatus } from "../utils/transactions"; import { useConnection, usePublicKey } from "react-xnft"; -import * as web3 from "@solana/web3.js"; -import * as splToken from "@solana/spl-token"; import { iWallet } from "../utils/wallet"; -import type { Connection } from "@solana/web3.js"; -import { SignerWallet } from "@saberhq/solana-contrib"; -import type { Wallet } from "@saberhq/solana-contrib"; -import { - AnchorProvider, - BorshAccountsCoder, - Program, - BN, - utils, -} from "@project-serum/anchor"; - -import { claimReceiptMint, mStake } from "../utils/instruction"; - -export const STAKE_ENTRY_SEED = "stake-entry"; -export const STAKE_POOL_ADDRESS = new PublicKey( - "stkBL96RZkjY5ine4TvPihGqW8UHJfch2cokjAPzV8i" -); -export const TOKEN_MANAGER_SEED = "token-manager"; -export const TOKEN_MANAGER_ADDRESS = new PublicKey( - "mgr99QFMYByTqGPWmNqunV7vBLmWWXdSrHUfV8Jf3JM" -); -import type { AnchorTypes } from "@saberhq/anchor-contrib"; - -import * as STAKE_POOL_TYPES from "../idl/cardinal_stake_pool"; - -export const STAKE_POOL_IDL = STAKE_POOL_TYPES.IDL; -export const STAKE_AUTHORIZATION_SEED = "stake-authorization"; - -export type STAKE_POOL_PROGRAM = STAKE_POOL_TYPES.CardinalStakePool; - -export type StakePoolTypes = AnchorTypes; -type Accounts = StakePoolTypes["Accounts"]; -export type StakeEntryData = Accounts["stakeEntry"]; -export type StakePoolData = Accounts["stakePool"]; - -export type ParsedTokenAccountData = { - isNative: boolean; - delegate: string; - mint: string; - owner: string; - state: "initialized" | "frozen"; - tokenAmount: { - amount: string; - decimals: number; - uiAmount: number; - uiAmountString: string; - }; -}; - -export type BaseTokenData = { - tokenAccount?: AccountData; - metaplexData?: AccountData; -}; - -export type AllowedTokenData = BaseTokenData & { - metadata?: AccountData | null; - stakeEntry?: AccountData | null; -}; - -type AccountFn = () => Promise>; - -enum ReceiptType { - // Receive the original mint wrapped in a token manager - Original = 1, - // Receive a receipt mint wrapped in a token manager - Receipt = 2, - // Receive nothing - None = 3, -} - -/** - * Get total supply of mint - * @param connection - * @param originalMintId - * @returns - */ -const getMintSupply = async ( - connection: web3.Connection, - originalMintId: web3.PublicKey -): Promise => { - const mint = new splToken.Token( - connection, - originalMintId, - splToken.TOKEN_PROGRAM_ID, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - null - ); - return (await mint.getMintInfo()).supply; -}; - -/** - * Tries to get account based on function fn - * Return null if account doesn't exist - * @param fn - * @returns - */ -async function tryGetAccount(fn: AccountFn) { - try { - return await fn(); - } catch { - return null; - } -} - -const getProgram = (connection: Connection) => { - const provider = new AnchorProvider( - connection, - new SignerWallet(Keypair.generate()), - {} - ); - return new Program( - STAKE_POOL_IDL, - STAKE_POOL_ADDRESS, - provider - ); -}; - -const getStakeEntry = async ( - connection: Connection, - stakeEntryId: PublicKey -): Promise> => { - const stakePoolProgram = getProgram(connection); - - const parsed = await stakePoolProgram.account.stakeEntry.fetch(stakeEntryId); - return { - parsed, - pubkey: stakeEntryId, - }; -}; - -/** - * Convenience call to create a stake entry - * @param connection - Connection to use - * @param wallet - Wallet to use - * @param stakePoolId - Stake pool ID - * @param originalMintId - Original mint ID - * @param user - (Optional) User pubkey in case the person paying for the transaction and - * stake entry owner are different - * @returns - */ -const createStakeEntry = async ( - connection: Connection, - wallet: Wallet, - params: { - stakePoolId: PublicKey; - originalMintId: PublicKey; - } -): Promise<[Transaction, PublicKey]> => { - return withInitStakeEntry(new Transaction(), connection, wallet, { - stakePoolId: params.stakePoolId, - originalMintId: params.originalMintId, - }); -}; - -/** - * Convenience method to find the stake entry id. - * @returns - */ -const findStakeEntryId = async ( - wallet: web3.PublicKey, - stakePoolId: web3.PublicKey, - originalMintId: web3.PublicKey, - isFungible: boolean -): Promise<[web3.PublicKey, number]> => { - return web3.PublicKey.findProgramAddress( - [ - utils.bytes.utf8.encode(STAKE_ENTRY_SEED), - stakePoolId.toBuffer(), - originalMintId.toBuffer(), - isFungible ? wallet.toBuffer() : web3.PublicKey.default.toBuffer(), - ], - STAKE_POOL_ADDRESS - ); -}; - -/** - * Convenience method to find the stake entry id from a mint - * NOTE: This will lookup the mint on-chain to get the supply - * @returns - */ -const findStakeEntryIdFromMint = async ( - connection: web3.Connection, - wallet: web3.PublicKey, - stakePoolId: web3.PublicKey, - originalMintId: web3.PublicKey, - isFungible?: boolean -): Promise<[web3.PublicKey, number]> => { - if (isFungible === undefined) { - const supply = await getMintSupply(connection, originalMintId); - isFungible = supply.gt(new BN(1)); - } - return findStakeEntryId(wallet, stakePoolId, originalMintId, !isFungible); -}; +import { useTokens } from "../hooks/useTokens"; +import { SentryData } from "../typings/tokenMetadata"; -/** - * Add init stake entry instructions to a transaction - * @param transaction - * @param connection - * @param wallet - * @param params - * @returns Transaction, public key for the created stake entry - */ -const withInitStakeEntry = async ( - transaction: web3.Transaction, - connection: web3.Connection, - wallet: Wallet, - params: { - stakePoolId: web3.PublicKey; - originalMintId: web3.PublicKey; - } -): Promise<[web3.Transaction, web3.PublicKey]> => { - const [[stakeEntryId], originalMintMetadatId] = await Promise.all([ - findStakeEntryIdFromMint( - connection, - wallet.publicKey, - params.stakePoolId, - params.originalMintId - ), - metaplex.Metadata.getPDA(params.originalMintId), - ]); - - transaction.add( - await initStakeEntry(connection, wallet, { - stakePoolId: params.stakePoolId, - stakeEntryId: stakeEntryId, - originalMintId: params.originalMintId, - originalMintMetadatId: originalMintMetadatId, - }) - ); - return [transaction, stakeEntryId]; -}; - -const remainingAccountsForInitStakeEntry = async ( - stakePoolId: web3.PublicKey, - originalMintId: web3.PublicKey -): Promise => { - const [stakeAuthorizationRecordId] = await findStakeAuthorizationId( - stakePoolId, - originalMintId - ); - return [ - { - pubkey: stakeAuthorizationRecordId, - isSigner: false, - isWritable: false, - }, - ]; -}; - -/** - * Find stake authorization id. - * @returns - */ -const findStakeAuthorizationId = async ( - stakePoolId: web3.PublicKey, - mintId: web3.PublicKey -): Promise<[web3.PublicKey, number]> => { - return web3.PublicKey.findProgramAddress( - [ - utils.bytes.utf8.encode(STAKE_AUTHORIZATION_SEED), - stakePoolId.toBuffer(), - mintId.toBuffer(), - ], - STAKE_POOL_ADDRESS - ); -}; - -const initStakeEntry = async ( - connection: Connection, - wallet: Wallet, - params: { - stakePoolId: PublicKey; - stakeEntryId: PublicKey; - originalMintId: PublicKey; - originalMintMetadatId: PublicKey; - } -): Promise => { - const provider = new AnchorProvider(connection, wallet, {}); - const stakePoolProgram = new Program( - STAKE_POOL_IDL, - STAKE_POOL_ADDRESS, - provider - ); - const remainingAccounts = await remainingAccountsForInitStakeEntry( - params.stakePoolId, - params.originalMintId - ); - return stakePoolProgram.instruction.initEntry(wallet.publicKey, { - accounts: { - stakeEntry: params.stakeEntryId, - stakePool: params.stakePoolId, - originalMint: params.originalMintId, - originalMintMetadata: params.originalMintMetadatId, - payer: wallet.publicKey, - systemProgram: SystemProgram.programId, - }, - remainingAccounts, - }); -}; - -/** - * Finds the token manager address for a given mint and mint counter - * @returns - */ -const findTokenManagerAddress = async ( - mint: PublicKey -): Promise<[PublicKey, number]> => { - return await PublicKey.findProgramAddress( - [utils.bytes.utf8.encode(TOKEN_MANAGER_SEED), mint.toBuffer()], - TOKEN_MANAGER_ADDRESS - ); -}; - -/** - * Add claim receipt mint instructions to a transaction - * @param transaction - * @param connection - * @param wallet - * @param params - * @returns Transaction - */ -const withClaimReceiptMint = async ( - transaction: web3.Transaction, - connection: web3.Connection, - wallet: Wallet, - params: { - stakePoolId: web3.PublicKey; - stakeEntryId: web3.PublicKey; - originalMintId: web3.PublicKey; - receiptMintId: web3.PublicKey; - receiptType: ReceiptType; - } -): Promise => { - if ( - params.receiptType === ReceiptType.Original && - (await getMintSupply(connection, params.receiptMintId)).gt(new BN(1)) - ) { - throw new Error( - "Fungible staking and locked reecipt type not supported yet" - ); - } - - const tokenManagerReceiptMintTokenAccountId = - await withFindOrInitAssociatedTokenAccount( - transaction, - connection, - params.receiptMintId, - ( - await findTokenManagerAddress(params.receiptMintId) - )[0], - wallet.publicKey, - true - ); - - transaction.add( - await claimReceiptMint(connection, wallet, { - stakeEntryId: params.stakeEntryId, - tokenManagerReceiptMintTokenAccountId: - tokenManagerReceiptMintTokenAccountId, - originalMintId: params.originalMintId, - receiptMintId: params.receiptMintId, - receiptType: params.receiptType, - }) - ); - return transaction; -}; - -/** - * Utility function for adding a find or init associated token account instruction to a transaction - * Useful when using associated token accounts so you can be sure they are created before hand - * @param transaction - * @param connection - * @param mint - * @param owner - * @param payer - * @param allowOwnerOffCurve - * @returns The associated token account ID that was found or will be created. This also adds the relevent instruction to create it to the transaction if not found - */ -async function withFindOrInitAssociatedTokenAccount( - transaction: web3.Transaction, - connection: web3.Connection, - mint: web3.PublicKey, - owner: web3.PublicKey, - payer: web3.PublicKey, - allowOwnerOffCurve?: boolean -): Promise { - const associatedAddress = await splToken.Token.getAssociatedTokenAddress( - splToken.ASSOCIATED_TOKEN_PROGRAM_ID, - splToken.TOKEN_PROGRAM_ID, - mint, - owner, - allowOwnerOffCurve - ); - const account = await connection.getAccountInfo(associatedAddress); - if (!account) { - transaction.add( - splToken.Token.createAssociatedTokenAccountInstruction( - splToken.ASSOCIATED_TOKEN_PROGRAM_ID, - splToken.TOKEN_PROGRAM_ID, - mint, - associatedAddress, - owner, - payer - ) - ); - } - return associatedAddress; -} - -/** - * Add stake instructions to a transaction - * @param transaction - * @param connection - * @param wallet - * @param params - * @returns Transaction - */ -const withStake = async ( - transaction: web3.Transaction, - connection: web3.Connection, - wallet: Wallet, - params: { - stakePoolId: web3.PublicKey; - originalMintId: web3.PublicKey; - userOriginalMintTokenAccountId: web3.PublicKey; - amount?: BN; - } -): Promise => { - const [stakeEntryId] = await findStakeEntryIdFromMint( - connection, - wallet.publicKey, - params.stakePoolId, - params.originalMintId - ); - const stakeEntryOriginalMintTokenAccountId = - await withFindOrInitAssociatedTokenAccount( - transaction, - connection, - params.originalMintId, - stakeEntryId, - wallet.publicKey, - true - ); - - transaction.add( - mStake(connection, wallet, { - stakeEntryId: stakeEntryId, - stakePoolId: params.stakePoolId, - originalMint: params.originalMintId, - stakeEntryOriginalMintTokenAccountId: - stakeEntryOriginalMintTokenAccountId, - userOriginalMintTokenAccountId: params.userOriginalMintTokenAccountId, - amount: params.amount || new BN(1), - }) - ); - - return transaction; -}; - -/** - * Convenience method to stake tokens - * @param connection - Connection to use - * @param wallet - Wallet to use - * @param stakePoolId - Stake pool id - * @param originalMintId - Original mint id - * @param userOriginalMintTokenAccountId - User's original mint token account id - * @param receiptType - (Optional) ReceiptType to be received back. If none provided, none will be claimed - * @param user - (Optional) User pubkey in case the person paying for the transaction and - * stake entry owner are different - * @param amount - (Optional) Amount of tokens to be staked, defaults to 1 - * @returns - */ -const stake = async ( - connection: Connection, - wallet: Wallet, - params: { - stakePoolId: PublicKey; - originalMintId: PublicKey; - userOriginalMintTokenAccountId: PublicKey; - receiptType?: ReceiptType; - amount?: BN; - } -): Promise => { - const supply = await getMintSupply(connection, params.originalMintId); - if ( - (supply.gt(new BN(1)) || params.amount?.gt(new BN(1))) && - params.receiptType === ReceiptType.Original - ) { - throw new Error("Fungible with receipt type Original is not supported yet"); - } - - let transaction = new Transaction(); - const [stakeEntryId] = await web3.PublicKey.findProgramAddress( - [ - utils.bytes.utf8.encode(STAKE_ENTRY_SEED), - params.stakePoolId.toBuffer(), - params.originalMintId.toBuffer(), - wallet.publicKey!.toBuffer(), - ], - STAKE_POOL_ADDRESS - ); - - const stakeEntryData = await tryGetAccount(() => - getStakeEntry(connection, stakeEntryId) - ); - if (!stakeEntryData) { - [transaction] = await createStakeEntry(connection, wallet, { - stakePoolId: params.stakePoolId, - originalMintId: params.originalMintId, - }); - } - - await withStake(transaction, connection, wallet, { - stakePoolId: params.stakePoolId, - originalMintId: params.originalMintId, - userOriginalMintTokenAccountId: params.userOriginalMintTokenAccountId, - amount: params.amount, - }); - - if (params.receiptType && params.receiptType !== ReceiptType.None) { - const receiptMintId = - params.receiptType === ReceiptType.Receipt - ? stakeEntryData?.parsed.stakeMint - : params.originalMintId; - if (!receiptMintId) { - throw new Error( - "Stake entry has no stake mint. Initialize stake mint first." - ); - } - if ( - stakeEntryData?.parsed.stakeMintClaimed || - stakeEntryData?.parsed.originalMintClaimed - ) { - throw new Error("Receipt has already been claimed."); - } - - if ( - !stakeEntryData?.parsed || - stakeEntryData.parsed.amount.toNumber() === 0 - ) { - await withClaimReceiptMint(transaction, connection, wallet, { - stakePoolId: params.stakePoolId, - stakeEntryId: stakeEntryId, - originalMintId: params.originalMintId, - receiptMintId: receiptMintId, - receiptType: params.receiptType, - }); - } - } - - return transaction; -}; - -export const useHandleStake = () => { +export const useHandleStake = (selectedSentries: SentryData[]) => { const walletId = usePublicKey(); const wallet = iWallet(walletId); const connection = useConnection(); - const queryClient = useQueryClient(); - const stakePoolId = new PublicKey( - "3WS5GJSUAPXeLBbcPQRocxDYRtWbcX9PXb87J1TzFnmX" - ); // TODO: Move to const for us. - - return useMutation( - async ({ - tokenDatas, - receiptType = ReceiptType.Original, - }: { - tokenDatas: ({ amount?: BN } & Pick< - AllowedTokenData, - "tokenAccount" | "stakeEntry" - >)[]; - receiptType?: ReceiptType; - }): Promise => { - if (!stakePoolId) throw "Stake pool not found"; - if (tokenDatas.length <= 0) throw "No tokens selected"; - const txs: (Transaction | null)[] = await Promise.all( - tokenDatas.map(async (token) => { - try { - if (!token.tokenAccount) throw "Token account invalid"; - // if ( - // token.stakeEntry && - // token.stakeEntry.parsed.amount.toNumber() > 0 - // ) { - // throw 'Fungible tokens already staked in the pool. Staked tokens need to be unstaked and then restaked together with the new tokens.' - // } - // const amount = token?.amount - // ? new BN( - // token?.amount && token.tokenListData - // ? parseMintNaturalAmountFromDecimal( - // token?.amount, - // token.tokenListData.decimals - // ).toString() - // : 1 - // ) - // : undefined - // stake - return stake(connection, wallet, { - stakePoolId: stakePoolId, - receiptType: - (!token.amount || - (token.amount && - token.amount.eq(new BN(1)) && - receiptType === ReceiptType.Receipt)) && - receiptType !== ReceiptType.None - ? receiptType - : undefined, - originalMintId: new PublicKey(token.tokenAccount.parsed.mint), - userOriginalMintTokenAccountId: token.tokenAccount.pubkey, - amount: token.amount, - }); - } catch (e) { - console.log({ - message: `Failed to unstake token ${token?.stakeEntry?.pubkey.toString()}`, - description: `${e}`, - type: "error", - }); - return null; - } - }) - ); - try { - await executeAllTransactions( - connection, - wallet, - txs.filter((tx): tx is Transaction => tx !== null), - { - notificationConfig: { - message: `Successfully staked`, - description: "Stake progress will now dynamically update", - }, - } - ); - } catch (e) {} + const { mutate } = useTokens(); - return []; - }, - { - onSuccess: () => { - queryClient.resetQueries(); - }, - } - ); + return mutate(updateStakeStatus(selectedSentries, connection, wallet)); }; diff --git a/src/hooks/useAllowedTokenDatas.tsx b/src/hooks/useAllowedTokenDatas.tsx index 9fcad1b..3b208f7 100644 --- a/src/hooks/useAllowedTokenDatas.tsx +++ b/src/hooks/useAllowedTokenDatas.tsx @@ -2,7 +2,6 @@ import { AccountData, getBatchedMultipleAccounts } from "@cardinal/common"; import * as metaplex from "@metaplex-foundation/mpl-token-metadata"; import { TOKEN_PROGRAM_ID } from "@project-serum/anchor/dist/cjs/utils/token"; import { Keypair, PublicKey } from "@solana/web3.js"; -import { useQuery } from "@tanstack/react-query"; import { useConnection, usePublicKey } from "react-xnft"; import * as web3 from "@solana/web3.js"; import { SignerWallet } from "@saberhq/solana-contrib"; @@ -23,6 +22,7 @@ export const STAKE_POOL_ADDRESS = new PublicKey( import type { AnchorTypes } from "@saberhq/anchor-contrib"; import * as STAKE_POOL_TYPES from "../idl/cardinal_stake_pool"; +import useSWR from "swr"; export const STAKE_POOL_IDL = STAKE_POOL_TYPES.IDL; @@ -120,7 +120,7 @@ export const useAllowedTokenDatas = (showFungibleTokens: boolean) => { ); const walletId = usePublicKey(); const connection = useConnection(); - return useQuery( + return useSWR( [ "allowedTokenDatas", stakePoolId?.toString(), // stakePoolId?.toString() @@ -235,9 +235,9 @@ export const useAllowedTokenDatas = (showFungibleTokens: boolean) => { ), stakeEntryData: stakeEntries[i], })); - }, - { - enabled: !!stakePoolId && !!walletId, } + // { + // enabled: !!stakePoolId && !!walletId, + // } ); }; diff --git a/src/screens/Stake.tsx b/src/screens/Stake.tsx index 3010426..9671160 100644 --- a/src/screens/Stake.tsx +++ b/src/screens/Stake.tsx @@ -1,5 +1,15 @@ import { useState, useEffect } from "react"; -import { View, Image, Text, Path, Svg, Button, Loading } from "react-xnft"; +import { + View, + Image, + Text, + Path, + Svg, + Button, + Loading, + useConnection, + usePublicKey, +} from "react-xnft"; import { Layout } from "../common/Layout"; import { ActiveFilter, StakeFilter } from "../features/StakeFilter"; import { useTokens } from "../hooks/useTokens"; @@ -7,6 +17,8 @@ import { SentryData } from "../typings/tokenMetadata"; import { theme } from "../utils/theme"; import { checkUniqueStake, whichStakeType } from "../utils/utils"; import { useHandleStake } from "../handlers/useHandleStake"; +import { updateStakeStatus } from "../utils/transactions"; +import { iWallet } from "../utils/wallet"; type SentryRowProps = { tokenMetadata: SentryData; @@ -19,7 +31,7 @@ export function Stake() { const [selected, setSelected] = useState([]); const filters: ActiveFilter[] = ["all", "staked", "unstaked"]; - const handleStake = useHandleStake(); + // const stake = useHandleStake(selected); const { sentries, isLoading } = useTokens(); @@ -47,6 +59,14 @@ export function Stake() { setSelected([...selected, sentry]); } + const walletId = usePublicKey(); + const wallet = iWallet(walletId); + const connection = useConnection(); + + async function handleStake() { + updateStakeStatus(selected, connection, wallet); + } + const isOneOfAKind = checkUniqueStake(selected); const isIndeterminate = selected.length && !isOneOfAKind; const whichStake = whichStakeType(selected); @@ -117,8 +137,11 @@ export function Stake() { {isIndeterminate ? ( ) : ( - // - + handleStake()} + /> )} @@ -225,9 +248,11 @@ function SentryRow(props: SentryRowProps) { function ActionButton({ selected, stakeType, + onClick, }: { selected: SentryData[]; - stakeType: "stake" | "unstake"; + stakeType: "stake" | "unstake" | undefined; + onClick: () => void; }) { return ( diff --git a/src/typings/tokenMetadata.ts b/src/typings/tokenMetadata.ts index c9a3aad..278c2a1 100644 --- a/src/typings/tokenMetadata.ts +++ b/src/typings/tokenMetadata.ts @@ -44,4 +44,8 @@ export enum TokenState { Unstaked = "initialized", } -export type SentryData = TokenMetaUriData & { mint: string; staked: boolean }; +export type SentryData = TokenMetaUriData & { + mint: string; + staked: boolean; + publicKey: string; +}; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 624224d..26d7ad9 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1 +1,16 @@ +import { PublicKey } from "@solana/web3.js"; + export const MAX_SENTRIES = 8000; +export const STAKE_POOL_ID = new PublicKey( + "3WS5GJSUAPXeLBbcPQRocxDYRtWbcX9PXb87J1TzFnmX" +); + +export const STAKE_ENTRY_SEED = "stake-entry"; +export const STAKE_POOL_ADDRESS = new PublicKey( + "stkBL96RZkjY5ine4TvPihGqW8UHJfch2cokjAPzV8i" +); + +export const TOKEN_MANAGER_SEED = "token-manager"; +export const TOKEN_MANAGER_ADDRESS = new PublicKey( + "mgr99QFMYByTqGPWmNqunV7vBLmWWXdSrHUfV8Jf3JM" +); diff --git a/src/utils/tokenOps.ts b/src/utils/tokenOps.ts index 163ed62..cbb7242 100644 --- a/src/utils/tokenOps.ts +++ b/src/utils/tokenOps.ts @@ -10,12 +10,12 @@ export async function getTokens(wallet: PublicKey, connection: Connection) { const res = (await connection.customSplTokenAccounts( wallet )) as TokenAccounts; - // Retrieve only Sentries const sentries: Omit[] = res.nftMetadata - .map(([_, entry]) => ({ + .map(([publicKey, entry]) => ({ ...entry.tokenMetaUriData, mint: entry.metadata.mint, + publicKey: publicKey, })) .filter((nft) => nft.name.includes("Sentry")); diff --git a/src/utils/transactions.ts b/src/utils/transactions.ts index 1c96a68..83e779f 100644 --- a/src/utils/transactions.ts +++ b/src/utils/transactions.ts @@ -1,12 +1,571 @@ -import type { Wallet } from "@saberhq/solana-contrib"; -import type { +import { ConfirmOptions, Connection, SendTransactionError, Signer, Transaction, + TransactionInstruction, + SystemProgram, } from "@solana/web3.js"; -import { sendAndConfirmRawTransaction } from "@solana/web3.js"; +import { sendAndConfirmRawTransaction, Keypair } from "@solana/web3.js"; +import { SentryData } from "../typings/tokenMetadata"; +import { + STAKE_ENTRY_SEED, + STAKE_POOL_ID, + TOKEN_MANAGER_ADDRESS, + TOKEN_MANAGER_SEED, +} from "./constants"; +import { iWallet } from "./wallet"; +import { PublicKey } from "@solana/web3.js"; +import type { Wallet } from "@saberhq/solana-contrib"; +import * as web3 from "@solana/web3.js"; +import * as splToken from "@solana/spl-token"; +import * as metaplex from "@metaplex-foundation/mpl-token-metadata"; +import { SignerWallet } from "@saberhq/solana-contrib"; +import { + AnchorProvider, + BorshAccountsCoder, + Program, + BN, + utils, +} from "@project-serum/anchor"; +import { + claimReceiptMint, + mStake, + STAKE_POOL_ADDRESS, +} from "../utils/instruction"; +import type { AnchorTypes } from "@saberhq/anchor-contrib"; +import { AccountData } from "@cardinal/common"; + +import * as STAKE_POOL_TYPES from "../idl/cardinal_stake_pool"; + +export const STAKE_POOL_IDL = STAKE_POOL_TYPES.IDL; +export const STAKE_AUTHORIZATION_SEED = "stake-authorization"; + +export type STAKE_POOL_PROGRAM = STAKE_POOL_TYPES.CardinalStakePool; + +export type StakePoolTypes = AnchorTypes; +type Accounts = StakePoolTypes["Accounts"]; +export type StakeEntryData = Accounts["stakeEntry"]; +export type StakePoolData = Accounts["stakePool"]; + +export type ParsedTokenAccountData = { + isNative: boolean; + delegate: string; + mint: string; + owner: string; + state: "initialized" | "frozen"; + tokenAmount: { + amount: string; + decimals: number; + uiAmount: number; + uiAmountString: string; + }; +}; + +export type BaseTokenData = { + tokenAccount?: AccountData; + metaplexData?: AccountData; +}; + +export type AllowedTokenData = BaseTokenData & { + metadata?: AccountData | null; + stakeEntry?: AccountData | null; +}; + +type AccountFn = () => Promise>; + +enum ReceiptType { + // Receive the original mint wrapped in a token manager + Original = 1, + // Receive a receipt mint wrapped in a token manager + Receipt = 2, + // Receive nothing + None = 3, +} + +/** + * Get total supply of mint + * @param connection + * @param originalMintId + * @returns + */ +const getMintSupply = async ( + connection: web3.Connection, + originalMintId: web3.PublicKey +): Promise => { + const mint = new splToken.Token( + connection, + originalMintId, + splToken.TOKEN_PROGRAM_ID, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + null + ); + return (await mint.getMintInfo()).supply; +}; + +/** + * Tries to get account based on function fn + * Return null if account doesn't exist + * @param fn + * @returns + */ +async function tryGetAccount(fn: AccountFn) { + try { + return await fn(); + } catch { + return null; + } +} + +const getProgram = (connection: Connection) => { + const provider = new AnchorProvider( + connection, + new SignerWallet(Keypair.generate()), + {} + ); + return new Program( + STAKE_POOL_IDL, + STAKE_POOL_ADDRESS, + provider + ); +}; + +const getStakeEntry = async ( + connection: Connection, + stakeEntryId: PublicKey +): Promise> => { + const stakePoolProgram = getProgram(connection); + + const parsed = await stakePoolProgram.account.stakeEntry.fetch(stakeEntryId); + return { + parsed, + pubkey: stakeEntryId, + }; +}; + +/** + * Convenience call to create a stake entry + * @param connection - Connection to use + * @param wallet - Wallet to use + * @param stakePoolId - Stake pool ID + * @param originalMintId - Original mint ID + * @param user - (Optional) User pubkey in case the person paying for the transaction and + * stake entry owner are different + * @returns + */ +const createStakeEntry = async ( + connection: Connection, + wallet: Wallet, + params: { + stakePoolId: PublicKey; + originalMintId: PublicKey; + } +): Promise<[Transaction, PublicKey]> => { + return withInitStakeEntry(new Transaction(), connection, wallet, { + stakePoolId: params.stakePoolId, + originalMintId: params.originalMintId, + }); +}; + +/** + * Convenience method to find the stake entry id. + * @returns + */ +const findStakeEntryId = async ( + wallet: web3.PublicKey, + stakePoolId: web3.PublicKey, + originalMintId: web3.PublicKey, + isFungible: boolean +): Promise<[web3.PublicKey, number]> => { + return web3.PublicKey.findProgramAddress( + [ + utils.bytes.utf8.encode(STAKE_ENTRY_SEED), + stakePoolId.toBuffer(), + originalMintId.toBuffer(), + isFungible ? wallet.toBuffer() : web3.PublicKey.default.toBuffer(), + ], + STAKE_POOL_ADDRESS + ); +}; + +/** + * Convenience method to find the stake entry id from a mint + * NOTE: This will lookup the mint on-chain to get the supply + * @returns + */ +const findStakeEntryIdFromMint = async ( + connection: web3.Connection, + wallet: web3.PublicKey, + stakePoolId: web3.PublicKey, + originalMintId: web3.PublicKey, + isFungible?: boolean +): Promise<[web3.PublicKey, number]> => { + if (isFungible === undefined) { + const supply = await getMintSupply(connection, originalMintId); + isFungible = supply.gt(new BN(1)); + } + return findStakeEntryId(wallet, stakePoolId, originalMintId, !isFungible); +}; + +/** + * Add init stake entry instructions to a transaction + * @param transaction + * @param connection + * @param wallet + * @param params + * @returns Transaction, public key for the created stake entry + */ +const withInitStakeEntry = async ( + transaction: web3.Transaction, + connection: web3.Connection, + wallet: Wallet, + params: { + stakePoolId: web3.PublicKey; + originalMintId: web3.PublicKey; + } +): Promise<[web3.Transaction, web3.PublicKey]> => { + const [[stakeEntryId], originalMintMetadatId] = await Promise.all([ + findStakeEntryIdFromMint( + connection, + wallet.publicKey, + params.stakePoolId, + params.originalMintId + ), + metaplex.Metadata.getPDA(params.originalMintId), + ]); + + transaction.add( + await initStakeEntry(connection, wallet, { + stakePoolId: params.stakePoolId, + stakeEntryId: stakeEntryId, + originalMintId: params.originalMintId, + originalMintMetadatId: originalMintMetadatId, + }) + ); + return [transaction, stakeEntryId]; +}; + +const remainingAccountsForInitStakeEntry = async ( + stakePoolId: web3.PublicKey, + originalMintId: web3.PublicKey +): Promise => { + const [stakeAuthorizationRecordId] = await findStakeAuthorizationId( + stakePoolId, + originalMintId + ); + return [ + { + pubkey: stakeAuthorizationRecordId, + isSigner: false, + isWritable: false, + }, + ]; +}; + +/** + * Find stake authorization id. + * @returns + */ +const findStakeAuthorizationId = async ( + stakePoolId: web3.PublicKey, + mintId: web3.PublicKey +): Promise<[web3.PublicKey, number]> => { + return web3.PublicKey.findProgramAddress( + [ + utils.bytes.utf8.encode(STAKE_AUTHORIZATION_SEED), + stakePoolId.toBuffer(), + mintId.toBuffer(), + ], + STAKE_POOL_ADDRESS + ); +}; + +const initStakeEntry = async ( + connection: Connection, + wallet: Wallet, + params: { + stakePoolId: PublicKey; + stakeEntryId: PublicKey; + originalMintId: PublicKey; + originalMintMetadatId: PublicKey; + } +): Promise => { + const provider = new AnchorProvider(connection, wallet, {}); + const stakePoolProgram = new Program( + STAKE_POOL_IDL, + STAKE_POOL_ADDRESS, + provider + ); + const remainingAccounts = await remainingAccountsForInitStakeEntry( + params.stakePoolId, + params.originalMintId + ); + return stakePoolProgram.instruction.initEntry(wallet.publicKey, { + accounts: { + stakeEntry: params.stakeEntryId, + stakePool: params.stakePoolId, + originalMint: params.originalMintId, + originalMintMetadata: params.originalMintMetadatId, + payer: wallet.publicKey, + systemProgram: SystemProgram.programId, + }, + remainingAccounts, + }); +}; + +/** + * Finds the token manager address for a given mint and mint counter + * @returns + */ +const findTokenManagerAddress = async ( + mint: PublicKey +): Promise<[PublicKey, number]> => { + return await PublicKey.findProgramAddress( + [utils.bytes.utf8.encode(TOKEN_MANAGER_SEED), mint.toBuffer()], + TOKEN_MANAGER_ADDRESS + ); +}; + +/** + * Utility function for adding a find or init associated token account instruction to a transaction + * Useful when using associated token accounts so you can be sure they are created before hand + * @param transaction + * @param connection + * @param mint + * @param owner + * @param payer + * @param allowOwnerOffCurve + * @returns The associated token account ID that was found or will be created. This also adds the relevent instruction to create it to the transaction if not found + */ +async function withFindOrInitAssociatedTokenAccount( + transaction: web3.Transaction, + connection: web3.Connection, + mint: web3.PublicKey, + owner: web3.PublicKey, + payer: web3.PublicKey, + allowOwnerOffCurve?: boolean +): Promise { + const associatedAddress = await splToken.Token.getAssociatedTokenAddress( + splToken.ASSOCIATED_TOKEN_PROGRAM_ID, + splToken.TOKEN_PROGRAM_ID, + mint, + owner, + allowOwnerOffCurve + ); + const account = await connection.getAccountInfo(associatedAddress); + if (!account) { + transaction.add( + splToken.Token.createAssociatedTokenAccountInstruction( + splToken.ASSOCIATED_TOKEN_PROGRAM_ID, + splToken.TOKEN_PROGRAM_ID, + mint, + associatedAddress, + owner, + payer + ) + ); + } + return associatedAddress; +} + +/** + * Add stake instructions to a transaction + * @param transaction + * @param connection + * @param wallet + * @param params + * @returns Transaction + */ +const withStake = async ( + transaction: web3.Transaction, + connection: web3.Connection, + wallet: Wallet, + params: { + stakePoolId: web3.PublicKey; + originalMintId: web3.PublicKey; + userOriginalMintTokenAccountId: web3.PublicKey; + amount?: BN; + } +): Promise => { + const [stakeEntryId] = await findStakeEntryIdFromMint( + connection, + wallet.publicKey, + params.stakePoolId, + params.originalMintId + ); + const stakeEntryOriginalMintTokenAccountId = + await withFindOrInitAssociatedTokenAccount( + transaction, + connection, + params.originalMintId, + stakeEntryId, + wallet.publicKey, + true + ); + + transaction.add( + mStake(connection, wallet, { + stakeEntryId: stakeEntryId, + stakePoolId: params.stakePoolId, + originalMint: params.originalMintId, + stakeEntryOriginalMintTokenAccountId: + stakeEntryOriginalMintTokenAccountId, + userOriginalMintTokenAccountId: params.userOriginalMintTokenAccountId, + amount: params.amount || new BN(1), + }) + ); + + return transaction; +}; +/** + * Add claim receipt mint instructions to a transaction + * @param transaction + * @param connection + * @param wallet + * @param params + * @returns Transaction + */ +const withClaimReceiptMint = async ( + transaction: web3.Transaction, + connection: web3.Connection, + wallet: Wallet, + params: { + stakePoolId: web3.PublicKey; + stakeEntryId: web3.PublicKey; + originalMintId: web3.PublicKey; + receiptMintId: web3.PublicKey; + receiptType: ReceiptType; + } +): Promise => { + if ( + params.receiptType === ReceiptType.Original && + (await getMintSupply(connection, params.receiptMintId)).gt(new BN(1)) + ) { + throw new Error( + "Fungible staking and locked reecipt type not supported yet" + ); + } + + const tokenManagerReceiptMintTokenAccountId = + await withFindOrInitAssociatedTokenAccount( + transaction, + connection, + params.receiptMintId, + ( + await findTokenManagerAddress(params.receiptMintId) + )[0], + wallet.publicKey, + true + ); + + transaction.add( + await claimReceiptMint(connection, wallet, { + stakeEntryId: params.stakeEntryId, + tokenManagerReceiptMintTokenAccountId: + tokenManagerReceiptMintTokenAccountId, + originalMintId: params.originalMintId, + receiptMintId: params.receiptMintId, + receiptType: params.receiptType, + }) + ); + return transaction; +}; + +/** + * Convenience method to stake tokens + * @param connection - Connection to use + * @param wallet - Wallet to use + * @param stakePoolId - Stake pool id + * @param originalMintId - Original mint id + * @param userOriginalMintTokenAccountId - User's original mint token account id + * @param receiptType - (Optional) ReceiptType to be received back. If none provided, none will be claimed + * @param user - (Optional) User pubkey in case the person paying for the transaction and + * stake entry owner are different + * @param amount - (Optional) Amount of tokens to be staked, defaults to 1 + * @returns + */ +const stake = async ( + connection: Connection, + wallet: Wallet, + params: { + stakePoolId: PublicKey; + originalMintId: PublicKey; + userOriginalMintTokenAccountId: PublicKey; + receiptType?: ReceiptType; + amount?: BN; + } +): Promise => { + const supply = await getMintSupply(connection, params.originalMintId); + if ( + (supply.gt(new BN(1)) || params.amount?.gt(new BN(1))) && + params.receiptType === ReceiptType.Original + ) { + throw new Error("Fungible with receipt type Original is not supported yet"); + } + + let transaction = new Transaction(); + const [stakeEntryId] = await web3.PublicKey.findProgramAddress( + [ + utils.bytes.utf8.encode(STAKE_ENTRY_SEED), + params.stakePoolId.toBuffer(), + params.originalMintId.toBuffer(), + wallet.publicKey!.toBuffer(), + ], + STAKE_POOL_ADDRESS + ); + + const stakeEntryData = await tryGetAccount(() => + getStakeEntry(connection, stakeEntryId) + ); + if (!stakeEntryData) { + [transaction] = await createStakeEntry(connection, wallet, { + stakePoolId: params.stakePoolId, + originalMintId: params.originalMintId, + }); + } + + await withStake(transaction, connection, wallet, { + stakePoolId: params.stakePoolId, + originalMintId: params.originalMintId, + userOriginalMintTokenAccountId: params.userOriginalMintTokenAccountId, + amount: params.amount, + }); + + if (params.receiptType && params.receiptType !== ReceiptType.None) { + const receiptMintId = + params.receiptType === ReceiptType.Receipt + ? stakeEntryData?.parsed.stakeMint + : params.originalMintId; + if (!receiptMintId) { + throw new Error( + "Stake entry has no stake mint. Initialize stake mint first." + ); + } + if ( + stakeEntryData?.parsed.stakeMintClaimed || + stakeEntryData?.parsed.originalMintClaimed + ) { + throw new Error("Receipt has already been claimed."); + } + + if ( + !stakeEntryData?.parsed || + stakeEntryData.parsed.amount.toNumber() === 0 + ) { + await withClaimReceiptMint(transaction, connection, wallet, { + stakePoolId: params.stakePoolId, + stakeEntryId: stakeEntryId, + originalMintId: params.originalMintId, + receiptMintId: receiptMintId, + receiptType: params.receiptType, + }); + } + } + + return transaction; +}; export const executeAllTransactions = async ( connection: Connection, @@ -24,33 +583,26 @@ export const executeAllTransactions = async ( description?: string; }; callback?: (success: boolean) => void; - }, - preTx?: Transaction + } ): Promise<(string | null)[]> => { - const transactions = preTx ? [preTx, ...txs] : txs; + const transactions = txs; if (transactions.length === 0) return []; - const recentBlockhash = (await connection.getRecentBlockhash("max")) - .blockhash; + const recentBlockhash = await connection.getLatestBlockhash("finalized"); + for (const tx of transactions) { tx.feePayer = wallet.publicKey; - tx.recentBlockhash = recentBlockhash; + tx.recentBlockhash = recentBlockhash.blockhash; + tx.lastValidBlockHeight = recentBlockhash.lastValidBlockHeight; } + if (transactions.length > 1) { await wallet.signAllTransactions(transactions); } else { await wallet.signTransaction(transactions[0]!); } - let txIds: string[] = []; - if (preTx) { - const txid = await sendAndConfirmRawTransaction( - connection, - preTx.serialize(), - config.confirmOptions - ); - txIds.push(txid); - } + let txIds: string[] = []; txIds = [ ...txIds, ...( @@ -64,11 +616,19 @@ export const executeAllTransactions = async ( ) { tx.partialSign(...config.signers[index]!); } - const txid = await sendAndConfirmRawTransaction( + const dsa = await sendAndConfirmRawTransaction( connection, tx.serialize(), + // { + // signature: '', + // lastValidBlockHeight: tx.lastValidBlockHeight, + // blockhash: tx.recentBlockhash + // }, config.confirmOptions ); + console.log(dsa); + + return dsa; // config.notificationConfig && // config.notificationConfig.individualSuccesses && // notify({ @@ -78,7 +638,6 @@ export const executeAllTransactions = async ( // description: config.notificationConfig.message, // txid, // }) - return txid; } catch (e) { console.log( "Failed transaction: ", @@ -114,3 +673,76 @@ export const executeAllTransactions = async ( config.callback && config.callback(true); return txIds; }; + +export async function updateStakeStatus( + selectedSentries: SentryData[], + connection: Connection, + wallet: ReturnType +) { + // return async ({ + // tokenDatas, + // receiptType = ReceiptType.Original, + // }: { + // tokenDatas: ({ amount?: BN } & Pick< + // AllowedTokenData, + // "tokenAccount" | "stakeEntry" + // >)[]; + // receiptType?: ReceiptType; + // }): Promise => { + const stakePoolId = STAKE_POOL_ID; + if (!stakePoolId) throw "Stake pool not found"; + const txs: (Transaction | null)[] = await Promise.all( + selectedSentries.map(async (token) => { + try { + // if (!token.tokenAccount) throw "Token account invalid"; + // if ( + // token.stakeEntry && + // token.stakeEntry.parsed.amount.toNumber() > 0 + // ) { + // throw 'Fungible tokens already staked in the pool. Staked tokens need to be unstaked and then restaked together with the new tokens.' + // } + // const amount = token?.amount + // ? new BN( + // token?.amount && token.tokenListData + // ? parseMintNaturalAmountFromDecimal( + // token?.amount, + // token.tokenListData.decimals + // ).toString() + // : 1 + // ) + // : undefined + // stake + return stake(connection, wallet, { + stakePoolId: stakePoolId, + receiptType: ReceiptType.Original, + originalMintId: new PublicKey(token.mint), + userOriginalMintTokenAccountId: new PublicKey(token.publicKey), + amount: new BN(1), + }); + } catch (e) { + console.log({ + message: `Failed to unstake token ${token.publicKey}`, + description: `${e}`, + type: "error", + }); + return null; + } + }) + ); + + try { + await executeAllTransactions( + connection, + wallet, + txs.filter((tx): tx is Transaction => tx !== null), + { + notificationConfig: { + message: `Successfully staked`, + description: "Stake progress will now dynamically update", + }, + } + ); + } catch (e) {} + + return []; +} diff --git a/yarn.lock b/yarn.lock index 14f9dfe..48483f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1286,11 +1286,6 @@ dependencies: remove-accents "0.4.2" -"@tanstack/query-core@4.13.4": - version "4.13.4" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.13.4.tgz#77043e066586359eca40859803acc4a44e2a2dc8" - integrity sha512-DMIy6tgGehYoRUFyoR186+pQspOicyZNSGvBWxPc2CinHjWOQ7DPnGr9zmn/kE9xK4Zd3GXd25Nj3X20+TF6Lw== - "@tanstack/react-query-devtools@^4.2.3": version "4.13.4" resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-4.13.4.tgz#d631961fbb0803d2246cdf39dd2e35f443a88b6e" @@ -1300,14 +1295,6 @@ superjson "^1.10.0" use-sync-external-store "^1.2.0" -"@tanstack/react-query@^4.2.3": - version "4.13.4" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.13.4.tgz#6264e5513245a8cbec1195ba6ed9647d9230a520" - integrity sha512-OHkUulPorHDiWNcUrcSUNxedeZ28z9kCKRG3JY+aJ8dFH/o4fixtac4ys0lwCP/n/VL1XMPnu+/CXEhbXHyJZA== - dependencies: - "@tanstack/query-core" "4.13.4" - use-sync-external-store "^1.2.0" - "@trysound/sax@0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" From 72a16eb79de84a06f166a57275d3deb719d2d4ef Mon Sep 17 00:00:00 2001 From: Kollan House Date: Sun, 30 Oct 2022 20:10:12 +0400 Subject: [PATCH 2/6] fix: txn errors, still not working --- src/screens/Stake.tsx | 2 +- src/utils/transactions.ts | 105 +++++++++----------------------------- 2 files changed, 24 insertions(+), 83 deletions(-) diff --git a/src/screens/Stake.tsx b/src/screens/Stake.tsx index 9671160..805cbbd 100644 --- a/src/screens/Stake.tsx +++ b/src/screens/Stake.tsx @@ -16,7 +16,7 @@ import { useTokens } from "../hooks/useTokens"; import { SentryData } from "../typings/tokenMetadata"; import { theme } from "../utils/theme"; import { checkUniqueStake, whichStakeType } from "../utils/utils"; -import { useHandleStake } from "../handlers/useHandleStake"; +//import { useHandleStake } from "../handlers/useHandleStake"; import { updateStakeStatus } from "../utils/transactions"; import { iWallet } from "../utils/wallet"; diff --git a/src/utils/transactions.ts b/src/utils/transactions.ts index 83e779f..4cb6d8f 100644 --- a/src/utils/transactions.ts +++ b/src/utils/transactions.ts @@ -488,7 +488,7 @@ const withClaimReceiptMint = async ( */ const stake = async ( connection: Connection, - wallet: Wallet, + wallet: ReturnType, params: { stakePoolId: PublicKey; originalMintId: PublicKey; @@ -497,14 +497,6 @@ const stake = async ( amount?: BN; } ): Promise => { - const supply = await getMintSupply(connection, params.originalMintId); - if ( - (supply.gt(new BN(1)) || params.amount?.gt(new BN(1))) && - params.receiptType === ReceiptType.Original - ) { - throw new Error("Fungible with receipt type Original is not supported yet"); - } - let transaction = new Transaction(); const [stakeEntryId] = await web3.PublicKey.findProgramAddress( [ @@ -515,17 +507,19 @@ const stake = async ( ], STAKE_POOL_ADDRESS ); - + //console.log('stake entry:', stakeEntryId) const stakeEntryData = await tryGetAccount(() => getStakeEntry(connection, stakeEntryId) ); + + //console.log('stake entry data:', stakeEntryData) if (!stakeEntryData) { [transaction] = await createStakeEntry(connection, wallet, { stakePoolId: params.stakePoolId, originalMintId: params.originalMintId, }); } - + //console.log('transaction:', transaction) await withStake(transaction, connection, wallet, { stakePoolId: params.stakePoolId, originalMintId: params.originalMintId, @@ -534,15 +528,7 @@ const stake = async ( }); if (params.receiptType && params.receiptType !== ReceiptType.None) { - const receiptMintId = - params.receiptType === ReceiptType.Receipt - ? stakeEntryData?.parsed.stakeMint - : params.originalMintId; - if (!receiptMintId) { - throw new Error( - "Stake entry has no stake mint. Initialize stake mint first." - ); - } + const receiptMintId = params.originalMintId; if ( stakeEntryData?.parsed.stakeMintClaimed || stakeEntryData?.parsed.originalMintClaimed @@ -569,7 +555,7 @@ const stake = async ( export const executeAllTransactions = async ( connection: Connection, - wallet: Wallet, + wallet: ReturnType, txs: Transaction[], config: { throwIndividualError?: boolean; @@ -586,20 +572,22 @@ export const executeAllTransactions = async ( } ): Promise<(string | null)[]> => { const transactions = txs; + if (transactions.length === 0) return []; const recentBlockhash = await connection.getLatestBlockhash("finalized"); - for (const tx of transactions) { + for await (const tx of transactions) { tx.feePayer = wallet.publicKey; tx.recentBlockhash = recentBlockhash.blockhash; tx.lastValidBlockHeight = recentBlockhash.lastValidBlockHeight; } - + let _txs = transactions; if (transactions.length > 1) { - await wallet.signAllTransactions(transactions); + console.log("long txn"); + _txs = await wallet.signAllTransactions(transactions); } else { - await wallet.signTransaction(transactions[0]!); + _txs = await wallet.signTransaction(transactions[0]!); } let txIds: string[] = []; @@ -607,8 +595,9 @@ export const executeAllTransactions = async ( ...txIds, ...( await Promise.all( - txs.map(async (tx, index) => { + _txs.map(async (tx, index) => { try { + console.log("testing"); if ( config.signers && config.signers.length > 0 && @@ -616,43 +605,20 @@ export const executeAllTransactions = async ( ) { tx.partialSign(...config.signers[index]!); } - const dsa = await sendAndConfirmRawTransaction( + const txid = await sendAndConfirmRawTransaction( connection, tx.serialize(), - // { - // signature: '', - // lastValidBlockHeight: tx.lastValidBlockHeight, - // blockhash: tx.recentBlockhash - // }, config.confirmOptions ); - console.log(dsa); - - return dsa; - // config.notificationConfig && - // config.notificationConfig.individualSuccesses && - // notify({ - // message: `${config.notificationConfig.message} ${ - // index + (preTx ? 2 : 1) - // }/${transactions.length}`, - // description: config.notificationConfig.message, - // txid, - // }) + + return txid; } catch (e) { console.log( "Failed transaction: ", (e as SendTransactionError).logs, e ); - // config.notificationConfig && - // notify({ - // message: `${ - // config.notificationConfig.errorMessage ?? 'Failed transaction' - // } ${index + (preTx ? 2 : 1)}/${transactions.length}`, - // description: handleError(e, `Transaction failed: ${e}`), - // txid: '', - // type: 'error', - // }) + if (config.throwIndividualError) throw new Error(`${e}`); return null; } @@ -679,39 +645,15 @@ export async function updateStakeStatus( connection: Connection, wallet: ReturnType ) { - // return async ({ - // tokenDatas, - // receiptType = ReceiptType.Original, - // }: { - // tokenDatas: ({ amount?: BN } & Pick< - // AllowedTokenData, - // "tokenAccount" | "stakeEntry" - // >)[]; - // receiptType?: ReceiptType; - // }): Promise => { const stakePoolId = STAKE_POOL_ID; if (!stakePoolId) throw "Stake pool not found"; const txs: (Transaction | null)[] = await Promise.all( selectedSentries.map(async (token) => { try { - // if (!token.tokenAccount) throw "Token account invalid"; - // if ( - // token.stakeEntry && - // token.stakeEntry.parsed.amount.toNumber() > 0 - // ) { - // throw 'Fungible tokens already staked in the pool. Staked tokens need to be unstaked and then restaked together with the new tokens.' - // } - // const amount = token?.amount - // ? new BN( - // token?.amount && token.tokenListData - // ? parseMintNaturalAmountFromDecimal( - // token?.amount, - // token.tokenListData.decimals - // ).toString() - // : 1 - // ) - // : undefined // stake + // console.log(stakePoolId) + // console.log(token.mint) + // console.log(token.publicKey) return stake(connection, wallet, { stakePoolId: stakePoolId, receiptType: ReceiptType.Original, @@ -721,7 +663,7 @@ export async function updateStakeStatus( }); } catch (e) { console.log({ - message: `Failed to unstake token ${token.publicKey}`, + message: `Failed to stake token ${token.publicKey}`, description: `${e}`, type: "error", }); @@ -729,7 +671,6 @@ export async function updateStakeStatus( } }) ); - try { await executeAllTransactions( connection, From b23c98a5a4ebb5a71e1fcd0992a2454ae17311ab Mon Sep 17 00:00:00 2001 From: Kollan House Date: Mon, 31 Oct 2022 11:51:53 +0400 Subject: [PATCH 3/6] feat: new datamodel --- package.json | 5 +- src/handlers/useHandleStake.ts | 4 +- src/hooks/useAllowedTokenDatas.tsx | 158 +++---- src/hooks/useStakePoolId.tsx | 5 + src/index.tsx | 21 +- src/providers/EnvironmentProvider.tsx | 105 +++++ src/screens/Stake.tsx | 18 +- src/utils/tokenOps.ts | 80 +++- src/utils/transactions.ts | 653 ++------------------------ yarn.lock | 171 +++---- 10 files changed, 399 insertions(+), 821 deletions(-) create mode 100644 src/hooks/useStakePoolId.tsx create mode 100644 src/providers/EnvironmentProvider.tsx diff --git a/package.json b/package.json index 9547c18..f8dba9e 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ }, "devDependencies": { "@coral-xyz/xnft-cli": "^0.2.0-latest.300", - "@tanstack/react-query-devtools": "^4.2.3", "husky": "^8.0.1", "lint-staged": "^13.0.3", "prettier": "2.7.1", @@ -26,14 +25,16 @@ "dependencies": { "@cardinal/common": "^2.0.11", "@cardinal/payment-manager": "^1.7.9", - "@cardinal/staking": "^1.7.4", + "@cardinal/staking": "1.10.7", "@cardinal/stats": "^1.0.3", "@cardinal/token-manager": "^1.7.9", "@metaplex-foundation/mpl-token-metadata": "1.2.5", "@project-serum/anchor": "0.24.2", + "@saberhq/token-utils": "^1.14.9", "@solana/web3.js": "1.54.0", "assert": "^2.0.0", "bignumber.js": "^9.1.0", + "jsbi": "^4.3.0", "process": "^0.11.10", "react": "^17.0.2", "react-dom": "^18.2.0", diff --git a/src/handlers/useHandleStake.ts b/src/handlers/useHandleStake.ts index 9ddc4d2..74e0027 100644 --- a/src/handlers/useHandleStake.ts +++ b/src/handlers/useHandleStake.ts @@ -1,6 +1,6 @@ // Credit for https://github.com/cardinal-labs/cardinal-staking-xnft/blob/main/src/handlers/useHandleStake.ts -import { updateStakeStatus } from "../utils/transactions"; +// import { updateStakeStatus } from "../utils/transactions"; import { useConnection, usePublicKey } from "react-xnft"; import { iWallet } from "../utils/wallet"; import { useTokens } from "../hooks/useTokens"; @@ -13,5 +13,5 @@ export const useHandleStake = (selectedSentries: SentryData[]) => { const { mutate } = useTokens(); - return mutate(updateStakeStatus(selectedSentries, connection, wallet)); + // return mutate(updateStakeStatus(selectedSentries, connection, wallet)); }; diff --git a/src/hooks/useAllowedTokenDatas.tsx b/src/hooks/useAllowedTokenDatas.tsx index 3b208f7..0658a0c 100644 --- a/src/hooks/useAllowedTokenDatas.tsx +++ b/src/hooks/useAllowedTokenDatas.tsx @@ -1,42 +1,21 @@ +import useSWR from "swr"; import { AccountData, getBatchedMultipleAccounts } from "@cardinal/common"; +import type { + StakeAuthorizationData, + StakeEntryData, + StakePoolData, +} from "@cardinal/staking/dist/cjs/programs/stakePool"; +import { + getStakeAuthorizationsForPool, + getStakeEntries, + getStakePool, +} from "@cardinal/staking/dist/cjs/programs/stakePool/accounts"; +import { findStakeEntryIdFromMint } from "@cardinal/staking/dist/cjs/programs/stakePool/utils"; import * as metaplex from "@metaplex-foundation/mpl-token-metadata"; import { TOKEN_PROGRAM_ID } from "@project-serum/anchor/dist/cjs/utils/token"; -import { Keypair, PublicKey } from "@solana/web3.js"; +import { Connection, PublicKey } from "@solana/web3.js"; import { useConnection, usePublicKey } from "react-xnft"; -import * as web3 from "@solana/web3.js"; -import { SignerWallet } from "@saberhq/solana-contrib"; -import type { Connection } from "@solana/web3.js"; - -import { - AnchorProvider, - BorshAccountsCoder, - Program, - BN, - utils, -} from "@project-serum/anchor"; - -export const STAKE_ENTRY_SEED = "stake-entry"; -export const STAKE_POOL_ADDRESS = new PublicKey( - "stkBL96RZkjY5ine4TvPihGqW8UHJfch2cokjAPzV8i" -); -import type { AnchorTypes } from "@saberhq/anchor-contrib"; - -import * as STAKE_POOL_TYPES from "../idl/cardinal_stake_pool"; -import useSWR from "swr"; - -export const STAKE_POOL_IDL = STAKE_POOL_TYPES.IDL; - -export type STAKE_POOL_PROGRAM = STAKE_POOL_TYPES.CardinalStakePool; - -export type StakePoolTypes = AnchorTypes; -type Accounts = StakePoolTypes["Accounts"]; -export type StakeEntryData = Accounts["stakeEntry"]; -export type StakePoolData = Accounts["stakePool"]; - -export type BaseTokenData = { - tokenAccount?: AccountData; - metaplexData?: AccountData; -}; +import type { Wallet } from "@saberhq/solana-contrib"; export type AllowedTokenData = BaseTokenData & { metadata?: AccountData | null; @@ -57,46 +36,31 @@ export type ParsedTokenAccountData = { }; }; -const getProgram = (connection: Connection) => { - const provider = new AnchorProvider( - connection, - new SignerWallet(Keypair.generate()), - {} - ); - return new Program( - STAKE_POOL_IDL, - STAKE_POOL_ADDRESS, - provider - ); -}; - -export const getStakeEntries = async ( - connection: Connection, - stakeEntryIds: PublicKey[] -): Promise[]> => { - const stakePoolProgram = getProgram(connection); - - const stakeEntries = (await stakePoolProgram.account.stakeEntry.fetchMultiple( - stakeEntryIds - )) as StakePoolData[]; - return stakeEntries.map((tm, i) => ({ - parsed: tm, - pubkey: stakeEntryIds[i]!, - })); +export type BaseTokenData = { + tokenAccount?: AccountData; + metaplexData?: AccountData; }; export const allowedTokensForPool = ( tokenDatas: BaseTokenData[], + stakePool: AccountData, + stakeAuthorizations?: AccountData[], allowFrozen?: boolean ) => tokenDatas.filter((token) => { let isAllowed = true; - const creatorAddresses = ["Ha47XzLYkuZm32A6hXnEMLxL56jkAZvT9zRKJnioFvZK"]; // TODO: Move to const for us. + const creatorAddresses = stakePool.parsed.requiresCreators; + const collectionAddresses = stakePool.parsed.requiresCollections; + const requiresAuthorization = stakePool.parsed.requiresAuthorization; if (!allowFrozen && token.tokenAccount?.parsed.state === "frozen") { return false; } - if (creatorAddresses.length > 0) { + if ( + stakePool.parsed.requiresCreators.length > 0 || + stakePool.parsed.requiresCollections.length > 0 || + stakePool.parsed.requiresAuthorization + ) { isAllowed = false; if (creatorAddresses && creatorAddresses.length > 0) { creatorAddresses.forEach((filterCreator) => { @@ -110,28 +74,53 @@ export const allowedTokensForPool = ( } }); } + + if (collectionAddresses && collectionAddresses.length > 0 && !isAllowed) { + collectionAddresses.forEach((collectionAddress) => { + if ( + token.metaplexData?.parsed?.collection?.verified && + token.metaplexData?.parsed?.collection?.key.toString() === + collectionAddress.toString() + ) { + isAllowed = true; + } + }); + } + if ( + requiresAuthorization && + stakeAuthorizations + ?.map((s) => s.parsed.mint.toString()) + ?.includes(token?.tokenAccount?.parsed.mint ?? "") + ) { + isAllowed = true; + } } return isAllowed; }); -export const useAllowedTokenDatas = (showFungibleTokens: boolean) => { - const stakePoolId = new PublicKey( - "3WS5GJSUAPXeLBbcPQRocxDYRtWbcX9PXb87J1TzFnmX" - ); - const walletId = usePublicKey(); - const connection = useConnection(); +export const useAllowedTokenDatas = async ( + stakePoolId: PublicKey, + wallet: PublicKey, + connection: Connection, + showFungibleTokens: boolean +) => { return useSWR( [ "allowedTokenDatas", - stakePoolId?.toString(), // stakePoolId?.toString() - stakePoolId?.toString(), // stakePool?.pubkey.toString() - walletId?.toString(), + // stakePoolId?.toString(), // stakePoolId?.toString() + // stakePool?.pubkey.toString(), // stakePool?.pubkey.toString() + wallet?.toString(), showFungibleTokens, ], async () => { - if (!stakePoolId || !walletId) return; + const stakePool = await getStakePool(connection, stakePoolId); + const stakeAuthorizations = await getStakeAuthorizationsForPool( + connection, + stakePoolId + ); + if (!stakePoolId || !stakePool || !wallet) return; const allTokenAccounts = await connection.getParsedTokenAccountsByOwner( - walletId!, + wallet!, { programId: TOKEN_PROGRAM_ID, } @@ -184,22 +173,21 @@ export const useAllowedTokenDatas = (showFungibleTokens: boolean) => { metaplexData: metaplexData[tokenAccount.pubkey.toString()], })); - const allowedTokens = allowedTokensForPool(baseTokenDatas); + const allowedTokens = allowedTokensForPool( + baseTokenDatas, + stakePool, + stakeAuthorizations + ); const stakeEntryIds = await Promise.all( allowedTokens.map( async (allowedToken) => ( - await web3.PublicKey.findProgramAddress( - [ - utils.bytes.utf8.encode(STAKE_ENTRY_SEED), - stakePoolId.toBuffer(), - new PublicKey( - allowedToken.tokenAccount?.parsed.mint ?? "" - ).toBuffer(), - walletId!.toBuffer(), - ], - STAKE_POOL_ADDRESS + await findStakeEntryIdFromMint( + connection, + wallet!, + stakePoolId, + new PublicKey(allowedToken.tokenAccount?.parsed.mint ?? "") ) )[0] ) diff --git a/src/hooks/useStakePoolId.tsx b/src/hooks/useStakePoolId.tsx new file mode 100644 index 0000000..5b2cce0 --- /dev/null +++ b/src/hooks/useStakePoolId.tsx @@ -0,0 +1,5 @@ +import { PublicKey } from "@solana/web3.js"; + +export const useStakePoolId = () => { + return new PublicKey("3WS5GJSUAPXeLBbcPQRocxDYRtWbcX9PXb87J1TzFnmX"); +}; diff --git a/src/index.tsx b/src/index.tsx index 2128580..3c7abb5 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,17 +1,20 @@ import ReactXnft, { AnchorDom } from "react-xnft"; import { App } from "./App"; import { SWRConfig } from "swr"; +import { EnvironmentProvider } from "./providers/EnvironmentProvider"; ReactXnft.render( - - fetch(resource, init).then((res) => res.json()), - }} - > - - + + + fetch(resource, init).then((res) => res.json()), + }} + > + + + ); diff --git a/src/providers/EnvironmentProvider.tsx b/src/providers/EnvironmentProvider.tsx new file mode 100644 index 0000000..ca0c25b --- /dev/null +++ b/src/providers/EnvironmentProvider.tsx @@ -0,0 +1,105 @@ +import type { Cluster } from "@solana/web3.js"; +import { Connection } from "@solana/web3.js"; +import React, { useContext, useEffect, useMemo, useState } from "react"; + +export interface Environment { + label: Cluster; + primary: string; + secondary?: string; +} + +export interface EnvironmentContextValues { + environment: Environment; + setEnvironment: (newEnvironment: Environment) => void; + connection: Connection; + secondaryConnection: Connection; + customHostname: boolean; +} + +export const CLUSTER = "mainnet-beta"; + +export const ENVIRONMENTS: Environment[] = [ + { + label: "mainnet-beta", + primary: + process.env.MAINNET_PRIMARY || "https://solana-api.projectserum.com", + secondary: + process.env.MAINNET_SECONDARY || "https://solana-api.projectserum.com", + }, + { + label: "testnet", + primary: "https://api.testnet.solana.com", + }, + { + label: "devnet", + primary: "https://api.devnet.solana.com", + }, +]; + +const EnvironmentContext: React.Context = + React.createContext(null); + +export function EnvironmentProvider({ + children, + defaultCluster, +}: { + children: React.ReactChild; + defaultCluster: string; +}) { + const cluster = defaultCluster; + const foundEnvironment = ENVIRONMENTS.find((e) => e.label === cluster); + const [environment, setEnvironment] = useState( + foundEnvironment ?? ENVIRONMENTS[0]! + ); + const [customHostname, setCustomHostname] = useState(false); + + useMemo(() => { + const foundEnvironment = ENVIRONMENTS.find((e) => e.label === cluster); + setEnvironment(foundEnvironment ?? ENVIRONMENTS[0]!); + }, [cluster]); + + const connection = useMemo( + () => new Connection(environment.primary, { commitment: "recent" }), + [environment] + ); + + const secondaryConnection = useMemo( + () => + new Connection(environment.secondary ?? environment.primary, { + commitment: "recent", + }), + [environment] + ); + + useEffect(() => { + setCustomHostname( + window && + !( + window.location.hostname.includes("cardinal") || + window.location.hostname.includes("localhost") + ) + ); + }, []); + + return ( + + {children} + + ); +} + +export function useEnvironmentCtx(): EnvironmentContextValues { + const context = useContext(EnvironmentContext); + if (!context) { + throw new Error("Missing connection context"); + } + return context; +} diff --git a/src/screens/Stake.tsx b/src/screens/Stake.tsx index 805cbbd..57f1b3f 100644 --- a/src/screens/Stake.tsx +++ b/src/screens/Stake.tsx @@ -17,8 +17,11 @@ import { SentryData } from "../typings/tokenMetadata"; import { theme } from "../utils/theme"; import { checkUniqueStake, whichStakeType } from "../utils/utils"; //import { useHandleStake } from "../handlers/useHandleStake"; -import { updateStakeStatus } from "../utils/transactions"; +import { updateStakeStatus } from "../utils/tokenOps"; import { iWallet } from "../utils/wallet"; +import { useStakePoolId } from "../hooks/useStakePoolId"; +import { useEnvironmentCtx } from "../providers/EnvironmentProvider"; +import { useAllowedTokenDatas } from "../hooks/useAllowedTokenDatas"; type SentryRowProps = { tokenMetadata: SentryData; @@ -61,10 +64,19 @@ export function Stake() { const walletId = usePublicKey(); const wallet = iWallet(walletId); - const connection = useConnection(); + const { connection } = useEnvironmentCtx(); + const stakePoolId = useStakePoolId(); + const allowedTokens = useAllowedTokenDatas( + stakePoolId, + walletId, + connection, + true + ); async function handleStake() { - updateStakeStatus(selected, connection, wallet); + updateStakeStatus(selected, connection, wallet, stakePoolId); + //console.log(await tokenDatas(walletId, connection)) + console.log((await allowedTokens).data); } const isOneOfAKind = checkUniqueStake(selected); diff --git a/src/utils/tokenOps.ts b/src/utils/tokenOps.ts index cbb7242..3a54be4 100644 --- a/src/utils/tokenOps.ts +++ b/src/utils/tokenOps.ts @@ -1,9 +1,22 @@ -import { PublicKey, Connection } from "@solana/web3.js"; +import { PublicKey, Connection, Transaction } from "@solana/web3.js"; import { SentryData, TokenAccounts, TokenState, } from "../typings/tokenMetadata"; +import type { StakeEntryData } from "@cardinal/staking/dist/cjs/programs/stakePool"; +import { AccountData, getBatchedMultipleAccounts } from "@cardinal/common"; +import { iWallet } from "./wallet"; +import { usePublicKey } from "react-xnft"; +import { useEnvironmentCtx } from "../providers/EnvironmentProvider"; +import { useStakePoolId } from "../hooks/useStakePoolId"; +import { stake } from "@cardinal/staking"; +import { ReceiptType } from "@cardinal/staking/dist/cjs/programs/stakePool"; +import { executeAllTransactions } from "./transactions"; +import type { Wallet } from "@saberhq/solana-contrib"; +import { BN } from "@project-serum/anchor"; +import { TOKEN_PROGRAM_ID } from "@project-serum/anchor/dist/cjs/utils/token"; +import * as metaplex from "@metaplex-foundation/mpl-token-metadata"; export async function getTokens(wallet: PublicKey, connection: Connection) { // @ts-ignore @@ -53,3 +66,68 @@ async function getTokensWithStakeStatus( return sentries; } + +export async function updateStakeStatus( + selectedSentries: SentryData[], + connection: Connection, + wallet: Wallet, + stakePoolId: PublicKey +) { + if (!stakePoolId) throw "Stake pool not found"; + const txs: (Transaction | null)[] = await Promise.all( + selectedSentries.map(async (token) => { + try { + if (!token) throw "Token account invalid"; + // if ( + // token.stakeEntry && + // token.stakeEntry.parsed.amount.toNumber() > 0 + // ) { + // throw 'Fungible tokens already staked in the pool. Staked tokens need to be unstaked and then restaked together with the new tokens.' + // } + // const amount = token?.amount + // ? new BN( + // token?.amount && token.tokenListData + // ? parseMintNaturalAmountFromDecimal( + // token?.amount, + // token.tokenListData.decimals + // ).toString() + // : 1 + // ) + // : undefined + // stake + console.log(token.mint); + console.log(token.publicKey); + return stake(connection, wallet, { + stakePoolId: stakePoolId, + receiptType: ReceiptType.Original, + originalMintId: new PublicKey(token.mint), + userOriginalMintTokenAccountId: new PublicKey(token.publicKey), + amount: new BN(1), + }); + } catch (e) { + console.log({ + message: `Failed to stake token ${token?.publicKey.toString()}`, + description: `${e}`, + type: "error", + }); + return null; + } + }) + ); + console.log(txs); + try { + await executeAllTransactions( + connection, + wallet, + txs.filter((tx): tx is Transaction => tx !== null), + { + notificationConfig: { + message: `Successfully staked`, + description: "Stake progress will now dynamically update", + }, + } + ); + } catch (e) {} + + return []; +} diff --git a/src/utils/transactions.ts b/src/utils/transactions.ts index 4cb6d8f..7facd97 100644 --- a/src/utils/transactions.ts +++ b/src/utils/transactions.ts @@ -1,561 +1,16 @@ -import { +import type { Wallet } from "@saberhq/solana-contrib"; +import type { ConfirmOptions, Connection, SendTransactionError, Signer, Transaction, - TransactionInstruction, - SystemProgram, } from "@solana/web3.js"; -import { sendAndConfirmRawTransaction, Keypair } from "@solana/web3.js"; -import { SentryData } from "../typings/tokenMetadata"; -import { - STAKE_ENTRY_SEED, - STAKE_POOL_ID, - TOKEN_MANAGER_ADDRESS, - TOKEN_MANAGER_SEED, -} from "./constants"; -import { iWallet } from "./wallet"; -import { PublicKey } from "@solana/web3.js"; -import type { Wallet } from "@saberhq/solana-contrib"; -import * as web3 from "@solana/web3.js"; -import * as splToken from "@solana/spl-token"; -import * as metaplex from "@metaplex-foundation/mpl-token-metadata"; -import { SignerWallet } from "@saberhq/solana-contrib"; -import { - AnchorProvider, - BorshAccountsCoder, - Program, - BN, - utils, -} from "@project-serum/anchor"; -import { - claimReceiptMint, - mStake, - STAKE_POOL_ADDRESS, -} from "../utils/instruction"; -import type { AnchorTypes } from "@saberhq/anchor-contrib"; -import { AccountData } from "@cardinal/common"; - -import * as STAKE_POOL_TYPES from "../idl/cardinal_stake_pool"; - -export const STAKE_POOL_IDL = STAKE_POOL_TYPES.IDL; -export const STAKE_AUTHORIZATION_SEED = "stake-authorization"; - -export type STAKE_POOL_PROGRAM = STAKE_POOL_TYPES.CardinalStakePool; - -export type StakePoolTypes = AnchorTypes; -type Accounts = StakePoolTypes["Accounts"]; -export type StakeEntryData = Accounts["stakeEntry"]; -export type StakePoolData = Accounts["stakePool"]; - -export type ParsedTokenAccountData = { - isNative: boolean; - delegate: string; - mint: string; - owner: string; - state: "initialized" | "frozen"; - tokenAmount: { - amount: string; - decimals: number; - uiAmount: number; - uiAmountString: string; - }; -}; - -export type BaseTokenData = { - tokenAccount?: AccountData; - metaplexData?: AccountData; -}; - -export type AllowedTokenData = BaseTokenData & { - metadata?: AccountData | null; - stakeEntry?: AccountData | null; -}; - -type AccountFn = () => Promise>; - -enum ReceiptType { - // Receive the original mint wrapped in a token manager - Original = 1, - // Receive a receipt mint wrapped in a token manager - Receipt = 2, - // Receive nothing - None = 3, -} - -/** - * Get total supply of mint - * @param connection - * @param originalMintId - * @returns - */ -const getMintSupply = async ( - connection: web3.Connection, - originalMintId: web3.PublicKey -): Promise => { - const mint = new splToken.Token( - connection, - originalMintId, - splToken.TOKEN_PROGRAM_ID, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - null - ); - return (await mint.getMintInfo()).supply; -}; - -/** - * Tries to get account based on function fn - * Return null if account doesn't exist - * @param fn - * @returns - */ -async function tryGetAccount(fn: AccountFn) { - try { - return await fn(); - } catch { - return null; - } -} - -const getProgram = (connection: Connection) => { - const provider = new AnchorProvider( - connection, - new SignerWallet(Keypair.generate()), - {} - ); - return new Program( - STAKE_POOL_IDL, - STAKE_POOL_ADDRESS, - provider - ); -}; - -const getStakeEntry = async ( - connection: Connection, - stakeEntryId: PublicKey -): Promise> => { - const stakePoolProgram = getProgram(connection); - - const parsed = await stakePoolProgram.account.stakeEntry.fetch(stakeEntryId); - return { - parsed, - pubkey: stakeEntryId, - }; -}; - -/** - * Convenience call to create a stake entry - * @param connection - Connection to use - * @param wallet - Wallet to use - * @param stakePoolId - Stake pool ID - * @param originalMintId - Original mint ID - * @param user - (Optional) User pubkey in case the person paying for the transaction and - * stake entry owner are different - * @returns - */ -const createStakeEntry = async ( - connection: Connection, - wallet: Wallet, - params: { - stakePoolId: PublicKey; - originalMintId: PublicKey; - } -): Promise<[Transaction, PublicKey]> => { - return withInitStakeEntry(new Transaction(), connection, wallet, { - stakePoolId: params.stakePoolId, - originalMintId: params.originalMintId, - }); -}; - -/** - * Convenience method to find the stake entry id. - * @returns - */ -const findStakeEntryId = async ( - wallet: web3.PublicKey, - stakePoolId: web3.PublicKey, - originalMintId: web3.PublicKey, - isFungible: boolean -): Promise<[web3.PublicKey, number]> => { - return web3.PublicKey.findProgramAddress( - [ - utils.bytes.utf8.encode(STAKE_ENTRY_SEED), - stakePoolId.toBuffer(), - originalMintId.toBuffer(), - isFungible ? wallet.toBuffer() : web3.PublicKey.default.toBuffer(), - ], - STAKE_POOL_ADDRESS - ); -}; - -/** - * Convenience method to find the stake entry id from a mint - * NOTE: This will lookup the mint on-chain to get the supply - * @returns - */ -const findStakeEntryIdFromMint = async ( - connection: web3.Connection, - wallet: web3.PublicKey, - stakePoolId: web3.PublicKey, - originalMintId: web3.PublicKey, - isFungible?: boolean -): Promise<[web3.PublicKey, number]> => { - if (isFungible === undefined) { - const supply = await getMintSupply(connection, originalMintId); - isFungible = supply.gt(new BN(1)); - } - return findStakeEntryId(wallet, stakePoolId, originalMintId, !isFungible); -}; - -/** - * Add init stake entry instructions to a transaction - * @param transaction - * @param connection - * @param wallet - * @param params - * @returns Transaction, public key for the created stake entry - */ -const withInitStakeEntry = async ( - transaction: web3.Transaction, - connection: web3.Connection, - wallet: Wallet, - params: { - stakePoolId: web3.PublicKey; - originalMintId: web3.PublicKey; - } -): Promise<[web3.Transaction, web3.PublicKey]> => { - const [[stakeEntryId], originalMintMetadatId] = await Promise.all([ - findStakeEntryIdFromMint( - connection, - wallet.publicKey, - params.stakePoolId, - params.originalMintId - ), - metaplex.Metadata.getPDA(params.originalMintId), - ]); - - transaction.add( - await initStakeEntry(connection, wallet, { - stakePoolId: params.stakePoolId, - stakeEntryId: stakeEntryId, - originalMintId: params.originalMintId, - originalMintMetadatId: originalMintMetadatId, - }) - ); - return [transaction, stakeEntryId]; -}; - -const remainingAccountsForInitStakeEntry = async ( - stakePoolId: web3.PublicKey, - originalMintId: web3.PublicKey -): Promise => { - const [stakeAuthorizationRecordId] = await findStakeAuthorizationId( - stakePoolId, - originalMintId - ); - return [ - { - pubkey: stakeAuthorizationRecordId, - isSigner: false, - isWritable: false, - }, - ]; -}; - -/** - * Find stake authorization id. - * @returns - */ -const findStakeAuthorizationId = async ( - stakePoolId: web3.PublicKey, - mintId: web3.PublicKey -): Promise<[web3.PublicKey, number]> => { - return web3.PublicKey.findProgramAddress( - [ - utils.bytes.utf8.encode(STAKE_AUTHORIZATION_SEED), - stakePoolId.toBuffer(), - mintId.toBuffer(), - ], - STAKE_POOL_ADDRESS - ); -}; - -const initStakeEntry = async ( - connection: Connection, - wallet: Wallet, - params: { - stakePoolId: PublicKey; - stakeEntryId: PublicKey; - originalMintId: PublicKey; - originalMintMetadatId: PublicKey; - } -): Promise => { - const provider = new AnchorProvider(connection, wallet, {}); - const stakePoolProgram = new Program( - STAKE_POOL_IDL, - STAKE_POOL_ADDRESS, - provider - ); - const remainingAccounts = await remainingAccountsForInitStakeEntry( - params.stakePoolId, - params.originalMintId - ); - return stakePoolProgram.instruction.initEntry(wallet.publicKey, { - accounts: { - stakeEntry: params.stakeEntryId, - stakePool: params.stakePoolId, - originalMint: params.originalMintId, - originalMintMetadata: params.originalMintMetadatId, - payer: wallet.publicKey, - systemProgram: SystemProgram.programId, - }, - remainingAccounts, - }); -}; - -/** - * Finds the token manager address for a given mint and mint counter - * @returns - */ -const findTokenManagerAddress = async ( - mint: PublicKey -): Promise<[PublicKey, number]> => { - return await PublicKey.findProgramAddress( - [utils.bytes.utf8.encode(TOKEN_MANAGER_SEED), mint.toBuffer()], - TOKEN_MANAGER_ADDRESS - ); -}; - -/** - * Utility function for adding a find or init associated token account instruction to a transaction - * Useful when using associated token accounts so you can be sure they are created before hand - * @param transaction - * @param connection - * @param mint - * @param owner - * @param payer - * @param allowOwnerOffCurve - * @returns The associated token account ID that was found or will be created. This also adds the relevent instruction to create it to the transaction if not found - */ -async function withFindOrInitAssociatedTokenAccount( - transaction: web3.Transaction, - connection: web3.Connection, - mint: web3.PublicKey, - owner: web3.PublicKey, - payer: web3.PublicKey, - allowOwnerOffCurve?: boolean -): Promise { - const associatedAddress = await splToken.Token.getAssociatedTokenAddress( - splToken.ASSOCIATED_TOKEN_PROGRAM_ID, - splToken.TOKEN_PROGRAM_ID, - mint, - owner, - allowOwnerOffCurve - ); - const account = await connection.getAccountInfo(associatedAddress); - if (!account) { - transaction.add( - splToken.Token.createAssociatedTokenAccountInstruction( - splToken.ASSOCIATED_TOKEN_PROGRAM_ID, - splToken.TOKEN_PROGRAM_ID, - mint, - associatedAddress, - owner, - payer - ) - ); - } - return associatedAddress; -} - -/** - * Add stake instructions to a transaction - * @param transaction - * @param connection - * @param wallet - * @param params - * @returns Transaction - */ -const withStake = async ( - transaction: web3.Transaction, - connection: web3.Connection, - wallet: Wallet, - params: { - stakePoolId: web3.PublicKey; - originalMintId: web3.PublicKey; - userOriginalMintTokenAccountId: web3.PublicKey; - amount?: BN; - } -): Promise => { - const [stakeEntryId] = await findStakeEntryIdFromMint( - connection, - wallet.publicKey, - params.stakePoolId, - params.originalMintId - ); - const stakeEntryOriginalMintTokenAccountId = - await withFindOrInitAssociatedTokenAccount( - transaction, - connection, - params.originalMintId, - stakeEntryId, - wallet.publicKey, - true - ); - - transaction.add( - mStake(connection, wallet, { - stakeEntryId: stakeEntryId, - stakePoolId: params.stakePoolId, - originalMint: params.originalMintId, - stakeEntryOriginalMintTokenAccountId: - stakeEntryOriginalMintTokenAccountId, - userOriginalMintTokenAccountId: params.userOriginalMintTokenAccountId, - amount: params.amount || new BN(1), - }) - ); - - return transaction; -}; -/** - * Add claim receipt mint instructions to a transaction - * @param transaction - * @param connection - * @param wallet - * @param params - * @returns Transaction - */ -const withClaimReceiptMint = async ( - transaction: web3.Transaction, - connection: web3.Connection, - wallet: Wallet, - params: { - stakePoolId: web3.PublicKey; - stakeEntryId: web3.PublicKey; - originalMintId: web3.PublicKey; - receiptMintId: web3.PublicKey; - receiptType: ReceiptType; - } -): Promise => { - if ( - params.receiptType === ReceiptType.Original && - (await getMintSupply(connection, params.receiptMintId)).gt(new BN(1)) - ) { - throw new Error( - "Fungible staking and locked reecipt type not supported yet" - ); - } - - const tokenManagerReceiptMintTokenAccountId = - await withFindOrInitAssociatedTokenAccount( - transaction, - connection, - params.receiptMintId, - ( - await findTokenManagerAddress(params.receiptMintId) - )[0], - wallet.publicKey, - true - ); - - transaction.add( - await claimReceiptMint(connection, wallet, { - stakeEntryId: params.stakeEntryId, - tokenManagerReceiptMintTokenAccountId: - tokenManagerReceiptMintTokenAccountId, - originalMintId: params.originalMintId, - receiptMintId: params.receiptMintId, - receiptType: params.receiptType, - }) - ); - return transaction; -}; - -/** - * Convenience method to stake tokens - * @param connection - Connection to use - * @param wallet - Wallet to use - * @param stakePoolId - Stake pool id - * @param originalMintId - Original mint id - * @param userOriginalMintTokenAccountId - User's original mint token account id - * @param receiptType - (Optional) ReceiptType to be received back. If none provided, none will be claimed - * @param user - (Optional) User pubkey in case the person paying for the transaction and - * stake entry owner are different - * @param amount - (Optional) Amount of tokens to be staked, defaults to 1 - * @returns - */ -const stake = async ( - connection: Connection, - wallet: ReturnType, - params: { - stakePoolId: PublicKey; - originalMintId: PublicKey; - userOriginalMintTokenAccountId: PublicKey; - receiptType?: ReceiptType; - amount?: BN; - } -): Promise => { - let transaction = new Transaction(); - const [stakeEntryId] = await web3.PublicKey.findProgramAddress( - [ - utils.bytes.utf8.encode(STAKE_ENTRY_SEED), - params.stakePoolId.toBuffer(), - params.originalMintId.toBuffer(), - wallet.publicKey!.toBuffer(), - ], - STAKE_POOL_ADDRESS - ); - //console.log('stake entry:', stakeEntryId) - const stakeEntryData = await tryGetAccount(() => - getStakeEntry(connection, stakeEntryId) - ); - - //console.log('stake entry data:', stakeEntryData) - if (!stakeEntryData) { - [transaction] = await createStakeEntry(connection, wallet, { - stakePoolId: params.stakePoolId, - originalMintId: params.originalMintId, - }); - } - //console.log('transaction:', transaction) - await withStake(transaction, connection, wallet, { - stakePoolId: params.stakePoolId, - originalMintId: params.originalMintId, - userOriginalMintTokenAccountId: params.userOriginalMintTokenAccountId, - amount: params.amount, - }); - - if (params.receiptType && params.receiptType !== ReceiptType.None) { - const receiptMintId = params.originalMintId; - if ( - stakeEntryData?.parsed.stakeMintClaimed || - stakeEntryData?.parsed.originalMintClaimed - ) { - throw new Error("Receipt has already been claimed."); - } - - if ( - !stakeEntryData?.parsed || - stakeEntryData.parsed.amount.toNumber() === 0 - ) { - await withClaimReceiptMint(transaction, connection, wallet, { - stakePoolId: params.stakePoolId, - stakeEntryId: stakeEntryId, - originalMintId: params.originalMintId, - receiptMintId: receiptMintId, - receiptType: params.receiptType, - }); - } - } - - return transaction; -}; +import { sendAndConfirmRawTransaction } from "@solana/web3.js"; export const executeAllTransactions = async ( connection: Connection, - wallet: ReturnType, + wallet: Wallet, txs: Transaction[], config: { throwIndividualError?: boolean; @@ -569,35 +24,39 @@ export const executeAllTransactions = async ( description?: string; }; callback?: (success: boolean) => void; - } + }, + preTx?: Transaction ): Promise<(string | null)[]> => { - const transactions = txs; - + const transactions = preTx ? [preTx, ...txs] : txs; if (transactions.length === 0) return []; - const recentBlockhash = await connection.getLatestBlockhash("finalized"); - + const recentBlockhash = (await connection.getRecentBlockhash("max")) + .blockhash; for await (const tx of transactions) { tx.feePayer = wallet.publicKey; - tx.recentBlockhash = recentBlockhash.blockhash; - tx.lastValidBlockHeight = recentBlockhash.lastValidBlockHeight; + tx.recentBlockhash = recentBlockhash; } - let _txs = transactions; if (transactions.length > 1) { - console.log("long txn"); - _txs = await wallet.signAllTransactions(transactions); + await wallet.signAllTransactions(transactions); } else { - _txs = await wallet.signTransaction(transactions[0]!); + await wallet.signTransaction(transactions[0]!); } - let txIds: string[] = []; + if (preTx) { + const txid = await sendAndConfirmRawTransaction( + connection, + preTx.serialize(), + config.confirmOptions + ); + txIds.push(txid); + } + txIds = [ ...txIds, ...( await Promise.all( - _txs.map(async (tx, index) => { + txs.map(async (tx, index) => { try { - console.log("testing"); if ( config.signers && config.signers.length > 0 && @@ -610,7 +69,15 @@ export const executeAllTransactions = async ( tx.serialize(), config.confirmOptions ); - + // config.notificationConfig && + // config.notificationConfig.individualSuccesses && + // notify({ + // message: `${config.notificationConfig.message} ${ + // index + (preTx ? 2 : 1) + // }/${transactions.length}`, + // description: config.notificationConfig.message, + // txid, + // }) return txid; } catch (e) { console.log( @@ -618,7 +85,15 @@ export const executeAllTransactions = async ( (e as SendTransactionError).logs, e ); - + // config.notificationConfig && + // notify({ + // message: `${ + // config.notificationConfig.errorMessage ?? 'Failed transaction' + // } ${index + (preTx ? 2 : 1)}/${transactions.length}`, + // description: handleError(e, `Transaction failed: ${e}`), + // txid: '', + // type: 'error', + // }) if (config.throwIndividualError) throw new Error(`${e}`); return null; } @@ -639,51 +114,3 @@ export const executeAllTransactions = async ( config.callback && config.callback(true); return txIds; }; - -export async function updateStakeStatus( - selectedSentries: SentryData[], - connection: Connection, - wallet: ReturnType -) { - const stakePoolId = STAKE_POOL_ID; - if (!stakePoolId) throw "Stake pool not found"; - const txs: (Transaction | null)[] = await Promise.all( - selectedSentries.map(async (token) => { - try { - // stake - // console.log(stakePoolId) - // console.log(token.mint) - // console.log(token.publicKey) - return stake(connection, wallet, { - stakePoolId: stakePoolId, - receiptType: ReceiptType.Original, - originalMintId: new PublicKey(token.mint), - userOriginalMintTokenAccountId: new PublicKey(token.publicKey), - amount: new BN(1), - }); - } catch (e) { - console.log({ - message: `Failed to stake token ${token.publicKey}`, - description: `${e}`, - type: "error", - }); - return null; - } - }) - ); - try { - await executeAllTransactions( - connection, - wallet, - txs.filter((tx): tx is Transaction => tx !== null), - { - notificationConfig: { - message: `Successfully staked`, - description: "Stake progress will now dynamically update", - }, - } - ); - } catch (e) {} - - return []; -} diff --git a/yarn.lock b/yarn.lock index 48483f8..b84d162 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23,14 +23,7 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/runtime@^7.10.5", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.19.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7": - version "7.19.4" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.4.tgz#a42f814502ee467d55b38dd1c256f53a7b885c78" - integrity sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.17.8": +"@babel/runtime@^7.10.5", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.17.8", "@babel/runtime@^7.19.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7": version "7.20.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.0.tgz#824a9ef325ffde6f78056059db3168c08785e24a" integrity sha512-NDYdls71fTXoU8TZHfbBWg7DiZfNzClcKui/+kyi6ppD2L1qnWW3VV6CjtaBXSUGGhiTWJ6ereOIkUvenif66Q== @@ -129,13 +122,14 @@ ts-mocha "^8.0.0" typescript "^4.5.5" -"@cardinal/staking@^1.5.18", "@cardinal/staking@^1.7.4": - version "1.10.6" - resolved "https://registry.yarnpkg.com/@cardinal/staking/-/staking-1.10.6.tgz#d0d7deb8c20fb37f26d60f2c75c6e070e286ab3d" - integrity sha512-LMhNQNNj8NMAA16OxtPCqeOAh1fj3bxXunEF73emwf4rusr8t6EtQQnvLAWpIvkQpT8kK/FnK56NIKwTernfMg== +"@cardinal/staking@1.10.7", "@cardinal/staking@^1.5.18": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@cardinal/staking/-/staking-1.10.7.tgz#ecbdbac348d440542248419796d381030c2970c7" + integrity sha512-XzBXtQ+s90nAln0z8X/PS/+UW/5761wO085U7MiMKHj1kTUQgB79rZ5JB8HPN7lCvdi6Wx71WkHGSqUswybJ3g== dependencies: "@cardinal/common" "^2.0.11" - "@cardinal/token-manager" "^1.7.4" + "@cardinal/payment-manager" "^1.7.9" + "@cardinal/token-manager" "^1.7.9" "@metaplex-foundation/mpl-token-metadata" "^1.2.5" node-fetch "2" @@ -149,7 +143,7 @@ "@cardinal/token-manager" "^1.2.0" "@metaplex-foundation/mpl-token-metadata" "^1.2.5" -"@cardinal/token-manager@^1.2.0", "@cardinal/token-manager@^1.7.4", "@cardinal/token-manager@^1.7.9": +"@cardinal/token-manager@^1.2.0", "@cardinal/token-manager@^1.7.9": version "1.7.9" resolved "https://registry.yarnpkg.com/@cardinal/token-manager/-/token-manager-1.7.9.tgz#0413f99489051612482d6387b99b6299e9729791" integrity sha512-aFx5BVGeHdVNxXP/rMyIgw7GEY+1NbN1xwDcZzVy1neS3fRA9IZPQg0RYbJmUHRW3befZfE1miP18JdmhgjJGg== @@ -195,9 +189,9 @@ react "^17.0.0" "@coral-xyz/xnft-cli@^0.2.0-latest.300": - version "0.2.0-latest.300" - resolved "https://registry.yarnpkg.com/@coral-xyz/xnft-cli/-/xnft-cli-0.2.0-latest.300.tgz#8031961485db881e57f0c8dc5d681e09926a9a8d" - integrity sha512-Ugjza1GzM6699LZtz6aXp5DR9XTLI0Jh4wf0j1Y4tB5sH9AhxklD8UeMrZ1s/vY1ZEJsx/M5ZXSNUJy9zfZM8A== + version "0.2.0-latest.327" + resolved "https://registry.yarnpkg.com/@coral-xyz/xnft-cli/-/xnft-cli-0.2.0-latest.327.tgz#0126aef3c191cb5b19ebf3c8cad7f15fa0274c5d" + integrity sha512-vvCAkAU74iXComPIRlziXRT2IZYaBcbbjdPfAvkyEvSFedmfDUZtkb2PwjI1giOADeiyy6374IhKzB/IZcJtkA== dependencies: "@parcel/config-default" "^2.7.0" "@parcel/core" "^2.7.0" @@ -207,15 +201,15 @@ express "^4.18.1" "@emotion/cache@^11.10.3": - version "11.10.3" - resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.10.3.tgz#c4f67904fad10c945fea5165c3a5a0583c164b87" - integrity sha512-Psmp/7ovAa8appWh3g51goxu/z3iVms7JXOreq136D8Bbn6dYraPnmL6mdM8GThEx9vwSn92Fz+mGSjBzN8UPQ== + version "11.10.5" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.10.5.tgz#c142da9351f94e47527ed458f7bbbbe40bb13c12" + integrity sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA== dependencies: "@emotion/memoize" "^0.8.0" - "@emotion/sheet" "^1.2.0" + "@emotion/sheet" "^1.2.1" "@emotion/utils" "^1.2.0" "@emotion/weak-memoize" "^0.3.0" - stylis "4.0.13" + stylis "4.1.3" "@emotion/hash@^0.9.0": version "0.9.0" @@ -234,10 +228,10 @@ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.0.tgz#f580f9beb67176fa57aae70b08ed510e1b18980f" integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA== -"@emotion/sheet@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.0.tgz#771b1987855839e214fc1741bde43089397f7be5" - integrity sha512-OiTkRgpxescko+M51tZsMq7Puu/KP55wMT8BgpcXVG2hqXc0Vo0mfymJ/Uj24Hp0i083ji/o0aLddh08UEjq8w== +"@emotion/sheet@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.1.tgz#0767e0305230e894897cadb6c8df2c51e61a6c2c" + integrity sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA== "@emotion/utils@^1.2.0": version "1.2.0" @@ -408,10 +402,10 @@ resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-2.1.2.tgz#f2d8b9ddd8d191205ed26ce54aba3dfc5ae3e7c9" integrity sha512-rIZVR48zA8hGkHIK7ED6+ZiXsjRCcAVBJbm8o89OKAMTmEAQ2QvoOxoiu3w2isAaWwzgtQIOFIqHwvZDyLKCvw== -"@mui/base@5.0.0-alpha.102": - version "5.0.0-alpha.102" - resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.102.tgz#53b07d0b73d3afe1d2a3feb3b43c8188bb821796" - integrity sha512-5e/qAIP+DlkrZxIt/cwnDw/A3ii22WkoEoWKHyu4+oeGs3/1Flh7qLaN4h5EAIBB9TvTEZEUzvmsTInmIj6ghg== +"@mui/base@5.0.0-alpha.103": + version "5.0.0-alpha.103" + resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.103.tgz#90a91d1eba29ffa0bb243b25e6b1db3a9b4beeda" + integrity sha512-fJIyB2df3CHn7D26WHnutnY7vew6aytTlhmRJz6GX7ag19zU2GcOUhJAzY5qwWcrXKnlYgzimhEjaEnuiUWU4g== dependencies: "@babel/runtime" "^7.19.0" "@emotion/is-prop-valid" "^1.2.0" @@ -422,19 +416,19 @@ prop-types "^15.8.1" react-is "^18.2.0" -"@mui/core-downloads-tracker@^5.10.10": - version "5.10.10" - resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.10.10.tgz#a3e5d2f6e5146e9a85d48824c386a31be1746ba3" - integrity sha512-aDuE2PNEh+hAndxEWlZgq7uiFPZKJtnkPDX7v6kSCrMXA32ZaQ6rZi5olmC7DUHt/BaOSxb7N/im/ss0XBkDhA== +"@mui/core-downloads-tracker@^5.10.11": + version "5.10.11" + resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.10.11.tgz#e91640dfd2bd62c7f5b27da7e540f5d07e5cbc50" + integrity sha512-u5ff+UCFDHcR8MoQ8tuJR4c35vt7T/ki3aMEE2O3XQoGs8KJSrBiisFpFKyldg9/W2NSyoZxN+kxEGIfRxh+9Q== "@mui/material@^5.8.4": - version "5.10.10" - resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.10.10.tgz#df780d933c0aa9d4a5272f32c9cc24bf1ea72cff" - integrity sha512-ioLvqY7VvcePz9dnEIRhpiVvtJmAFmvG6rtLXXzVdMmAVbSaelr5Io07mPz/mCyqE+Uv8/4EuJV276DWO7etzA== + version "5.10.11" + resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.10.11.tgz#73c38b29d2df5e2f9a0825b363279bb0a2aa40df" + integrity sha512-KJ0wPCTbv6sFzwA3dgg0gowdfF+SRl7D510J9l6Nl/KFX0EawcewQudqKY4slYGFXniKa5PykqokpaWXsCCPqg== dependencies: "@babel/runtime" "^7.19.0" - "@mui/base" "5.0.0-alpha.102" - "@mui/core-downloads-tracker" "^5.10.10" + "@mui/base" "5.0.0-alpha.103" + "@mui/core-downloads-tracker" "^5.10.11" "@mui/system" "^5.10.10" "@mui/types" "^7.2.0" "@mui/utils" "^5.10.9" @@ -1279,22 +1273,6 @@ dependencies: tslib "^2.4.0" -"@tanstack/match-sorter-utils@^8.1.1": - version "8.5.14" - resolved "https://registry.yarnpkg.com/@tanstack/match-sorter-utils/-/match-sorter-utils-8.5.14.tgz#12efcd536abe491d09521e0242bc4d51442f8a8a" - integrity sha512-lVNhzTcOJ2bZ4IU+PeCPQ36vowBHvviJb2ZfdRFX5uhy7G0jM8N34zAMbmS5ZmVH8D2B7oU82OWo0e/5ZFzQrw== - dependencies: - remove-accents "0.4.2" - -"@tanstack/react-query-devtools@^4.2.3": - version "4.13.4" - resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-4.13.4.tgz#d631961fbb0803d2246cdf39dd2e35f443a88b6e" - integrity sha512-G0ZG+ZUk8ktJoi6Mzn4U7LnSOVbVFPyBJGB3dX4+SukkcKhWmErsYv2H1plRCL+V01Cg+dOg9RDfGYqsNbJszQ== - dependencies: - "@tanstack/match-sorter-utils" "^8.1.1" - superjson "^1.10.0" - use-sync-external-store "^1.2.0" - "@trysound/sax@0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" @@ -1335,9 +1313,9 @@ integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== "@types/node@*": - version "18.11.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.3.tgz#78a6d7ec962b596fc2d2ec102c4dd3ef073fea6a" - integrity sha512-fNjDQzzOsZeKZu5NATgXUPsaFaTxeRgFXoosrHivTl8RGeV733OLawXsGfEk9a8/tySyZUyiZ6E8LcjPFZ2y1A== + version "18.11.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.8.tgz#16d222a58d4363a2a359656dd20b28414de5d265" + integrity sha512-uGwPWlE0Hj972KkHtCDVwZ8O39GmyjfMane1Z3GUBGGnkZ2USDq7SxLpVIiIHpweY9DS0QTDH0Nw7RNBsAAZ5A== "@types/node@^12.12.54": version "12.20.55" @@ -1376,9 +1354,9 @@ "@types/react" "*" "@types/react@*": - version "18.0.21" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.21.tgz#b8209e9626bb00a34c76f55482697edd2b43cc67" - integrity sha512-7QUCOxvFgnD5Jk8ZKlUAhVcRj7GuJRjnjjiY/IUBWKgOlnvDvTMLD4RTF7NPyVmbRhNrbomZiOepg7M/2Kj1mA== + version "18.0.24" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.24.tgz#2f79ed5b27f08d05107aab45c17919754cc44c20" + integrity sha512-wRJWT6ouziGUy+9uX0aW4YOJxAY0bG6/AOk5AW5QSvZqI7dk6VBIbXvcVgIw/W5Jrl24f77df98GEKTJGOLx7Q== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -1450,9 +1428,9 @@ accepts@~1.3.8: negotiator "0.6.3" acorn@^8.5.0: - version "8.8.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" - integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== + version "8.8.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" + integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== aggregate-error@^3.0.0: version "3.1.0" @@ -1814,9 +1792,9 @@ camelcase@^6.0.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001400: - version "1.0.30001423" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001423.tgz#57176d460aa8cd85ee1a72016b961eb9aca55d91" - integrity sha512-09iwWGOlifvE1XuHokFMP7eR38a0JnajoyL3/i87c8ZjRWRrdKo1fqjNfugfBD0UDBIOz0U+jtNhJ0EPm1VleQ== + version "1.0.30001427" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001427.tgz#d3a749f74be7ae0671fbec3a4eea18576e8ad646" + integrity sha512-lfXQ73oB9c8DP5Suxaszm+Ta2sr/4tf8+381GkIm1MLj/YdLf+rEDyDSRCzeltuyTVGm+/s18gdZ0q+Wmp8VsQ== caw@^2.0.1: version "2.0.1" @@ -2032,13 +2010,6 @@ cookie@0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== -copy-anything@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-3.0.2.tgz#7189171ff5e1893b2287e8bf574b8cd448ed50b1" - integrity sha512-CzATjGXzUQ0EvuvgOCI6A4BGOo2bcVx8B+eC2nF862iv9fopnPQwlrbACakNCHRIJbCSBj+J/9JeDf60k64MkA== - dependencies: - is-what "^4.1.6" - core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -3268,11 +3239,6 @@ is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" -is-what@^4.1.6: - version "4.1.7" - resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.7.tgz#c41dc1d2d2d6a9285c624c2505f61849c8b1f9cc" - integrity sha512-DBVOQNiPKnGMxRMLIYSwERAS5MVY1B7xYiGnpgctsOFvVDz9f9PFXXxMcTOHuoqYp4NK9qFYQaIC1NRRxLMpBQ== - isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -3337,6 +3303,11 @@ js-yaml@4.1.0: dependencies: argparse "^2.0.1" +jsbi@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/jsbi/-/jsbi-4.3.0.tgz#b54ee074fb6fcbc00619559305c8f7e912b04741" + integrity sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g== + json-buffer@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" @@ -4309,9 +4280,9 @@ react-transition-group@^4.4.5: prop-types "^15.6.2" react-xnft@^0.2.0-latest.300: - version "0.2.0-latest.300" - resolved "https://registry.yarnpkg.com/react-xnft/-/react-xnft-0.2.0-latest.300.tgz#cd7928e2477bed3f21c7ed01c7a8f3245c8e06c6" - integrity sha512-8gpueLJ/b6IPY3ktz6qUPEmzrzpRLprlOTAgl4ssNW9gEP+NZvnyEhKMkal7F+eqj9VtCeIubguj1YBARQHVMQ== + version "0.2.0-latest.327" + resolved "https://registry.yarnpkg.com/react-xnft/-/react-xnft-0.2.0-latest.327.tgz#bc561b292d5af16af00ba624feb5d76b6018e484" + integrity sha512-6qveGJVQ8dc+t8hPo384HcG3JZJg5uNqGqegNRx/wUrfSNI8ZfyrqerxKj6sgtsK2UMQ0WGZ8Czu+ssyajqGqg== dependencies: "@coral-xyz/common-public" "*" "@coral-xyz/themes" "*" @@ -4348,7 +4319,7 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -regenerator-runtime@^0.13.10, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7: +regenerator-runtime@^0.13.10, regenerator-runtime@^0.13.7: version "0.13.10" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz#ed07b19616bcbec5da6274ebc75ae95634bfc2ee" integrity sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw== @@ -4362,11 +4333,6 @@ regexp.prototype.flags@^1.4.3: define-properties "^1.1.3" functions-have-names "^1.2.2" -remove-accents@0.4.2: - version "0.4.2" - resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5" - integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA== - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -4735,17 +4701,10 @@ strip-outer@^1.0.0: dependencies: escape-string-regexp "^1.0.2" -stylis@4.0.13: - version "4.0.13" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91" - integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag== - -superjson@^1.10.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/superjson/-/superjson-1.11.0.tgz#f6e2ae0d8fbac61c3fca09ab6739ac9678414d1b" - integrity sha512-6PfAg1FKhqkwWvPb2uXhH4MkMttdc17eJ91+Aoz4s1XUEDZFmLfFx/xVA3wgkPxAGy5dpozgGdK6V/n20Wj9yg== - dependencies: - copy-anything "^3.0.2" +stylis@4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7" + integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA== superstruct@^0.14.2: version "0.14.2" @@ -5023,7 +4982,7 @@ url-to-options@^1.0.1: resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" integrity sha512-0kQLIzG4fdk/G5NONku64rSH/x32NOA39LVQqlK8Le6lvTF6GGRJpqaQFGgU+CLwySIqBSMdwYM0sYcW9f6P4A== -use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0: +use-sync-external-store@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== @@ -5153,9 +5112,9 @@ ws@^7.4.5: integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== ws@^8.5.0: - version "8.9.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.9.0.tgz#2a994bb67144be1b53fe2d23c53c028adeb7f45e" - integrity sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg== + version "8.10.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.10.0.tgz#00a28c09dfb76eae4eb45c3b565f771d6951aa51" + integrity sha512-+s49uSmZpvtAsd2h37vIPy1RBusaLawVe8of+GyEPsaJTCMpj/2v8NpeK1SHXjBlQ95lQTmQofOJnFiLoaN3yw== xtend@^4.0.0: version "4.0.2" @@ -5234,8 +5193,8 @@ yocto-queue@^0.1.0: integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== zustand@^4.0.0-rc.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.1.2.tgz#4912b24741662d8a84ed1cb52198471cb369c4b6" - integrity sha512-gcRaKchcxFPbImrBb/BKgujOhHhik9YhVpIeP87ETT7uokEe2Szu7KkuZ9ghjtD+/KKkcrRNktR2AiLXPIbKIQ== + version "4.1.3" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.1.3.tgz#72bc4c0ed8ed906fbd92c7c20cde8dd6114c018f" + integrity sha512-AdFyr6+4sVD6xlyc/ArQaOrleqzxJEBbAXglufZ5lgvisoz8GUN3icOrKOnX1uRSxmpmdVUQPen9hhymWIzhBg== dependencies: use-sync-external-store "1.2.0" From 55b7545fca936415c6ccf4cc785b6633b0690d81 Mon Sep 17 00:00:00 2001 From: Kollan House Date: Mon, 31 Oct 2022 15:10:25 +0400 Subject: [PATCH 4/6] feat: loads our UI with tokenized data --- src/features/StakeFilter.tsx | 7 +++-- src/hooks/useAllowedTokenDatas.tsx | 14 ++++++++-- src/index.tsx | 2 +- src/screens/Stake.tsx | 44 ++++++++++++++++++------------ src/utils/tokenOps.ts | 30 +++++++++++++------- src/utils/utils.tsx | 17 ++++++++---- 6 files changed, 75 insertions(+), 39 deletions(-) diff --git a/src/features/StakeFilter.tsx b/src/features/StakeFilter.tsx index 9a2faab..2ef91b0 100644 --- a/src/features/StakeFilter.tsx +++ b/src/features/StakeFilter.tsx @@ -1,11 +1,12 @@ import { View, Text, Button } from "react-xnft"; +import { AllowedTokenData } from "../hooks/useAllowedTokenDatas"; import { SentryData } from "../typings/tokenMetadata"; import { theme } from "../utils/theme"; export type ActiveFilter = "all" | "staked" | "unstaked"; type StakeFilterProps = { - sentries: SentryData[]; + sentries: AllowedTokenData[]; filters: ActiveFilter[]; activeFilter: ActiveFilter; onClick: (filter: ActiveFilter) => void; @@ -88,14 +89,14 @@ function FilterButton({ ); } -function getStakedAndUnstakedTokens(tokens: SentryData[]): { +function getStakedAndUnstakedTokens(tokens: AllowedTokenData[]): { staked: number; unstaked: number; } { const stakeStatus = { staked: 0, unstaked: 0 }; tokens.forEach((sentry) => { - if (sentry.staked) { + if (sentry.tokenAccount?.parsed.delegate) { stakeStatus.staked = stakeStatus.staked + 1; } else { stakeStatus.unstaked = stakeStatus.unstaked + 1; diff --git a/src/hooks/useAllowedTokenDatas.tsx b/src/hooks/useAllowedTokenDatas.tsx index 0658a0c..8aeb590 100644 --- a/src/hooks/useAllowedTokenDatas.tsx +++ b/src/hooks/useAllowedTokenDatas.tsx @@ -98,13 +98,17 @@ export const allowedTokensForPool = ( return isAllowed; }); -export const useAllowedTokenDatas = async ( +export const useAllowedTokenDatas = ( stakePoolId: PublicKey, wallet: PublicKey, connection: Connection, showFungibleTokens: boolean ) => { - return useSWR( + const { + data: sentries, + error, + mutate, + } = useSWR( [ "allowedTokenDatas", // stakePoolId?.toString(), // stakePoolId?.toString() @@ -228,4 +232,10 @@ export const useAllowedTokenDatas = async ( // enabled: !!stakePoolId && !!walletId, // } ); + return { + sentries: sentries ? sentries : [], + error, + isLoading: !sentries && !error, + mutate, + }; }; diff --git a/src/index.tsx b/src/index.tsx index 3c7abb5..6e47c2f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,7 +8,7 @@ ReactXnft.render( fetch(resource, init).then((res) => res.json()), }} diff --git a/src/screens/Stake.tsx b/src/screens/Stake.tsx index 57f1b3f..fd6ffaf 100644 --- a/src/screens/Stake.tsx +++ b/src/screens/Stake.tsx @@ -21,22 +21,25 @@ import { updateStakeStatus } from "../utils/tokenOps"; import { iWallet } from "../utils/wallet"; import { useStakePoolId } from "../hooks/useStakePoolId"; import { useEnvironmentCtx } from "../providers/EnvironmentProvider"; -import { useAllowedTokenDatas } from "../hooks/useAllowedTokenDatas"; +import { + AllowedTokenData, + useAllowedTokenDatas, +} from "../hooks/useAllowedTokenDatas"; type SentryRowProps = { - tokenMetadata: SentryData; + tokenMetadata: AllowedTokenData; selected: boolean; onClick: (sentry: SentryData) => void; }; export function Stake() { const [activeFilter, setActiveFilter] = useState("all"); - const [selected, setSelected] = useState([]); + const [selected, setSelected] = useState([]); const filters: ActiveFilter[] = ["all", "staked", "unstaked"]; // const stake = useHandleStake(selected); - const { sentries, isLoading } = useTokens(); + // const { sentries, isLoading } = useTokens(); useEffect(() => { if (selected.length) { @@ -44,15 +47,19 @@ export function Stake() { } }, [activeFilter]); - function handleSelection(sentry: SentryData) { + function handleSelection(sentry: AllowedTokenData) { const isAlreadySelected = selected.some( - (selectedEntry) => selectedEntry.name === sentry.name + (selectedEntry) => + selectedEntry.metaplexData?.parsed.data.name === + sentry.metaplexData?.parsed.data.name ); if (isAlreadySelected) { setSelected((entry) => entry.filter( - (selectedToFilter) => selectedToFilter.name !== sentry.name + (selectedToFilter) => + selectedToFilter.metaplexData?.parsed.data.name !== + sentry.metaplexData?.parsed.data.name ) ); @@ -66,7 +73,7 @@ export function Stake() { const wallet = iWallet(walletId); const { connection } = useEnvironmentCtx(); const stakePoolId = useStakePoolId(); - const allowedTokens = useAllowedTokenDatas( + const { sentries, isLoading } = useAllowedTokenDatas( stakePoolId, walletId, connection, @@ -76,7 +83,6 @@ export function Stake() { async function handleStake() { updateStakeStatus(selected, connection, wallet, stakePoolId); //console.log(await tokenDatas(walletId, connection)) - console.log((await allowedTokens).data); } const isOneOfAKind = checkUniqueStake(selected); @@ -123,15 +129,17 @@ export function Stake() { > {!sentries.length ? : null} {sentries - .filter((sentry) => { + .filter((sentry: any) => { if (activeFilter === "all") return sentry; if (activeFilter === "staked") return sentry.staked; if (activeFilter === "unstaked") return !sentry.staked; }) - .map((sentry) => ( + .map((sentry: any) => ( entry.name === sentry.name)} + selected={selected.some( + (entry) => entry.metaplexData?.parsed.data.name === sentry.name + )} onClick={() => handleSelection(sentry)} /> ))} @@ -189,8 +197,8 @@ function SentryRow(props: SentryRowProps) { }} > {sentry.name} - {sentry.name} + {sentry.metaplexData?.parsed.data.name} - {sentry.staked ? "Staked" : "Not Staked"} + {sentry.tokenAccount?.parsed.delegate ? "Staked" : "Not Staked"} - {sentry.staked ? ( + {sentry.tokenAccount?.parsed.delegate ? ( void; }) { diff --git a/src/utils/tokenOps.ts b/src/utils/tokenOps.ts index 3a54be4..16d9d55 100644 --- a/src/utils/tokenOps.ts +++ b/src/utils/tokenOps.ts @@ -17,6 +17,7 @@ import type { Wallet } from "@saberhq/solana-contrib"; import { BN } from "@project-serum/anchor"; import { TOKEN_PROGRAM_ID } from "@project-serum/anchor/dist/cjs/utils/token"; import * as metaplex from "@metaplex-foundation/mpl-token-metadata"; +import { AllowedTokenData } from "../hooks/useAllowedTokenDatas"; export async function getTokens(wallet: PublicKey, connection: Connection) { // @ts-ignore @@ -68,16 +69,20 @@ async function getTokensWithStakeStatus( } export async function updateStakeStatus( - selectedSentries: SentryData[], + selectedSentries: ({ amount?: BN } & Pick< + AllowedTokenData, + "tokenAccount" | "stakeEntry" + >)[], connection: Connection, wallet: Wallet, - stakePoolId: PublicKey + stakePoolId: PublicKey, + receiptType?: ReceiptType ) { if (!stakePoolId) throw "Stake pool not found"; const txs: (Transaction | null)[] = await Promise.all( selectedSentries.map(async (token) => { try { - if (!token) throw "Token account invalid"; + if (!token.tokenAccount) throw "Token account invalid"; // if ( // token.stakeEntry && // token.stakeEntry.parsed.amount.toNumber() > 0 @@ -95,18 +100,23 @@ export async function updateStakeStatus( // ) // : undefined // stake - console.log(token.mint); - console.log(token.publicKey); return stake(connection, wallet, { stakePoolId: stakePoolId, - receiptType: ReceiptType.Original, - originalMintId: new PublicKey(token.mint), - userOriginalMintTokenAccountId: new PublicKey(token.publicKey), - amount: new BN(1), + receiptType: + (!token.amount || + (token.amount && + token.amount.eq(new BN(1)) && + receiptType === ReceiptType.Receipt)) && + receiptType !== ReceiptType.None + ? receiptType + : undefined, + originalMintId: new PublicKey(token.tokenAccount.parsed.mint), + userOriginalMintTokenAccountId: token.tokenAccount.pubkey, + amount: token.amount, }); } catch (e) { console.log({ - message: `Failed to stake token ${token?.publicKey.toString()}`, + message: `Failed to stake token ${token?.stakeEntry?.pubkey.toString()}`, description: `${e}`, type: "error", }); diff --git a/src/utils/utils.tsx b/src/utils/utils.tsx index 49465b9..281160e 100644 --- a/src/utils/utils.tsx +++ b/src/utils/utils.tsx @@ -1,5 +1,6 @@ import { Tab, Image } from "react-xnft"; import { ActiveFilter } from "../features/StakeFilter"; +import { AllowedTokenData } from "../hooks/useAllowedTokenDatas"; import { NavigatorRoute, Route } from "../typings/routes"; import { SentryData } from "../typings/tokenMetadata"; import { Icon } from "./icons"; @@ -82,17 +83,23 @@ export function formatUSD(number?: number) { return "$" + formatCompactUSD.format(number); } -export function checkUniqueStake(selected: SentryData[]) { +export function checkUniqueStake(selected: AllowedTokenData[]) { return selected.length - ? selected.every((selectedElement) => selectedElement.staked) || - selected.every((selectedElement) => selectedElement.staked === false) + ? selected.every( + (selectedElement) => selectedElement.tokenAccount?.parsed.delegate + ) || + selected.every((selectedElement) => + selectedElement.tokenAccount?.parsed.delegate ? true : false + ) : undefined; } -export function whichStakeType(selected: SentryData[]) { +export function whichStakeType(selected: AllowedTokenData[]) { if (selected.length === 0) return undefined; - return !selected.every((selectedEntry) => selectedEntry.staked) // do the opposite of this truthy value + return !selected.every( + (selectedEntry) => selectedEntry.tokenAccount?.parsed.delegate + ) // do the opposite of this truthy value ? "stake" : "unstake"; } From 15517bc1824f59e42de6548c1eb9fd6b0e6defc2 Mon Sep 17 00:00:00 2001 From: tmskrtsz Date: Mon, 31 Oct 2022 19:21:00 +0200 Subject: [PATCH 5/6] Show button regardless of stake status --- src/screens/Stake.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/screens/Stake.tsx b/src/screens/Stake.tsx index fd6ffaf..f844fbb 100644 --- a/src/screens/Stake.tsx +++ b/src/screens/Stake.tsx @@ -154,7 +154,12 @@ export function Stake() { visibility: selected.length ? "visible" : "hidden", }} > - {isIndeterminate ? ( + handleStake()} + /> + {/* {isIndeterminate ? ( ) : ( handleStake()} /> - )} + )} */} ); From 681713a0542660daf5653c2ceb6877ec11360b40 Mon Sep 17 00:00:00 2001 From: Kollan House Date: Wed, 2 Nov 2022 08:31:35 +0400 Subject: [PATCH 6/6] feat: just trying to get closer to the cardinal --- src/hooks/useAllStakePools.tsx | 55 ++++++++ src/hooks/useAllowedTokenDatas.tsx | 16 +-- src/hooks/useStakeAuthorizationsForPool.tsx | 20 +++ src/hooks/useStakePool.ts | 12 ++ src/hooks/useStakePoolData.tsx | 20 +++ src/hooks/useStakePoolId.tsx | 5 +- src/index.tsx | 5 +- src/providers/StakePoolMetadataProvider.tsx | 148 ++++++++++++++++++++ src/screens/Stake.tsx | 6 + src/utils/tokenOps.ts | 76 ++++++++-- src/utils/transactions.ts | 2 +- 11 files changed, 336 insertions(+), 29 deletions(-) create mode 100644 src/hooks/useAllStakePools.tsx create mode 100644 src/hooks/useStakeAuthorizationsForPool.tsx create mode 100644 src/hooks/useStakePool.ts create mode 100644 src/hooks/useStakePoolData.tsx create mode 100644 src/providers/StakePoolMetadataProvider.tsx diff --git a/src/hooks/useAllStakePools.tsx b/src/hooks/useAllStakePools.tsx new file mode 100644 index 0000000..df55d37 --- /dev/null +++ b/src/hooks/useAllStakePools.tsx @@ -0,0 +1,55 @@ +import { AccountData, shortPubKey } from "@cardinal/common"; +import type { StakePoolData } from "@cardinal/staking/dist/cjs/programs/stakePool"; +import { getAllStakePools } from "@cardinal/staking/dist/cjs/programs/stakePool/accounts"; +import type { StakePoolMetadata } from "../providers/StakePoolMetadataProvider"; +import { stakePoolMetadatas } from "../providers/StakePoolMetadataProvider"; +import { useEnvironmentCtx } from "../providers/EnvironmentProvider"; +import useSWR from "swr"; + +export type StakePool = { + stakePoolMetadata?: StakePoolMetadata; + stakePoolData?: AccountData; +}; + +export const stakePoolId = (stakePool: StakePool) => + stakePool.stakePoolMetadata?.stakePoolAddress ?? + stakePool.stakePoolData?.pubkey; + +export const stakePoolDisplayName = (stakePool: StakePool) => + stakePool.stakePoolMetadata?.displayName ?? + shortPubKey(stakePool.stakePoolData?.pubkey); + +export const useAllStakePools = () => { + const { connection } = useEnvironmentCtx(); + return useSWR< + | { + stakePoolsWithMetadata: StakePool[]; + stakePoolsWithoutMetadata: StakePool[]; + } + | undefined + >(["useAllStakePools"], async () => { + const allStakePoolDatas = await getAllStakePools(connection); + const [stakePoolsWithMetadata, stakePoolsWithoutMetadata] = + allStakePoolDatas.reduce( + (acc, stakePoolData) => { + const stakePoolMetadata = stakePoolMetadatas.find( + (md: any) => + md.stakePoolAddress.toString() === stakePoolData.pubkey.toString() + ); + if (stakePoolMetadata) { + return [[...acc[0], { stakePoolMetadata, stakePoolData }], acc[1]]; + } + return [acc[0], [...acc[1], { stakePoolData }]]; + }, + [[] as StakePool[], [] as StakePool[]] + ); + return { + stakePoolsWithMetadata: stakePoolsWithMetadata.sort((a, b) => + a + .stakePoolMetadata!.name.toString() + .localeCompare(b.stakePoolMetadata!.name.toString()) + ), + stakePoolsWithoutMetadata: stakePoolsWithoutMetadata, + }; + }); +}; diff --git a/src/hooks/useAllowedTokenDatas.tsx b/src/hooks/useAllowedTokenDatas.tsx index 8aeb590..435ef6b 100644 --- a/src/hooks/useAllowedTokenDatas.tsx +++ b/src/hooks/useAllowedTokenDatas.tsx @@ -14,8 +14,6 @@ import { findStakeEntryIdFromMint } from "@cardinal/staking/dist/cjs/programs/st import * as metaplex from "@metaplex-foundation/mpl-token-metadata"; import { TOKEN_PROGRAM_ID } from "@project-serum/anchor/dist/cjs/utils/token"; import { Connection, PublicKey } from "@solana/web3.js"; -import { useConnection, usePublicKey } from "react-xnft"; -import type { Wallet } from "@saberhq/solana-contrib"; export type AllowedTokenData = BaseTokenData & { metadata?: AccountData | null; @@ -100,6 +98,8 @@ export const allowedTokensForPool = ( export const useAllowedTokenDatas = ( stakePoolId: PublicKey, + stakePool: any, + stakeAuthorizations: any, wallet: PublicKey, connection: Connection, showFungibleTokens: boolean @@ -111,17 +111,12 @@ export const useAllowedTokenDatas = ( } = useSWR( [ "allowedTokenDatas", - // stakePoolId?.toString(), // stakePoolId?.toString() - // stakePool?.pubkey.toString(), // stakePool?.pubkey.toString() + stakePoolId?.toString(), // stakePoolId?.toString() + stakePool?.pubkey.toString(), // stakePool?.pubkey.toString() wallet?.toString(), showFungibleTokens, ], async () => { - const stakePool = await getStakePool(connection, stakePoolId); - const stakeAuthorizations = await getStakeAuthorizationsForPool( - connection, - stakePoolId - ); if (!stakePoolId || !stakePool || !wallet) return; const allTokenAccounts = await connection.getParsedTokenAccountsByOwner( wallet!, @@ -228,9 +223,6 @@ export const useAllowedTokenDatas = ( stakeEntryData: stakeEntries[i], })); } - // { - // enabled: !!stakePoolId && !!walletId, - // } ); return { sentries: sentries ? sentries : [], diff --git a/src/hooks/useStakeAuthorizationsForPool.tsx b/src/hooks/useStakeAuthorizationsForPool.tsx new file mode 100644 index 0000000..5dab9fa --- /dev/null +++ b/src/hooks/useStakeAuthorizationsForPool.tsx @@ -0,0 +1,20 @@ +import type { AccountData } from "@cardinal/common"; +import type { StakeAuthorizationData } from "@cardinal/staking/dist/cjs/programs/stakePool"; +import { getStakeAuthorizationsForPool } from "@cardinal/staking/dist/cjs/programs/stakePool/accounts"; +import { useEnvironmentCtx } from "../providers/EnvironmentProvider"; +import useSWR from "swr"; + +import { useStakePoolId } from "./useStakePoolId"; + +export const useStakeAuthorizationsForPool = () => { + const { secondaryConnection } = useEnvironmentCtx(); + const stakePoolId = useStakePoolId(); + return useSWR[] | undefined>( + ["useStakeAuthorizationsForPool", stakePoolId?.toString()], + async () => { + if (stakePoolId) { + return getStakeAuthorizationsForPool(secondaryConnection, stakePoolId); + } + } + ); +}; diff --git a/src/hooks/useStakePool.ts b/src/hooks/useStakePool.ts new file mode 100644 index 0000000..d096800 --- /dev/null +++ b/src/hooks/useStakePool.ts @@ -0,0 +1,12 @@ +import { useStakePoolData } from "./useStakePoolData"; +import { useStakePoolMetadata } from "../providers/StakePoolMetadataProvider"; +import { StakePool } from "./useAllStakePools"; + +export const useStakePool = (): StakePool => { + const stakePoolData = useStakePoolData(); + const { stakePoolMetadata } = useStakePoolMetadata(); + return { + stakePoolData: stakePoolData.data, + stakePoolMetadata: stakePoolMetadata ?? undefined, + }; +}; diff --git a/src/hooks/useStakePoolData.tsx b/src/hooks/useStakePoolData.tsx new file mode 100644 index 0000000..b40993a --- /dev/null +++ b/src/hooks/useStakePoolData.tsx @@ -0,0 +1,20 @@ +import type { AccountData } from "@cardinal/common"; +import type { StakePoolData } from "@cardinal/staking/dist/cjs/programs/stakePool"; +import { getStakePool } from "@cardinal/staking/dist/cjs/programs/stakePool/accounts"; +import { useEnvironmentCtx } from "../providers/EnvironmentProvider"; +import useSWR from "swr"; + +import { useStakePoolId } from "./useStakePoolId"; + +export const useStakePoolData = () => { + const stakePoolId = useStakePoolId(); + const { connection } = useEnvironmentCtx(); + + return useSWR | undefined>( + ["stakePoolData", "sentries"], + async () => { + if (!stakePoolId) return; + return getStakePool(connection, stakePoolId); + } + ); +}; diff --git a/src/hooks/useStakePoolId.tsx b/src/hooks/useStakePoolId.tsx index 5b2cce0..1c78c96 100644 --- a/src/hooks/useStakePoolId.tsx +++ b/src/hooks/useStakePoolId.tsx @@ -1,5 +1,6 @@ -import { PublicKey } from "@solana/web3.js"; +import { useStakePoolMetadata } from "../providers/StakePoolMetadataProvider"; export const useStakePoolId = () => { - return new PublicKey("3WS5GJSUAPXeLBbcPQRocxDYRtWbcX9PXb87J1TzFnmX"); + const { stakePoolMetadata } = useStakePoolMetadata(); + return stakePoolMetadata?.stakePoolAddress; }; diff --git a/src/index.tsx b/src/index.tsx index 6e47c2f..0a09996 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,6 +2,7 @@ import ReactXnft, { AnchorDom } from "react-xnft"; import { App } from "./App"; import { SWRConfig } from "swr"; import { EnvironmentProvider } from "./providers/EnvironmentProvider"; +import { StakePoolMetadataProvider } from "./providers/StakePoolMetadataProvider"; ReactXnft.render( @@ -13,7 +14,9 @@ ReactXnft.render( fetch(resource, init).then((res) => res.json()), }} > - + + + diff --git a/src/providers/StakePoolMetadataProvider.tsx b/src/providers/StakePoolMetadataProvider.tsx new file mode 100644 index 0000000..e1f0875 --- /dev/null +++ b/src/providers/StakePoolMetadataProvider.tsx @@ -0,0 +1,148 @@ +import React, { useContext, useState } from "react"; +import { ReceiptType } from "@cardinal/staking/dist/cjs/programs/stakePool"; +import { PublicKey } from "@solana/web3.js"; +import type { CSSProperties } from "react"; +export enum TokenStandard { + // Fungible, can have more than 1 + Fungible = 1, + // Non fungible are all unique + NonFungible = 2, + // No receipt + None = 3, +} + +export type Analytic = { + metadata?: { + key: string; + type: "staked"; + totals?: { + key: string; + value: number; + }[]; + }; +}; +export type StakePoolMetadata = { + // Name of this stake pool used as an id. Should be in lower-case kebab-case since it is used in the URL as /{name} + // https://www.theserverside.com/blog/Coffee-Talk-Java-News-Stories-and-Opinions/Why-you-should-make-kebab-case-a-URL-naming-convention-best-practice + name: string; + // Display name to be displayed in the header. Often the same as name but with capital letters and spaces + displayName: string; + // Whether or not to show name in header, defaults false + nameInHeader?: boolean; + // Publickey for this stake pool + stakePoolAddress: PublicKey; + // Description for this stake pool + description?: string; + // Default receipt type. Setting this will remove the option for the user to choose which receipt type to use + receiptType?: ReceiptType; + // Default empty. Setting this will tell the UI to only show tokens of that standard. Supports fungible or non-fungible + tokenStandard?: TokenStandard; + // Optional config to hide this pool from the main page + hidden?: boolean; + // Optional config to disable finding this pool + notFound?: boolean; + // Optional hostname to remap + hostname?: string; + // Optional hide footer + hideFooter?: boolean; + // Optional config to link redirect to page when you click on this pool + redirect?: string; + // Hide allowed tokens style + hideAllowedTokens?: boolean; + // styles to apply to the whole stake pool + styles?: CSSProperties; + // Contrast dark background + darkBg?: boolean; + // Contrast light background + lightBg?: boolean; + // Colors object to style the stake page + colors?: { + primary: string; + secondary: string; + accent?: string; + fontColor?: string; + fontColorSecondary?: string; + backgroundSecondary?: string; + }; + // Disallow regions based on IP address + disallowRegions?: { code: string; subdivision?: string }[]; + // If the logo should be displayed with paddding + logoPadding?: boolean; + // Optional social links + socialLinks?: []; + // Image url to be used as the icon in the pool selector and the header + imageUrl?: string; + // Secondary image url to be used next to the icon in the pool selector and the header + secondaryImageUrl?: string; + // Background image for pool + backgroundImage?: string; + // Website url if specified will be navigated to when the image in the header is clicked + websiteUrl?: string; + // Max staked is used to compute percentage of total staked + maxStaked?: number; + // Links to show at the top right of the page + links?: { text: string; value: string }[]; + // Analytics to show at the top of stake pool. supports trait based analytics and overall tokens data + analytics?: Analytic[]; +}; + +export interface StakePoolMetadataValues { + stakePoolMetadata: StakePoolMetadata | null; + setStakePoolMetadata: (stakePoolMetadata: StakePoolMetadata | null) => void; +} + +const EnvironmentContext: React.Context = + React.createContext(null); + +export const stakePoolMetadatas: StakePoolMetadata[] = [ + { + name: "sentries", + displayName: "Sentries", + stakePoolAddress: new PublicKey( + "3WS5GJSUAPXeLBbcPQRocxDYRtWbcX9PXb87J1TzFnmX" + ), + websiteUrl: "https://www.sentries.io/", + receiptType: ReceiptType.Original, + lightBg: true, + maxStaked: 8000, // update with collection size + imageUrl: + "https://github.com/cardinal-labs/cardinal-staking-xnft/blob/main/assets/logos/sentries-logo.svg?raw=true", + tokenStandard: TokenStandard.NonFungible, + colors: { + primary: "#383838", + secondary: "#fff", + accent: "#0d0d0d", + fontColor: "#fff", + fontColorSecondary: "#000", + }, + }, +]; + +export function StakePoolMetadataProvider({ + children, +}: { + children: React.ReactChild; +}) { + const [stakePoolMetadata, setStakePoolMetadata] = + useState(null); + return ( + { + setStakePoolMetadata(stakePoolMetadatas[0]); + }, + }} + > + {children} + + ); +} + +export function useStakePoolMetadata(): StakePoolMetadataValues { + const context = useContext(EnvironmentContext); + if (!context) { + throw new Error("Missing stakePoolMetadata context"); + } + return context; +} diff --git a/src/screens/Stake.tsx b/src/screens/Stake.tsx index f844fbb..ab4ba0d 100644 --- a/src/screens/Stake.tsx +++ b/src/screens/Stake.tsx @@ -25,6 +25,8 @@ import { AllowedTokenData, useAllowedTokenDatas, } from "../hooks/useAllowedTokenDatas"; +import { useStakePoolData } from "../hooks/useStakePoolData"; +import { useStakeAuthorizationsForPool } from "../hooks/useStakeAuthorizationsForPool"; type SentryRowProps = { tokenMetadata: AllowedTokenData; @@ -73,8 +75,12 @@ export function Stake() { const wallet = iWallet(walletId); const { connection } = useEnvironmentCtx(); const stakePoolId = useStakePoolId(); + const { data: stakePool } = useStakePoolData(); + const { data: stakeAuthorizations } = useStakeAuthorizationsForPool(); const { sentries, isLoading } = useAllowedTokenDatas( stakePoolId, + stakePool, + stakeAuthorizations, walletId, connection, true diff --git a/src/utils/tokenOps.ts b/src/utils/tokenOps.ts index 16d9d55..c2bb980 100644 --- a/src/utils/tokenOps.ts +++ b/src/utils/tokenOps.ts @@ -1,22 +1,15 @@ -import { PublicKey, Connection, Transaction } from "@solana/web3.js"; +import { PublicKey, Connection, Transaction, Signer } from "@solana/web3.js"; import { SentryData, TokenAccounts, TokenState, } from "../typings/tokenMetadata"; -import type { StakeEntryData } from "@cardinal/staking/dist/cjs/programs/stakePool"; -import { AccountData, getBatchedMultipleAccounts } from "@cardinal/common"; -import { iWallet } from "./wallet"; -import { usePublicKey } from "react-xnft"; -import { useEnvironmentCtx } from "../providers/EnvironmentProvider"; -import { useStakePoolId } from "../hooks/useStakePoolId"; -import { stake } from "@cardinal/staking"; +import { stake, createStakeEntryAndStakeMint } from "@cardinal/staking"; + import { ReceiptType } from "@cardinal/staking/dist/cjs/programs/stakePool"; import { executeAllTransactions } from "./transactions"; import type { Wallet } from "@saberhq/solana-contrib"; import { BN } from "@project-serum/anchor"; -import { TOKEN_PROGRAM_ID } from "@project-serum/anchor/dist/cjs/utils/token"; -import * as metaplex from "@metaplex-foundation/mpl-token-metadata"; import { AllowedTokenData } from "../hooks/useAllowedTokenDatas"; export async function getTokens(wallet: PublicKey, connection: Connection) { @@ -79,6 +72,61 @@ export async function updateStakeStatus( receiptType?: ReceiptType ) { if (!stakePoolId) throw "Stake pool not found"; + if (selectedSentries.length <= 0) throw "No tokens selected"; + const initTxs: { tx: Transaction; signers: Signer[] }[] = []; + for (let i = 0; i < selectedSentries.length; i++) { + try { + const token = selectedSentries[i]!; + if (!token.tokenAccount) throw "Token account invalid"; + if (receiptType === ReceiptType.Receipt) { + console.log("Creating stake entry and stake mint..."); + const [initTx, , stakeMintKeypair] = await createStakeEntryAndStakeMint( + connection, + wallet, + { + stakePoolId: stakePoolId, + originalMintId: new PublicKey(token.tokenAccount.parsed.mint), + } + ); + if (initTx.instructions.length > 0) { + initTxs.push({ + tx: initTx, + signers: stakeMintKeypair ? [stakeMintKeypair] : [], + }); + } + } + } catch (e) { + console.log(e); + // notify({ + // message: `Failed to stake token ${tokens[ + // i + // ]?.stakeEntry?.pubkey.toString()}`, + // description: `${e}`, + // type: 'error', + // }) + } + } + + if (initTxs.length > 0) { + try { + await executeAllTransactions( + connection, + wallet, + initTxs.map(({ tx }) => tx), + { + signers: initTxs.map(({ signers }) => signers), + throwIndividualError: true, + notificationConfig: { + message: `Successfully staked`, + description: "Stake progress will now dynamically update", + }, + } + ); + } catch (e) { + console.log(e); + } + } + const txs: (Transaction | null)[] = await Promise.all( selectedSentries.map(async (token) => { try { @@ -116,7 +164,7 @@ export async function updateStakeStatus( }); } catch (e) { console.log({ - message: `Failed to stake token ${token?.stakeEntry?.pubkey.toString()}`, + message: `Failed to unstake token ${token?.stakeEntry?.pubkey.toString()}`, description: `${e}`, type: "error", }); @@ -124,7 +172,7 @@ export async function updateStakeStatus( } }) ); - console.log(txs); + //console.log(txs) try { await executeAllTransactions( connection, @@ -137,7 +185,9 @@ export async function updateStakeStatus( }, } ); - } catch (e) {} + } catch (e) { + console.log(e); + } return []; } diff --git a/src/utils/transactions.ts b/src/utils/transactions.ts index 7facd97..1c96a68 100644 --- a/src/utils/transactions.ts +++ b/src/utils/transactions.ts @@ -32,7 +32,7 @@ export const executeAllTransactions = async ( const recentBlockhash = (await connection.getRecentBlockhash("max")) .blockhash; - for await (const tx of transactions) { + for (const tx of transactions) { tx.feePayer = wallet.publicKey; tx.recentBlockhash = recentBlockhash; }