From f9e37fb3829deef2ab7f7c6ebf3ac29383101b0e Mon Sep 17 00:00:00 2001 From: Chiu Date: Fri, 21 Oct 2022 14:37:11 +0800 Subject: [PATCH 1/7] chore(ui): evm to solana swap --- apps/ui/package.json | 2 +- ...UsdOnSolana.tsx => ClaimTokenOnSolana.tsx} | 15 +- .../buildEuiStepsForInteraction.tsx | 28 +- .../src/fixtures/swim/interactionStateV2.ts | 15 +- .../useCreateInteractionStateV2.ts | 9 +- ...ChainEvmToSolanaSwapInteractionMutation.ts | 311 ++++++++++++++++-- .../interaction/useInteractionMutation.ts | 2 +- .../interaction/useInteractionMutationV2.ts | 2 +- apps/ui/src/models/swim/interactionStateV2.ts | 17 +- packages/solana/package.json | 4 +- packages/solana/src/client.ts | 258 +++++++++------ packages/solana/src/getAccounts.ts | 296 +++++++++++++++++ packages/solana/src/protocol.ts | 2 + yarn.lock | 4 +- 14 files changed, 785 insertions(+), 180 deletions(-) rename apps/ui/src/components/molecules/{ClaimSwimUsdOnSolana.tsx => ClaimTokenOnSolana.tsx} (78%) create mode 100644 packages/solana/src/getAccounts.ts diff --git a/apps/ui/package.json b/apps/ui/package.json index a87d9fba5..3629cb6b1 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -55,7 +55,7 @@ "@swim-io/evm": "^0.40.0", "@swim-io/evm-contracts": "^0.40.0", "@swim-io/pool-math": "^0.40.0", - "@swim-io/solana": "^0.40.0", + "@swim-io/solana": "workspace:^", "@swim-io/solana-contracts": "^0.40.0", "@swim-io/token-projects": "^0.40.0", "@swim-io/utils": "^0.40.0", diff --git a/apps/ui/src/components/molecules/ClaimSwimUsdOnSolana.tsx b/apps/ui/src/components/molecules/ClaimTokenOnSolana.tsx similarity index 78% rename from apps/ui/src/components/molecules/ClaimSwimUsdOnSolana.tsx rename to apps/ui/src/components/molecules/ClaimTokenOnSolana.tsx index 3590727b0..aec22fa98 100644 --- a/apps/ui/src/components/molecules/ClaimSwimUsdOnSolana.tsx +++ b/apps/ui/src/components/molecules/ClaimTokenOnSolana.tsx @@ -1,36 +1,31 @@ import { EuiLoadingSpinner, EuiText } from "@elastic/eui"; +import type { TokenConfig } from "@swim-io/core/types"; import { SOLANA_ECOSYSTEM_ID } from "@swim-io/solana"; import { TOKEN_PROJECTS_BY_ID } from "@swim-io/token-projects"; import type { VFC } from "react"; import { useTranslation } from "react-i18next"; -import { useSwimUsd } from "../../hooks"; - import { TxEcosystemList } from "./TxList"; interface Props { readonly isLoading: boolean; + readonly tokenConfig: TokenConfig; readonly transactions: readonly string[]; } -export const ClaimSwimUsdOnSolana: VFC = ({ +export const ClaimTokenOnSolana: VFC = ({ isLoading, + tokenConfig, transactions, }) => { const { t } = useTranslation(); - const swimUsd = useSwimUsd(); - - if (swimUsd === null) { - return null; - } - return ( {isLoading && } {t("recent_interactions.claim_token_on_solana", { - tokenName: TOKEN_PROJECTS_BY_ID[swimUsd.projectId].displayName, + tokenName: TOKEN_PROJECTS_BY_ID[tokenConfig.projectId].displayName, })} diff --git a/apps/ui/src/components/molecules/InteractionStateComponentV2/buildEuiStepsForInteraction.tsx b/apps/ui/src/components/molecules/InteractionStateComponentV2/buildEuiStepsForInteraction.tsx index 41fae87d0..bdfc046c7 100644 --- a/apps/ui/src/components/molecules/InteractionStateComponentV2/buildEuiStepsForInteraction.tsx +++ b/apps/ui/src/components/molecules/InteractionStateComponentV2/buildEuiStepsForInteraction.tsx @@ -25,7 +25,7 @@ import { isTargetChainOperationCompleted, } from "../../../models"; import { AddTransfer } from "../AddTransfer"; -import { ClaimSwimUsdOnSolana } from "../ClaimSwimUsdOnSolana"; +import { ClaimTokenOnSolana } from "../ClaimTokenOnSolana"; import { RemoveTransfer } from "../RemoveTransfer"; import { SwapFromSwimUsd } from "../SwapFromSwimUsd"; import { SwapToSwimUsd } from "../SwapToSwimUsd"; @@ -314,9 +314,10 @@ const buildClaimTokenOnSolanaStep = ( interactionStatus: InteractionStatusV2, ): EuiStepProps => { const { - claimTokenOnSolanaTxId, - postVaaOnSolanaTxIds, - swapFromSwimUsdTxId, + verifySignatureTxId, + postVaaOnSolanaTxId, + completeNativeWithPayloadTxId, + processSwimPayloadTxId, interaction: { params: { toTokenData }, }, @@ -333,21 +334,16 @@ const buildClaimTokenOnSolanaStep = ( status, children: ( <> - - {!isSwimUsd(toTokenData.tokenConfig) && ( - - )} ), }; diff --git a/apps/ui/src/fixtures/swim/interactionStateV2.ts b/apps/ui/src/fixtures/swim/interactionStateV2.ts index 45e51589f..7df06ce73 100644 --- a/apps/ui/src/fixtures/swim/interactionStateV2.ts +++ b/apps/ui/src/fixtures/swim/interactionStateV2.ts @@ -370,10 +370,11 @@ export const CROSS_CHAIN_EVM_TO_SOLANA_SWAP_INTERACTION_STATE_INIT: CrossChainEv requiredSplTokenAccounts: SPL_TOKEN_ACCOUNTS_INIT, approvalTxIds: [], crossChainInitiateTxId: null, - signatureSetAddress: null, - postVaaOnSolanaTxIds: [], - claimTokenOnSolanaTxId: null, - swapFromSwimUsdTxId: null, + auxiliarySignerPublicKey: null, + verifySignatureTxId: null, + postVaaOnSolanaTxId: null, + completeNativeWithPayloadTxId: null, + processSwimPayloadTxId: null, }; export const CROSS_CHAIN_EVM_TO_SOLANA_SWAP_INTERACTION_STATE_EXISTING_SPL_TOKEN_ACCOUNTS: CrossChainEvmToSolanaSwapInteractionState = @@ -398,16 +399,16 @@ export const CROSS_CHAIN_EVM_TO_SOLANA_SWAP_INTERACTION_STATE_SWAP_AND_TRANSFER_ export const CROSS_CHAIN_EVM_TO_SOLANA_SWAP_INTERACTION_STATE_POST_VAA_COMPLETED: CrossChainEvmToSolanaSwapInteractionState = { ...CROSS_CHAIN_EVM_TO_SOLANA_SWAP_INTERACTION_STATE_SWAP_AND_TRANSFER_COMPLETED, - postVaaOnSolanaTxIds: [ + verifySignatureTxId: "53r98E5EiffkmJ6WVA2VKmq78LVCT4zcRVxo76EWoUFiNpdxbno7UVeUT6oQgsVM3xeU99mQmnUjFVscz7PC1gK8", + postVaaOnSolanaTxId: "53r98E5EiffkmJ6WVA2VKmq78LVCT4zcRVxo76EWoUFiNpdxbno7UVeUT6oQgsVM3xeU99mQmnUjFVscz7PC1gK9", - ], }; export const CROSS_CHAIN_EVM_TO_SOLANA_SWAP_INTERACTION_STATE_COMPLETED: CrossChainEvmToSolanaSwapInteractionState = { ...CROSS_CHAIN_EVM_TO_SOLANA_SWAP_INTERACTION_STATE_POST_VAA_COMPLETED, - claimTokenOnSolanaTxId: + completeNativeWithPayloadTxId: "53r98E5EiffkmJ6WVA2VKmq78LVCT4zcRVxo76EWoUFiNpdxbno7UVeUT6oQgsVM3xeU99mQmnUjFVscz7PC1gK8", }; diff --git a/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.ts b/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.ts index 09ad899a4..8feca3c3e 100644 --- a/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.ts +++ b/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.ts @@ -212,10 +212,11 @@ const createSwapInteractionState = ( requiredSplTokenAccounts, approvalTxIds: [], crossChainInitiateTxId: null, - signatureSetAddress: null, - postVaaOnSolanaTxIds: [], - claimTokenOnSolanaTxId: null, - swapFromSwimUsdTxId: null, + auxiliarySignerPublicKey: null, + verifySignatureTxId: null, + postVaaOnSolanaTxId: null, + completeNativeWithPayloadTxId: null, + processSwimPayloadTxId: null, }; } }; diff --git a/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts b/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts index 04dec0977..c800743c6 100644 --- a/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts +++ b/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts @@ -1,42 +1,299 @@ -import { useMutation } from "react-query"; +import type { ChainId } from "@certusone/wormhole-sdk"; +import { + getEmitterAddressEth, + parseSequenceFromLogEth, +} from "@certusone/wormhole-sdk"; +import { Keypair } from "@solana/web3.js"; +import { getTokenDetails } from "@swim-io/core"; +import { EVM_ECOSYSTEMS, isEvmEcosystemId } from "@swim-io/evm"; +import { Routing__factory } from "@swim-io/evm-contracts"; +import { + SOLANA_ECOSYSTEM_ID, + SolanaTxType, + findTokenAccountForMint, +} from "@swim-io/solana"; +import { TOKEN_PROJECTS_BY_ID } from "@swim-io/token-projects"; +import { useMutation, useQueryClient } from "react-query"; +import shallow from "zustand/shallow.js"; -import { useInteractionStateV2 } from "../../core/store"; +import { getWormholeRetries } from "../../config"; +import { selectConfig } from "../../core/selectors"; +import { useEnvironment, useInteractionStateV2 } from "../../core/store"; import type { CrossChainEvmToSolanaSwapInteractionState } from "../../models"; -import { InteractionType, SwapType } from "../../models"; +import { + InteractionType, + SwapType, + findOrCreateSplTokenAccount, + getSignedVaaWithRetry, + humanDecimalToAtomicString, +} from "../../models"; +import { useWallets } from "../crossEcosystem"; +import { useGetEvmClient } from "../evm"; +import { useSolanaClient, useUserSolanaTokenAccountsQuery } from "../solana"; +import { useSwimUsd } from "../swim"; export const useCrossChainEvmToSolanaSwapInteractionMutation = () => { + const queryClient = useQueryClient(); + const { data: existingSplTokenAccounts = [] } = + useUserSolanaTokenAccountsQuery(); const { updateInteractionState } = useInteractionStateV2(); + const wallets = useWallets(); + const solanaClient = useSolanaClient(); + const getEvmClient = useGetEvmClient(); + const { env } = useEnvironment(); + const config = useEnvironment(selectConfig, shallow); + const { ecosystems, wormhole } = config; + const swimUsd = useSwimUsd(); + return useMutation( - // eslint-disable-next-line @typescript-eslint/require-await async (interactionState: CrossChainEvmToSolanaSwapInteractionState) => { - const { interaction } = interactionState; - - // TODO: Handle cross chain evm to solana swap, swapAndTransfer - - updateInteractionState(interaction.id, (draft) => { - if (draft.interactionType !== InteractionType.SwapV2) { - throw new Error("Interaction type mismatch"); + if (swimUsd === null) { + throw new Error("SwimUsd not found"); + } + if (wormhole === null) { + throw new Error("No Wormhole RPC configured"); + } + const { interaction, requiredSplTokenAccounts } = interactionState; + const { fromTokenData, toTokenData, firstMinimumOutputAmount } = + interaction.params; + if (firstMinimumOutputAmount === null) { + throw new Error("Missing first minimum output amount"); + } + const fromEcosystem = fromTokenData.ecosystemId; + const toEcosystem = toTokenData.ecosystemId; + if ( + !isEvmEcosystemId(fromEcosystem) || + toEcosystem !== SOLANA_ECOSYSTEM_ID + ) { + throw new Error("Expect ecosystem id"); + } + const fromWallet = wallets[fromEcosystem].wallet; + if ( + fromWallet === null || + fromWallet.address === null || + fromWallet.signer === null + ) { + throw new Error(`${fromEcosystem} wallet not found`); + } + const toWallet = wallets[toEcosystem].wallet; + if ( + toWallet === null || + toWallet.address === null || + toWallet.publicKey === null + ) { + throw new Error(`${toEcosystem} wallet not found`); + } + const fromTokenSpec = fromTokenData.tokenConfig; + const toTokenSpec = toTokenData.tokenConfig; + const fromChainConfig = EVM_ECOSYSTEMS[fromEcosystem].chains[env] ?? null; + if (fromChainConfig === null) { + throw new Error(`${fromEcosystem} chain config not found`); + } + const fromTokenDetails = getTokenDetails( + fromChainConfig, + fromTokenSpec.projectId, + ); + await fromWallet.switchNetwork(fromChainConfig.chainId); + const evmClient = getEvmClient(fromEcosystem); + const fromRouting = Routing__factory.connect( + fromChainConfig.routingContractAddress, + evmClient.provider, + ); + const splTokenAccounts = await Promise.all( + Object.keys(requiredSplTokenAccounts).map(async (mint) => { + const { tokenAccount, creationTxId } = + await findOrCreateSplTokenAccount({ + env: interaction.env, + solanaClient, + wallet: toWallet, + queryClient, + splTokenMintAddress: mint, + splTokenAccounts: existingSplTokenAccounts, + }); + // Update interactionState + if (creationTxId !== null) { + updateInteractionState(interaction.id, (draft) => { + if ( + draft.interactionType !== InteractionType.SwapV2 || + draft.swapType !== SwapType.SingleChainSolana + ) { + throw new Error("Interaction type mismatch"); + } + draft.requiredSplTokenAccounts[mint].txId = creationTxId; + }); + } + return tokenAccount; + }), + ); + const swimUsdAccount = findTokenAccountForMint( + swimUsd.nativeDetails.address, + toWallet.address, + splTokenAccounts, + ); + if (swimUsdAccount === null) { + throw new Error("SwimUsd account not found"); + } + const memo = Buffer.from(interaction.id, "hex"); + let crossChainInitiateTxId = interactionState.crossChainInitiateTxId; + if (crossChainInitiateTxId === null) { + const atomicAmount = humanDecimalToAtomicString( + fromTokenData.value, + fromTokenData.tokenConfig, + fromTokenData.ecosystemId, + ); + const approveTxGenerator = evmClient.generateErc20ApproveTxs({ + atomicAmount, + wallet: fromWallet, + mintAddress: fromTokenDetails.address, + spenderAddress: fromChainConfig.routingContractAddress, + }); + for await (const result of approveTxGenerator) { + updateInteractionState(interaction.id, (draft) => { + if ( + draft.interactionType !== InteractionType.SwapV2 || + draft.swapType !== SwapType.CrossChainEvmToSolana + ) { + throw new Error("Interaction type mismatch"); + } + draft.approvalTxIds.push(result.tx.id); + }); } - if (draft.swapType !== SwapType.CrossChainEvmToSolana) { - throw new Error("Swap type mismatch"); - } - // TODO: update txId - // draft.swapAndTransferTxId = txId; - }); + const crossChainInitiateRequest = await fromRouting.populateTransaction[ + "crossChainInitiate(address,uint256,uint256,uint16,bytes32,bytes16)" + ]( + fromTokenDetails.address, + atomicAmount, + humanDecimalToAtomicString( + firstMinimumOutputAmount, + swimUsd, + fromEcosystem, + ), + ecosystems[toEcosystem].wormholeChainId, + toWallet.publicKey.toBytes(), + memo, + ); + await fromWallet.switchNetwork(fromChainConfig.chainId); + const crossChainInitiateResponse = + await fromWallet.signer.sendTransaction(crossChainInitiateRequest); + const crossChainInitiateTx = await evmClient.getTx( + crossChainInitiateResponse, + ); + crossChainInitiateTxId = crossChainInitiateTx.id; + updateInteractionState(interaction.id, (draft) => { + if ( + draft.interactionType !== InteractionType.SwapV2 || + draft.swapType !== SwapType.CrossChainEvmToSolana + ) { + throw new Error("Interaction type mismatch"); + } + draft.crossChainInitiateTxId = crossChainInitiateTx.id; + }); + } + const crossChainInitiateTx = await evmClient.getTx( + crossChainInitiateTxId, + ); + const wormholeSequence = parseSequenceFromLogEth( + crossChainInitiateTx.original, + fromChainConfig.wormhole.bridge, + ); + const { wormholeChainId: emitterChainId } = ecosystems[fromEcosystem]; + const retries = getWormholeRetries(emitterChainId); + const { vaaBytes: signedVaa } = await getSignedVaaWithRetry( + [...wormhole.rpcUrls], + emitterChainId, + getEmitterAddressEth(fromChainConfig.wormhole.portal), + wormholeSequence, + undefined, + undefined, + retries, + ); + if (interactionState.postVaaOnSolanaTxId === null) { + const auxiliarySigner = Keypair.generate(); + const postVaaTxIdsGenerator = + solanaClient.generateCompleteWormholeMessageTxs({ + interactionId: interaction.id, + vaa: signedVaa, + wallet: toWallet, + auxiliarySigner, + }); + for await (const result of postVaaTxIdsGenerator) { + updateInteractionState(interaction.id, (draft) => { + if ( + draft.interactionType !== InteractionType.SwapV2 || + draft.swapType !== SwapType.CrossChainEvmToSolana + ) { + throw new Error("Interaction type mismatch"); + } - // TODO: Handle cross chain evm to solana swap, + switch (result.type) { + case SolanaTxType.WormholeVerifySignatures: + draft.verifySignatureTxId = result.tx.id; + break; + case SolanaTxType.WormholePostVaa: + draft.postVaaOnSolanaTxId = result.tx.id; + draft.auxiliarySignerPublicKey = + auxiliarySigner.publicKey.toBase58(); + break; + default: + throw new Error(`Unexpected transaction type: ${result.tx.id}`); + } + }); + } + } + if (interactionState.completeNativeWithPayloadTxId === null) { + const sourceWormholeChainId = EVM_ECOSYSTEMS[fromEcosystem] + .wormholeChainId as ChainId; + const completeNativeWithPayloadGenerator = + solanaClient.generateCompleteNativeWithPayloadTx({ + wallet: toWallet, + interactionId: interaction.id, + sourceChainConfig: fromChainConfig, + sourceWormholeChainId, + signedVaa: Buffer.from(signedVaa), + }); - updateInteractionState(interaction.id, (draft) => { - if (draft.interactionType !== InteractionType.SwapV2) { - throw new Error("Interaction type mismatch"); + for await (const result of completeNativeWithPayloadGenerator) { + updateInteractionState(interaction.id, (draft) => { + if ( + draft.interactionType !== InteractionType.SwapV2 || + draft.swapType !== SwapType.CrossChainEvmToSolana + ) { + throw new Error("Interaction type mismatch"); + } + draft.completeNativeWithPayloadTxId = result.tx.id; + }); + } + } + if (interactionState.processSwimPayloadTxId === null) { + const tokenProject = TOKEN_PROJECTS_BY_ID[toTokenSpec.projectId]; + if (tokenProject.tokenNumber === null) { + throw new Error(`Token number for ${tokenProject.symbol} not found`); } - if (draft.swapType !== SwapType.CrossChainEvmToSolana) { - throw new Error("Swap type mismatch"); + const minOutputAmount = humanDecimalToAtomicString( + toTokenData.value, + toTokenData.tokenConfig, + toTokenData.ecosystemId, + ); + const processSwimPayloadGenerator = + solanaClient.generateProcessSwimPayloadTx({ + wallet: toWallet, + interactionId: interaction.id, + signedVaa: Buffer.from(signedVaa), + targetTokenNumber: tokenProject.tokenNumber, + minOutputAmount: minOutputAmount, + }); + for await (const result of processSwimPayloadGenerator) { + updateInteractionState(interaction.id, (draft) => { + if ( + draft.interactionType !== InteractionType.SwapV2 || + draft.swapType !== SwapType.CrossChainEvmToSolana + ) { + throw new Error("Interaction type mismatch"); + } + draft.processSwimPayloadTxId = result.tx.id; + }); } - // TODO: update txId - // draft.postVaaOnSolanaTxIds = txIds; - // draft.claimTokenOnSolanaTxId = txId; - }); + } }, ); }; diff --git a/apps/ui/src/hooks/interaction/useInteractionMutation.ts b/apps/ui/src/hooks/interaction/useInteractionMutation.ts index e78a5ad9c..c428fbad3 100644 --- a/apps/ui/src/hooks/interaction/useInteractionMutation.ts +++ b/apps/ui/src/hooks/interaction/useInteractionMutation.ts @@ -41,7 +41,7 @@ export const useInteractionMutation = () => { }, onSettled: async () => { await queryClient.invalidateQueries([env, "erc20Balance"]); - await queryClient.invalidateQueries([env, "tokenAccounts"]); + await queryClient.invalidateQueries([env, "userSolanaTokenAccounts"]); }, }, ); diff --git a/apps/ui/src/hooks/interaction/useInteractionMutationV2.ts b/apps/ui/src/hooks/interaction/useInteractionMutationV2.ts index ce2f24a98..98a8fe211 100644 --- a/apps/ui/src/hooks/interaction/useInteractionMutationV2.ts +++ b/apps/ui/src/hooks/interaction/useInteractionMutationV2.ts @@ -83,7 +83,7 @@ export const useInteractionMutationV2 = () => { }, onSettled: async () => { await queryClient.invalidateQueries([env, "erc20Balance"]); - await queryClient.invalidateQueries([env, "tokenAccounts"]); + await queryClient.invalidateQueries([env, "userSolanaTokenAccounts"]); }, }, ); diff --git a/apps/ui/src/models/swim/interactionStateV2.ts b/apps/ui/src/models/swim/interactionStateV2.ts index 63d9153c2..425f1f6d6 100644 --- a/apps/ui/src/models/swim/interactionStateV2.ts +++ b/apps/ui/src/models/swim/interactionStateV2.ts @@ -4,8 +4,6 @@ import type { SolanaTx } from "@swim-io/solana"; import { SOLANA_ECOSYSTEM_ID } from "@swim-io/solana"; import { isNotNull } from "@swim-io/utils"; -import { isSwimUsd } from "../../config"; - import type { AddInteraction, RemoveExactBurnInteraction, @@ -72,10 +70,11 @@ export interface CrossChainEvmToSolanaSwapInteractionState { readonly requiredSplTokenAccounts: RequiredSplTokenAccounts; readonly approvalTxIds: readonly EvmTx["id"][]; readonly crossChainInitiateTxId: EvmTx["id"] | null; - readonly signatureSetAddress: string | null; - readonly postVaaOnSolanaTxIds: readonly SolanaTx["id"][]; - readonly claimTokenOnSolanaTxId: SolanaTx["id"] | null; - readonly swapFromSwimUsdTxId: SolanaTx["id"] | null; + readonly auxiliarySignerPublicKey: string | null; + readonly verifySignatureTxId: SolanaTx["id"] | null; + readonly postVaaOnSolanaTxId: SolanaTx["id"] | null; + readonly completeNativeWithPayloadTxId: SolanaTx["id"] | null; + readonly processSwimPayloadTxId: SolanaTx["id"] | null; } export interface AddInteractionState { @@ -176,11 +175,7 @@ export const isTargetChainOperationCompleted = ( case SwapType.CrossChainSolanaToEvm: return state.crossChainCompleteTxId !== null; case SwapType.CrossChainEvmToSolana: { - if (isSwimUsd(state.interaction.params.toTokenData.tokenConfig)) { - return state.claimTokenOnSolanaTxId !== null; - } else { - return state.swapFromSwimUsdTxId !== null; - } + return state.processSwimPayloadTxId !== null; } } }; diff --git a/packages/solana/package.json b/packages/solana/package.json index feb2b2570..d59434b69 100644 --- a/packages/solana/package.json +++ b/packages/solana/package.json @@ -38,7 +38,9 @@ "@swim-io/core": "workspace:^", "@swim-io/solana-contracts": "workspace:^", "@swim-io/token-projects": "workspace:^", - "@swim-io/utils": "workspace:^" + "@swim-io/utils": "workspace:^", + "byteify": "^2.0.10", + "keccak256": "^1.0.6" }, "devDependencies": { "@certusone/wormhole-sdk": "^0.6.2", diff --git a/packages/solana/src/client.ts b/packages/solana/src/client.ts index 2f9dfdbaa..df634fbdd 100644 --- a/packages/solana/src/client.ts +++ b/packages/solana/src/client.ts @@ -1,10 +1,8 @@ import type { ChainId } from "@certusone/wormhole-sdk"; import { createVerifySignaturesInstructionsSolana } from "@certusone/wormhole-sdk"; -import type { Accounts } from "@project-serum/anchor"; import { AnchorProvider, Program } from "@project-serum/anchor"; import { createMemoInstruction } from "@solana/spl-memo"; import { - TOKEN_PROGRAM_ID, createAssociatedTokenAccountInstruction, getAssociatedTokenAddress, getAssociatedTokenAddressSync, @@ -24,9 +22,9 @@ import { Keypair, LAMPORTS_PER_SOL, PublicKey, - SYSVAR_CLOCK_PUBKEY, } from "@solana/web3.js"; import type { + ChainConfig, CompletePortalTransferParams, InitiatePortalTransferParams, InitiatePropellerParams, @@ -42,6 +40,12 @@ import { atomicToHuman, chunks, humanToAtomic, sleep } from "@swim-io/utils"; import BN from "bn.js"; import Decimal from "decimal.js"; +import { + createCompleteNativeWithPayloadAccounts, + createProcessSwimPayloadAccounts, + getAddAccounts, + getPropellerTransferAccounts, +} from "./getAccounts"; import type { SolanaChainConfig, SolanaEcosystemId, @@ -369,6 +373,136 @@ export class SolanaClient extends Client< }; } + public async *generateCompleteNativeWithPayloadTx({ + wallet, + interactionId, + sourceWormholeChainId, + sourceChainConfig, + signedVaa, + }: { + readonly wallet: SolanaWalletAdapter; + readonly interactionId: string; + readonly sourceWormholeChainId: ChainId; + readonly sourceChainConfig: ChainConfig; + readonly signedVaa: Buffer; + }): AsyncGenerator< + TxGeneratorResult, + any, + unknown + > { + const walletPublicKey = wallet.publicKey; + if (walletPublicKey === null) { + throw new Error("Missing Solana wallet"); + } + const routingContract = this.getRoutingContract(wallet); + const swimUsdAtaPublicKey = getAssociatedTokenAddressSync( + new PublicKey(this.chainConfig.swimUsdDetails.address), + walletPublicKey, + ); + const accounts = await createCompleteNativeWithPayloadAccounts( + this.chainConfig, + new PublicKey(walletPublicKey), + signedVaa, + swimUsdAtaPublicKey, + sourceWormholeChainId, + sourceChainConfig, + ); + const setComputeUnitLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ + units: 900_000, + }); + const txRequest = await routingContract.methods + .completeNativeWithPayload() + .accounts(accounts) + .preInstructions([setComputeUnitLimitIx]) + .postInstructions([createMemoInstruction(interactionId)]) + .transaction(); + // eslint-disable-next-line functional/immutable-data + txRequest.feePayer = walletPublicKey; + const txId = await this.sendAndConfirmTx( + (tx) => wallet.signTransaction(tx), + txRequest, + ); + const tx = await this.getTx(txId); + yield { + tx, + type: SolanaTxType.SwimCompleteNativeWithPayload, + }; + } + + public async *generateProcessSwimPayloadTx({ + wallet, + interactionId, + signedVaa, + targetTokenNumber, + minOutputAmount, + }: { + readonly wallet: SolanaWalletAdapter; + readonly interactionId: string; + readonly signedVaa: Buffer; + readonly targetTokenNumber: number; + readonly minOutputAmount: string; + }): AsyncGenerator< + TxGeneratorResult, + any, + unknown + > { + const [twoPoolConfig] = this.chainConfig.pools; + const walletPublicKey = wallet.publicKey; + if (walletPublicKey === null) { + throw new Error("Missing Solana wallet"); + } + const routingContract = this.getRoutingContract(wallet); + const poolTokenAccounts = [...twoPoolConfig.tokenAccounts.values()].map( + (address) => new PublicKey(address), + ); + const userTokenAccounts = SUPPORTED_TOKEN_PROJECT_IDS.reduce( + (accumulator, tokenProjectId) => { + const { address } = getTokenDetails(this.chainConfig, tokenProjectId); + return { + ...accumulator, + [tokenProjectId]: getAssociatedTokenAddressSync( + new PublicKey(address), + walletPublicKey, + ), + }; + }, + {} as ReadonlyRecord, + ); + const accounts = await createProcessSwimPayloadAccounts( + this.chainConfig, + new PublicKey(walletPublicKey), + signedVaa, + userTokenAccounts[TokenProjectId.SwimUsd], + [ + userTokenAccounts[TokenProjectId.Usdc], + userTokenAccounts[TokenProjectId.Usdt], + ], + poolTokenAccounts, + new PublicKey(twoPoolConfig.governanceFeeAccount), + targetTokenNumber, + ); + const setComputeUnitLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ + units: 900_000, + }); + const txRequest = await routingContract.methods + .processSwimPayload(targetTokenNumber, new BN(minOutputAmount)) + .accounts(accounts) + .preInstructions([setComputeUnitLimitIx]) + .postInstructions([createMemoInstruction(interactionId)]) + .transaction(); + // eslint-disable-next-line functional/immutable-data + txRequest.feePayer = walletPublicKey; + const txId = await this.sendAndConfirmTx( + (tx) => wallet.signTransaction(tx), + txRequest, + ); + const tx = await this.getTx(txId); + yield { + tx, + type: SolanaTxType.SwimProcessSwimPayload, + }; + } + public async *generateInitiatePropellerTxs({ wallet, interactionId, @@ -401,19 +535,7 @@ export class SolanaClient extends Client< sourceTokenDetails.decimals, ).toString(); - const anchorProvider = new AnchorProvider( - this.connection, - { - ...wallet, - publicKey: senderPublicKey, - }, - { commitment: "confirmed" }, - ); - const routingContract = new Program( - idl.propeller, - this.chainConfig.routingContractAddress, - anchorProvider, - ); + const routingContract = this.getRoutingContract(wallet); let addOutputAmountAtomic: string | null = null; if (sourceTokenId !== TokenProjectId.SwimUsd) { @@ -806,90 +928,24 @@ export class SolanaClient extends Client< } } - private getAddAccounts( - userSwimUsdAtaPublicKey: PublicKey, - userTokenAccounts: readonly PublicKey[], - auxiliarySigner: PublicKey, - lpMint: PublicKey, - poolTokenAccounts: readonly PublicKey[], - poolGovernanceFeeAccount: PublicKey, - ): Accounts { - return { - propeller: new PublicKey(this.chainConfig.routingContractStateAddress), - tokenProgram: TOKEN_PROGRAM_ID, - poolTokenAccount0: poolTokenAccounts[0], - poolTokenAccount1: poolTokenAccounts[1], - lpMint, - governanceFee: poolGovernanceFeeAccount, - userTransferAuthority: auxiliarySigner, - userTokenAccount0: userTokenAccounts[0], - userTokenAccount1: userTokenAccounts[1], - userLpTokenAccount: userSwimUsdAtaPublicKey, - twoPoolProgram: new PublicKey(this.chainConfig.twoPoolContractAddress), - }; - } - - private async getPropellerTransferAccounts( - walletPublicKey: PublicKey, - swimUsdAtaPublicKey: PublicKey, - auxiliarySigner: PublicKey, - ): Promise { - const bridgePublicKey = new PublicKey(this.chainConfig.wormhole.bridge); - const portalPublicKey = new PublicKey(this.chainConfig.wormhole.portal); - const swimUsdMintPublicKey = new PublicKey( - this.chainConfig.swimUsdDetails.address, - ); - const [wormholeConfig] = await PublicKey.findProgramAddress( - [Buffer.from("Bridge")], - bridgePublicKey, - ); - const [tokenBridgeConfig] = await PublicKey.findProgramAddress( - [Buffer.from("config")], - portalPublicKey, - ); - const [custody] = await PublicKey.findProgramAddress( - [swimUsdMintPublicKey.toBytes()], - portalPublicKey, - ); - const [custodySigner] = await PublicKey.findProgramAddress( - [Buffer.from("custody_signer")], - portalPublicKey, - ); - const [authoritySigner] = await PublicKey.findProgramAddress( - [Buffer.from("authority_signer")], - portalPublicKey, - ); - const [wormholeEmitter] = await PublicKey.findProgramAddress( - [Buffer.from("emitter")], - portalPublicKey, - ); - const [wormholeSequence] = await PublicKey.findProgramAddress( - [Buffer.from("Sequence"), wormholeEmitter.toBytes()], - bridgePublicKey, + private getRoutingContract(wallet: SolanaWalletAdapter): Program { + const walletPublicKey = wallet.publicKey; + if (walletPublicKey === null) { + throw new Error("Missing Solana wallet"); + } + const anchorProvider = new AnchorProvider( + this.connection, + { + ...wallet, + publicKey: walletPublicKey, + }, + { commitment: "confirmed" }, ); - const [wormholeFeeCollector] = await PublicKey.findProgramAddress( - [Buffer.from("fee_collector")], - bridgePublicKey, + return new Program( + idl.propeller, + this.chainConfig.routingContractAddress, + anchorProvider, ); - return { - propeller: new PublicKey(this.chainConfig.routingContractStateAddress), - tokenProgram: TOKEN_PROGRAM_ID, - payer: walletPublicKey, - wormhole: bridgePublicKey, - tokenBridgeConfig, - userSwimUsdAta: swimUsdAtaPublicKey, - swimUsdMint: swimUsdMintPublicKey, - custody, - tokenBridge: portalPublicKey, - custodySigner, - authoritySigner, - wormholeConfig, - wormholeMessage: auxiliarySigner, - wormholeEmitter, - wormholeSequence, - wormholeFeeCollector, - clock: SYSVAR_CLOCK_PUBKEY, - }; } private async propellerAdd({ @@ -921,7 +977,8 @@ export class SolanaClient extends Client< }, {} as ReadonlyRecord, ); - const addAccounts = this.getAddAccounts( + const addAccounts = getAddAccounts( + this.chainConfig, userTokenAccounts[TokenProjectId.SwimUsd], [ userTokenAccounts[TokenProjectId.Usdc], @@ -981,7 +1038,8 @@ export class SolanaClient extends Client< new PublicKey(this.chainConfig.swimUsdDetails.address), senderPublicKey, ); - const transferAccounts = await this.getPropellerTransferAccounts( + const transferAccounts = await getPropellerTransferAccounts( + this.chainConfig, senderPublicKey, swimUsdTokenAccount, auxiliarySigner.publicKey, diff --git a/packages/solana/src/getAccounts.ts b/packages/solana/src/getAccounts.ts new file mode 100644 index 000000000..de45dc124 --- /dev/null +++ b/packages/solana/src/getAccounts.ts @@ -0,0 +1,296 @@ +import { + getClaimAddressSolana, + getEmitterAddressEth, +} from "@certusone/wormhole-sdk"; +import type { Accounts } from "@project-serum/anchor"; +import { BN } from "@project-serum/anchor"; +import { TOKEN_PROGRAM_ID, getAssociatedTokenAddress } from "@solana/spl-token"; +import { + PublicKey, + SYSVAR_CLOCK_PUBKEY, + SYSVAR_RENT_PUBKEY, + SystemProgram, +} from "@solana/web3.js"; +import type { ChainConfig } from "@swim-io/core/types"; +import * as byteify from "byteify"; +import keccak256 from "keccak256"; + +import type { SolanaChainConfig } from "./protocol"; + +export const getAddAccounts = ( + solanaChainConfig: SolanaChainConfig, + userSwimUsdAtaPublicKey: PublicKey, + userTokenAccounts: readonly PublicKey[], + auxiliarySigner: PublicKey, + lpMint: PublicKey, + poolTokenAccounts: readonly PublicKey[], + poolGovernanceFeeAccount: PublicKey, +): Accounts => { + return { + propeller: new PublicKey(solanaChainConfig.routingContractStateAddress), + tokenProgram: TOKEN_PROGRAM_ID, + poolTokenAccount0: poolTokenAccounts[0], + poolTokenAccount1: poolTokenAccounts[1], + lpMint, + governanceFee: poolGovernanceFeeAccount, + userTransferAuthority: auxiliarySigner, + userTokenAccount0: userTokenAccounts[0], + userTokenAccount1: userTokenAccounts[1], + userLpTokenAccount: userSwimUsdAtaPublicKey, + twoPoolProgram: new PublicKey(solanaChainConfig.twoPoolContractAddress), + }; +}; + +export const getPropellerTransferAccounts = async ( + solanaChainConfig: SolanaChainConfig, + walletPublicKey: PublicKey, + swimUsdAtaPublicKey: PublicKey, + auxiliarySigner: PublicKey, +): Promise => { + const bridgePublicKey = new PublicKey(solanaChainConfig.wormhole.bridge); + const portalPublicKey = new PublicKey(solanaChainConfig.wormhole.portal); + const swimUsdMintPublicKey = new PublicKey( + solanaChainConfig.swimUsdDetails.address, + ); + const [wormholeConfig] = await PublicKey.findProgramAddress( + [Buffer.from("Bridge")], + bridgePublicKey, + ); + const [tokenBridgeConfig] = await PublicKey.findProgramAddress( + [Buffer.from("config")], + portalPublicKey, + ); + const [custody] = await PublicKey.findProgramAddress( + [swimUsdMintPublicKey.toBytes()], + portalPublicKey, + ); + const [custodySigner] = await PublicKey.findProgramAddress( + [Buffer.from("custody_signer")], + portalPublicKey, + ); + const [authoritySigner] = await PublicKey.findProgramAddress( + [Buffer.from("authority_signer")], + portalPublicKey, + ); + const [wormholeEmitter] = await PublicKey.findProgramAddress( + [Buffer.from("emitter")], + portalPublicKey, + ); + const [wormholeSequence] = await PublicKey.findProgramAddress( + [Buffer.from("Sequence"), wormholeEmitter.toBytes()], + bridgePublicKey, + ); + const [wormholeFeeCollector] = await PublicKey.findProgramAddress( + [Buffer.from("fee_collector")], + bridgePublicKey, + ); + return { + propeller: new PublicKey(solanaChainConfig.routingContractStateAddress), + tokenProgram: TOKEN_PROGRAM_ID, + payer: walletPublicKey, + wormhole: bridgePublicKey, + tokenBridgeConfig, + userSwimUsdAta: swimUsdAtaPublicKey, + swimUsdMint: swimUsdMintPublicKey, + custody, + tokenBridge: portalPublicKey, + custodySigner, + authoritySigner, + wormholeConfig, + wormholeMessage: auxiliarySigner, + wormholeEmitter, + wormholeSequence, + wormholeFeeCollector, + clock: SYSVAR_CLOCK_PUBKEY, + }; +}; + +const hashVaa = (signedVaa: Buffer): Buffer => { + const sigStart = 6; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const numSigners = signedVaa[5]!; + const sigLength = 66; + const body = signedVaa.subarray(sigStart + sigLength * numSigners); + return keccak256(Buffer.from(body)); +}; + +export const createCompleteNativeWithPayloadAccounts = async ( + solanaChainConfig: SolanaChainConfig, + walletPublicKey: PublicKey, + signedVaa: Buffer, + swimUsdAtaPublicKey: PublicKey, + sourceWormholeChainId: number, + sourceChainConfig: ChainConfig, +): Promise => { + const bridgePublicKey = new PublicKey(solanaChainConfig.wormhole.bridge); + const portalPublicKey = new PublicKey(solanaChainConfig.wormhole.portal); + const swimUsdMintPublicKey = new PublicKey( + solanaChainConfig.swimUsdDetails.address, + ); + const [tokenBridgeConfig] = await PublicKey.findProgramAddress( + [Buffer.from("config")], + portalPublicKey, + ); + const [custody] = await PublicKey.findProgramAddress( + [swimUsdMintPublicKey.toBytes()], + portalPublicKey, + ); + const [custodySigner] = await PublicKey.findProgramAddress( + [Buffer.from("custody_signer")], + portalPublicKey, + ); + + const hash = hashVaa(signedVaa); + const [message] = await PublicKey.findProgramAddress( + [Buffer.from("PostedVAA"), hash], + bridgePublicKey, + ); + const claim = await getClaimAddressSolana( + portalPublicKey.toBase58(), + signedVaa, + ); + + const [endpoint] = await PublicKey.findProgramAddress( + [ + byteify.serializeUint16(sourceWormholeChainId), + Buffer.from( + getEmitterAddressEth(sourceChainConfig.wormhole.portal), + "hex", + ), + ], + portalPublicKey, + ); + + const propellerRedeemer = ( + await PublicKey.findProgramAddress( + [Buffer.from("redeemer")], + new PublicKey(solanaChainConfig.routingContractAddress), + ) + )[0]; + const propellerRedeemerEscrowAccount = await getAssociatedTokenAddress( + swimUsdMintPublicKey, + propellerRedeemer, + true, + ); + + return { + propeller: new PublicKey(solanaChainConfig.routingContractStateAddress), + payer: walletPublicKey, + tokenBridgeConfig, + message, + claim, + endpoint, + to: propellerRedeemerEscrowAccount, + redeemer: propellerRedeemer, + feeRecipient: swimUsdAtaPublicKey, + custody, + swimUsdMint: swimUsdMintPublicKey, + custodySigner, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + wormhole: bridgePublicKey, + tokenProgram: TOKEN_PROGRAM_ID, + tokenBridge: portalPublicKey, + }; +}; + +const getSwimPayloadMessagePda = async ( + wormholeClaim: PublicKey, + propellerProgramId: PublicKey, +): Promise => { + return await PublicKey.findProgramAddress( + [ + Buffer.from("propeller"), + Buffer.from("swim_payload"), + wormholeClaim.toBuffer(), + ], + propellerProgramId, + ); +}; + +const getToTokenNumberMapAddr = async ( + propellerState: PublicKey, + toTokenNumber: number, + propellerProgramId: PublicKey, +) => { + return await PublicKey.findProgramAddress( + [ + Buffer.from("propeller"), + Buffer.from("token_id"), + propellerState.toBuffer(), + new BN(toTokenNumber).toArrayLike(Buffer, "le", 2), + ], + propellerProgramId, + ); +}; + +export const createProcessSwimPayloadAccounts = async ( + solanaChainConfig: SolanaChainConfig, + walletPublicKey: PublicKey, + signedVaa: Buffer, + swimUsdAtaPublicKey: PublicKey, + userTokenAccounts: readonly PublicKey[], + poolTokenAccounts: readonly PublicKey[], + governanceFeeKey: PublicKey, + toTokenNumber: number, +) => { + const propeller = new PublicKey( + solanaChainConfig.routingContractStateAddress, + ); + const portalPublicKey = new PublicKey(solanaChainConfig.wormhole.portal); + const swimUsdMintPublicKey = new PublicKey( + solanaChainConfig.swimUsdDetails.address, + ); + const claim = await getClaimAddressSolana( + portalPublicKey.toBase58(), + signedVaa, + ); + const propellerProgramId = new PublicKey( + solanaChainConfig.routingContractAddress, + ); + const [swimPayloadMessage] = await getSwimPayloadMessagePda( + claim, + propellerProgramId, + ); + const propellerRedeemer = ( + await PublicKey.findProgramAddress( + [Buffer.from("redeemer")], + propellerProgramId, + ) + )[0]; + const propellerRedeemerEscrowAccount = await getAssociatedTokenAddress( + swimUsdMintPublicKey, + propellerRedeemer, + true, + ); + const twoPoolConfig = solanaChainConfig.pools[0]; + const twoPoolProgramId = new PublicKey(twoPoolConfig.contract); + const twoPoolAddress = new PublicKey(twoPoolConfig.address); + const [tokenIdMap] = await getToTokenNumberMapAddr( + propeller, + toTokenNumber, + propellerProgramId, + ); + return { + propeller, + payer: walletPublicKey, + claim, + swimPayloadMessage: new PublicKey(swimPayloadMessage), + swimPayloadMessagePayer: walletPublicKey, + redeemer: propellerRedeemer, + redeemerEscrow: propellerRedeemerEscrowAccount, + pool: twoPoolAddress, + poolTokenAccount0: poolTokenAccounts[0], + poolTokenAccount1: poolTokenAccounts[1], + lpMint: swimUsdMintPublicKey, + governanceFee: governanceFeeKey, + userTransferAuthority: walletPublicKey, + userTokenAccount0: userTokenAccounts[0], + userTokenAccount1: userTokenAccounts[1], + userLpTokenAccount: swimUsdAtaPublicKey, + tokenProgram: TOKEN_PROGRAM_ID, + twoPoolProgram: twoPoolProgramId, + systemProgram: SystemProgram.programId, + tokenIdMap, + }; +}; diff --git a/packages/solana/src/protocol.ts b/packages/solana/src/protocol.ts index fd4b43525..84e4fa96e 100644 --- a/packages/solana/src/protocol.ts +++ b/packages/solana/src/protocol.ts @@ -44,6 +44,8 @@ export enum SolanaTxType { WormholePostVaa = "wormhole:postVaa", SwimPropellerAdd = "swimPropeller:add", SwimPropellerTransfer = "swimPropeller:transfer", + SwimCompleteNativeWithPayload = "swim:completeNativeWithPayload", + SwimProcessSwimPayload = "swim:processSwimPayload", } export type SolanaTx = Tx; diff --git a/yarn.lock b/yarn.lock index 9170f8b16..b04a6d91f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7826,6 +7826,7 @@ __metadata: "@typescript-eslint/eslint-plugin": ^5.38.1 "@typescript-eslint/parser": ^5.38.1 bn.js: ^5.2.1 + byteify: ^2.0.10 decimal.js: ^10.3.1 eslint: ^8.18.0 eslint-config-prettier: ^8.5.0 @@ -7837,6 +7838,7 @@ __metadata: eslint-plugin-prettier: ^4.0.0 ethers: ^5.7.0 jest: ^28.1.1 + keccak256: ^1.0.6 prettier: ^2.7.1 ts-jest: ^28.0.5 typescript: ~4.8.4 @@ -7977,7 +7979,7 @@ __metadata: "@swim-io/evm": ^0.40.0 "@swim-io/evm-contracts": ^0.40.0 "@swim-io/pool-math": ^0.40.0 - "@swim-io/solana": ^0.40.0 + "@swim-io/solana": "workspace:^" "@swim-io/solana-contracts": ^0.40.0 "@swim-io/token-projects": ^0.40.0 "@swim-io/tsconfig": "workspace:^" From 709bb34de6e7045b998ad4dae64d32cc8780ddd9 Mon Sep 17 00:00:00 2001 From: Chiu Date: Tue, 25 Oct 2022 19:42:56 +0800 Subject: [PATCH 2/7] chore(ui): fix test --- .../useCreateInteractionStateV2.test.ts.snap | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/ui/src/hooks/interaction/__snapshots__/useCreateInteractionStateV2.test.ts.snap b/apps/ui/src/hooks/interaction/__snapshots__/useCreateInteractionStateV2.test.ts.snap index 523290309..f4bd74c55 100644 --- a/apps/ui/src/hooks/interaction/__snapshots__/useCreateInteractionStateV2.test.ts.snap +++ b/apps/ui/src/hooks/interaction/__snapshots__/useCreateInteractionStateV2.test.ts.snap @@ -354,7 +354,8 @@ Object { exports[`useCreateInteractionStateV2 should create state for Swap from ETHEREUM USDC to SOLANA USDC 1`] = ` Object { "approvalTxIds": Array [], - "claimTokenOnSolanaTxId": null, + "auxiliarySignerPublicKey": null, + "completeNativeWithPayloadTxId": null, "crossChainInitiateTxId": null, "interaction": Object { "connectedWallets": Object { @@ -415,7 +416,8 @@ Object { "type": 5, }, "interactionType": 5, - "postVaaOnSolanaTxIds": Array [], + "postVaaOnSolanaTxId": null, + "processSwimPayloadTxId": null, "requiredSplTokenAccounts": Object { "3ngTtoyP9GFybFifX1dr7gCFXFiM2Wr6NfXn6EuU7k6C": Object { "isExistingAccount": false, @@ -430,9 +432,8 @@ Object { "txId": null, }, }, - "signatureSetAddress": null, - "swapFromSwimUsdTxId": null, "swapType": "CrossChainEvmToSolana", + "verifySignatureTxId": null, "version": 2, } `; From 1c766512cc4e4aa62f4d47200fb06b6f1a3b4856 Mon Sep 17 00:00:00 2001 From: Chiu Date: Tue, 25 Oct 2022 22:23:51 +0800 Subject: [PATCH 3/7] chore(ui): rename functions, DRYer --- .../buildEuiStepsForInteraction.tsx | 4 +- .../src/fixtures/swim/interactionStateV2.ts | 4 +- .../useCreateInteractionStateV2.test.ts.snap | 2 +- .../useCreateInteractionStateV2.ts | 2 +- ...ChainEvmToSolanaSwapInteractionMutation.ts | 6 +- apps/ui/src/models/swim/interactionStateV2.ts | 2 +- packages/solana/src/client.ts | 104 +++++------------- packages/solana/src/getAccounts.ts | 86 ++++++++++----- .../solana/src/supportedTokenProjectIds.ts | 16 +++ 9 files changed, 113 insertions(+), 113 deletions(-) create mode 100644 packages/solana/src/supportedTokenProjectIds.ts diff --git a/apps/ui/src/components/molecules/InteractionStateComponentV2/buildEuiStepsForInteraction.tsx b/apps/ui/src/components/molecules/InteractionStateComponentV2/buildEuiStepsForInteraction.tsx index bdfc046c7..6f4daaa90 100644 --- a/apps/ui/src/components/molecules/InteractionStateComponentV2/buildEuiStepsForInteraction.tsx +++ b/apps/ui/src/components/molecules/InteractionStateComponentV2/buildEuiStepsForInteraction.tsx @@ -314,7 +314,7 @@ const buildClaimTokenOnSolanaStep = ( interactionStatus: InteractionStatusV2, ): EuiStepProps => { const { - verifySignatureTxId, + verifySignaturesTxId, postVaaOnSolanaTxId, completeNativeWithPayloadTxId, processSwimPayloadTxId, @@ -338,7 +338,7 @@ const buildClaimTokenOnSolanaStep = ( isLoading={status === "loading"} tokenConfig={toTokenData.tokenConfig} transactions={[ - verifySignatureTxId, + verifySignaturesTxId, postVaaOnSolanaTxId, completeNativeWithPayloadTxId, processSwimPayloadTxId, diff --git a/apps/ui/src/fixtures/swim/interactionStateV2.ts b/apps/ui/src/fixtures/swim/interactionStateV2.ts index 7df06ce73..ff8f91011 100644 --- a/apps/ui/src/fixtures/swim/interactionStateV2.ts +++ b/apps/ui/src/fixtures/swim/interactionStateV2.ts @@ -371,7 +371,7 @@ export const CROSS_CHAIN_EVM_TO_SOLANA_SWAP_INTERACTION_STATE_INIT: CrossChainEv approvalTxIds: [], crossChainInitiateTxId: null, auxiliarySignerPublicKey: null, - verifySignatureTxId: null, + verifySignaturesTxId: null, postVaaOnSolanaTxId: null, completeNativeWithPayloadTxId: null, processSwimPayloadTxId: null, @@ -399,7 +399,7 @@ export const CROSS_CHAIN_EVM_TO_SOLANA_SWAP_INTERACTION_STATE_SWAP_AND_TRANSFER_ export const CROSS_CHAIN_EVM_TO_SOLANA_SWAP_INTERACTION_STATE_POST_VAA_COMPLETED: CrossChainEvmToSolanaSwapInteractionState = { ...CROSS_CHAIN_EVM_TO_SOLANA_SWAP_INTERACTION_STATE_SWAP_AND_TRANSFER_COMPLETED, - verifySignatureTxId: + verifySignaturesTxId: "53r98E5EiffkmJ6WVA2VKmq78LVCT4zcRVxo76EWoUFiNpdxbno7UVeUT6oQgsVM3xeU99mQmnUjFVscz7PC1gK8", postVaaOnSolanaTxId: "53r98E5EiffkmJ6WVA2VKmq78LVCT4zcRVxo76EWoUFiNpdxbno7UVeUT6oQgsVM3xeU99mQmnUjFVscz7PC1gK9", diff --git a/apps/ui/src/hooks/interaction/__snapshots__/useCreateInteractionStateV2.test.ts.snap b/apps/ui/src/hooks/interaction/__snapshots__/useCreateInteractionStateV2.test.ts.snap index f4bd74c55..6e20d8dcc 100644 --- a/apps/ui/src/hooks/interaction/__snapshots__/useCreateInteractionStateV2.test.ts.snap +++ b/apps/ui/src/hooks/interaction/__snapshots__/useCreateInteractionStateV2.test.ts.snap @@ -433,7 +433,7 @@ Object { }, }, "swapType": "CrossChainEvmToSolana", - "verifySignatureTxId": null, + "verifySignaturesTxId": null, "version": 2, } `; diff --git a/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.ts b/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.ts index 8feca3c3e..12925ecfa 100644 --- a/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.ts +++ b/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.ts @@ -213,7 +213,7 @@ const createSwapInteractionState = ( approvalTxIds: [], crossChainInitiateTxId: null, auxiliarySignerPublicKey: null, - verifySignatureTxId: null, + verifySignaturesTxId: null, postVaaOnSolanaTxId: null, completeNativeWithPayloadTxId: null, processSwimPayloadTxId: null, diff --git a/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts b/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts index c800743c6..a8d8c4258 100644 --- a/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts +++ b/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts @@ -227,7 +227,7 @@ export const useCrossChainEvmToSolanaSwapInteractionMutation = () => { switch (result.type) { case SolanaTxType.WormholeVerifySignatures: - draft.verifySignatureTxId = result.tx.id; + draft.verifySignaturesTxId = result.tx.id; break; case SolanaTxType.WormholePostVaa: draft.postVaaOnSolanaTxId = result.tx.id; @@ -269,7 +269,7 @@ export const useCrossChainEvmToSolanaSwapInteractionMutation = () => { if (tokenProject.tokenNumber === null) { throw new Error(`Token number for ${tokenProject.symbol} not found`); } - const minOutputAmount = humanDecimalToAtomicString( + const minimumOutputAmount = humanDecimalToAtomicString( toTokenData.value, toTokenData.tokenConfig, toTokenData.ecosystemId, @@ -280,7 +280,7 @@ export const useCrossChainEvmToSolanaSwapInteractionMutation = () => { interactionId: interaction.id, signedVaa: Buffer.from(signedVaa), targetTokenNumber: tokenProject.tokenNumber, - minOutputAmount: minOutputAmount, + minimumOutputAmount, }); for await (const result of processSwimPayloadGenerator) { updateInteractionState(interaction.id, (draft) => { diff --git a/apps/ui/src/models/swim/interactionStateV2.ts b/apps/ui/src/models/swim/interactionStateV2.ts index 425f1f6d6..ba019298d 100644 --- a/apps/ui/src/models/swim/interactionStateV2.ts +++ b/apps/ui/src/models/swim/interactionStateV2.ts @@ -71,7 +71,7 @@ export interface CrossChainEvmToSolanaSwapInteractionState { readonly approvalTxIds: readonly EvmTx["id"][]; readonly crossChainInitiateTxId: EvmTx["id"] | null; readonly auxiliarySignerPublicKey: string | null; - readonly verifySignatureTxId: SolanaTx["id"] | null; + readonly verifySignaturesTxId: SolanaTx["id"] | null; readonly postVaaOnSolanaTxId: SolanaTx["id"] | null; readonly completeNativeWithPayloadTxId: SolanaTx["id"] | null; readonly processSwimPayloadTxId: SolanaTx["id"] | null; diff --git a/packages/solana/src/client.ts b/packages/solana/src/client.ts index df634fbdd..73c6138a5 100644 --- a/packages/solana/src/client.ts +++ b/packages/solana/src/client.ts @@ -35,15 +35,14 @@ import { Client, getTokenDetails } from "@swim-io/core"; import type { Propeller } from "@swim-io/solana-contracts"; import { idl } from "@swim-io/solana-contracts"; import { TokenProjectId } from "@swim-io/token-projects"; -import type { ReadonlyRecord } from "@swim-io/utils"; import { atomicToHuman, chunks, humanToAtomic, sleep } from "@swim-io/utils"; import BN from "bn.js"; import Decimal from "decimal.js"; import { - createCompleteNativeWithPayloadAccounts, - createProcessSwimPayloadAccounts, + getProcessSwimPayloadAccounts, getAddAccounts, + getCompleteNativeWithPayloadAccounts, getPropellerTransferAccounts, } from "./getAccounts"; import type { @@ -54,6 +53,8 @@ import type { import { SOLANA_ECOSYSTEM_ID, SolanaTxType } from "./protocol"; import type { TokenAccount } from "./serialization"; import { deserializeTokenAccount } from "./serialization"; +import type { SupportedTokenProjectId } from "./supportedTokenProjectIds"; +import { isSupportedTokenProjectId } from "./supportedTokenProjectIds"; import { createApproveAndRevokeIxs, createTx, @@ -85,21 +86,6 @@ interface GenerateVerifySignaturesTxsParams readonly auxiliarySigner: Keypair; } -type SupportedTokenProjectId = - | TokenProjectId.SwimUsd - | TokenProjectId.Usdc - | TokenProjectId.Usdt; - -const SUPPORTED_TOKEN_PROJECT_IDS = [ - TokenProjectId.SwimUsd, - TokenProjectId.Usdc, - TokenProjectId.Usdt, -]; - -const isSupportedTokenProjectId = ( - id: TokenProjectId, -): id is SupportedTokenProjectId => SUPPORTED_TOKEN_PROJECT_IDS.includes(id); - interface PropellerAddParams { readonly wallet: SolanaWalletAdapter; readonly routingContract: Program; @@ -392,18 +378,13 @@ export class SolanaClient extends Client< > { const walletPublicKey = wallet.publicKey; if (walletPublicKey === null) { - throw new Error("Missing Solana wallet"); + throw new Error("Missing Solana wallet public key"); } const routingContract = this.getRoutingContract(wallet); - const swimUsdAtaPublicKey = getAssociatedTokenAddressSync( - new PublicKey(this.chainConfig.swimUsdDetails.address), - walletPublicKey, - ); - const accounts = await createCompleteNativeWithPayloadAccounts( + const accounts = await getCompleteNativeWithPayloadAccounts( this.chainConfig, - new PublicKey(walletPublicKey), + walletPublicKey, signedVaa, - swimUsdAtaPublicKey, sourceWormholeChainId, sourceChainConfig, ); @@ -434,13 +415,13 @@ export class SolanaClient extends Client< interactionId, signedVaa, targetTokenNumber, - minOutputAmount, + minimumOutputAmount, }: { readonly wallet: SolanaWalletAdapter; readonly interactionId: string; readonly signedVaa: Buffer; readonly targetTokenNumber: number; - readonly minOutputAmount: string; + readonly minimumOutputAmount: string; }): AsyncGenerator< TxGeneratorResult, any, @@ -449,35 +430,17 @@ export class SolanaClient extends Client< const [twoPoolConfig] = this.chainConfig.pools; const walletPublicKey = wallet.publicKey; if (walletPublicKey === null) { - throw new Error("Missing Solana wallet"); + throw new Error("Missing Solana wallet public key"); } const routingContract = this.getRoutingContract(wallet); - const poolTokenAccounts = [...twoPoolConfig.tokenAccounts.values()].map( - (address) => new PublicKey(address), - ); - const userTokenAccounts = SUPPORTED_TOKEN_PROJECT_IDS.reduce( - (accumulator, tokenProjectId) => { - const { address } = getTokenDetails(this.chainConfig, tokenProjectId); - return { - ...accumulator, - [tokenProjectId]: getAssociatedTokenAddressSync( - new PublicKey(address), - walletPublicKey, - ), - }; - }, - {} as ReadonlyRecord, - ); - const accounts = await createProcessSwimPayloadAccounts( + const poolTokenAccountPublicKeys = [ + ...twoPoolConfig.tokenAccounts.values(), + ].map((address) => new PublicKey(address)); + const accounts = await getProcessSwimPayloadAccounts( this.chainConfig, new PublicKey(walletPublicKey), signedVaa, - userTokenAccounts[TokenProjectId.SwimUsd], - [ - userTokenAccounts[TokenProjectId.Usdc], - userTokenAccounts[TokenProjectId.Usdt], - ], - poolTokenAccounts, + poolTokenAccountPublicKeys, new PublicKey(twoPoolConfig.governanceFeeAccount), targetTokenNumber, ); @@ -485,7 +448,7 @@ export class SolanaClient extends Client< units: 900_000, }); const txRequest = await routingContract.methods - .processSwimPayload(targetTokenNumber, new BN(minOutputAmount)) + .processSwimPayload(targetTokenNumber, new BN(minimumOutputAmount)) .accounts(accounts) .preInstructions([setComputeUnitLimitIx]) .postInstructions([createMemoInstruction(interactionId)]) @@ -957,33 +920,19 @@ export class SolanaClient extends Client< inputAmountAtomic, auxiliarySigner = Keypair.generate(), }: WithOptionalAuxiliarySigner): Promise { + const walletPublicKey = wallet.publicKey; + if (walletPublicKey === null) { + throw new Error("Missing Solana wallet public key"); + } const [twoPoolConfig] = this.chainConfig.pools; const addInputAmounts = sourceTokenId === TokenProjectId.Usdc ? [inputAmountAtomic, "0"] : ["0", inputAmountAtomic]; const addMaxFee = "0"; // TODO: Change to a real value - - const userTokenAccounts = SUPPORTED_TOKEN_PROJECT_IDS.reduce( - (accumulator, tokenProjectId) => { - const { address } = getTokenDetails(this.chainConfig, tokenProjectId); - return { - ...accumulator, - [tokenProjectId]: getAssociatedTokenAddressSync( - new PublicKey(address), - senderPublicKey, - ), - }; - }, - {} as ReadonlyRecord, - ); const addAccounts = getAddAccounts( this.chainConfig, - userTokenAccounts[TokenProjectId.SwimUsd], - [ - userTokenAccounts[TokenProjectId.Usdc], - userTokenAccounts[TokenProjectId.Usdt], - ], + walletPublicKey, auxiliarySigner.publicKey, new PublicKey(this.chainConfig.swimUsdDetails.address), [...twoPoolConfig.tokenAccounts.values()].map( @@ -991,9 +940,16 @@ export class SolanaClient extends Client< ), new PublicKey(twoPoolConfig.governanceFeeAccount), ); - + const sourceTokenMint = getTokenDetails( + this.chainConfig, + sourceTokenId, + ).address; + const sourceTokenAccountPublicKey = getAssociatedTokenAddressSync( + new PublicKey(sourceTokenMint), + senderPublicKey, + ); const [approveIx, revokeIx] = await createApproveAndRevokeIxs( - userTokenAccounts[sourceTokenId], + sourceTokenAccountPublicKey, inputAmountAtomic, auxiliarySigner.publicKey, senderPublicKey, diff --git a/packages/solana/src/getAccounts.ts b/packages/solana/src/getAccounts.ts index de45dc124..1c6cd885b 100644 --- a/packages/solana/src/getAccounts.ts +++ b/packages/solana/src/getAccounts.ts @@ -4,28 +4,55 @@ import { } from "@certusone/wormhole-sdk"; import type { Accounts } from "@project-serum/anchor"; import { BN } from "@project-serum/anchor"; -import { TOKEN_PROGRAM_ID, getAssociatedTokenAddress } from "@solana/spl-token"; +import { + TOKEN_PROGRAM_ID, + getAssociatedTokenAddress, + getAssociatedTokenAddressSync, +} from "@solana/spl-token"; import { PublicKey, SYSVAR_CLOCK_PUBKEY, SYSVAR_RENT_PUBKEY, SystemProgram, } from "@solana/web3.js"; -import type { ChainConfig } from "@swim-io/core/types"; +import type { ChainConfig } from "@swim-io/core"; +import { getTokenDetails } from "@swim-io/core"; +import { TokenProjectId } from "@swim-io/token-projects"; +import type { ReadonlyRecord } from "@swim-io/utils"; import * as byteify from "byteify"; import keccak256 from "keccak256"; import type { SolanaChainConfig } from "./protocol"; +import type { SupportedTokenProjectId } from "./supportedTokenProjectIds"; +import { SUPPORTED_TOKEN_PROJECT_IDS } from "./supportedTokenProjectIds"; + +const getUserTokenAccounts = ( + walletPublicKey: PublicKey, + solanaChainConfig: SolanaChainConfig, +) => + SUPPORTED_TOKEN_PROJECT_IDS.reduce((accumulator, tokenProjectId) => { + const { address } = getTokenDetails(solanaChainConfig, tokenProjectId); + return { + ...accumulator, + [tokenProjectId]: getAssociatedTokenAddressSync( + new PublicKey(address), + walletPublicKey, + ), + }; + }, {} as ReadonlyRecord); export const getAddAccounts = ( solanaChainConfig: SolanaChainConfig, - userSwimUsdAtaPublicKey: PublicKey, - userTokenAccounts: readonly PublicKey[], + walletPublicKey: PublicKey, auxiliarySigner: PublicKey, lpMint: PublicKey, poolTokenAccounts: readonly PublicKey[], poolGovernanceFeeAccount: PublicKey, ): Accounts => { + const userTokenAccounts = getUserTokenAccounts( + walletPublicKey, + solanaChainConfig, + ); return { propeller: new PublicKey(solanaChainConfig.routingContractStateAddress), tokenProgram: TOKEN_PROGRAM_ID, @@ -34,9 +61,9 @@ export const getAddAccounts = ( lpMint, governanceFee: poolGovernanceFeeAccount, userTransferAuthority: auxiliarySigner, - userTokenAccount0: userTokenAccounts[0], - userTokenAccount1: userTokenAccounts[1], - userLpTokenAccount: userSwimUsdAtaPublicKey, + userTokenAccount0: userTokenAccounts[TokenProjectId.Usdc], + userTokenAccount1: userTokenAccounts[TokenProjectId.Usdt], + userLpTokenAccount: userTokenAccounts[TokenProjectId.SwimUsd], twoPoolProgram: new PublicKey(solanaChainConfig.twoPoolContractAddress), }; }; @@ -114,11 +141,10 @@ const hashVaa = (signedVaa: Buffer): Buffer => { return keccak256(Buffer.from(body)); }; -export const createCompleteNativeWithPayloadAccounts = async ( +export const getCompleteNativeWithPayloadAccounts = async ( solanaChainConfig: SolanaChainConfig, walletPublicKey: PublicKey, signedVaa: Buffer, - swimUsdAtaPublicKey: PublicKey, sourceWormholeChainId: number, sourceChainConfig: ChainConfig, ): Promise => { @@ -127,6 +153,10 @@ export const createCompleteNativeWithPayloadAccounts = async ( const swimUsdMintPublicKey = new PublicKey( solanaChainConfig.swimUsdDetails.address, ); + const swimUsdAtaPublicKey = await getAssociatedTokenAddress( + swimUsdMintPublicKey, + walletPublicKey, + ); const [tokenBridgeConfig] = await PublicKey.findProgramAddress( [Buffer.from("config")], portalPublicKey, @@ -161,12 +191,10 @@ export const createCompleteNativeWithPayloadAccounts = async ( portalPublicKey, ); - const propellerRedeemer = ( - await PublicKey.findProgramAddress( - [Buffer.from("redeemer")], - new PublicKey(solanaChainConfig.routingContractAddress), - ) - )[0]; + const [propellerRedeemer] = await PublicKey.findProgramAddress( + [Buffer.from("redeemer")], + new PublicKey(solanaChainConfig.routingContractAddress), + ); const propellerRedeemerEscrowAccount = await getAssociatedTokenAddress( swimUsdMintPublicKey, propellerRedeemer, @@ -208,7 +236,7 @@ const getSwimPayloadMessagePda = async ( ); }; -const getToTokenNumberMapAddr = async ( +const getToTokenNumberMapPda = async ( propellerState: PublicKey, toTokenNumber: number, propellerProgramId: PublicKey, @@ -224,12 +252,10 @@ const getToTokenNumberMapAddr = async ( ); }; -export const createProcessSwimPayloadAccounts = async ( +export const getProcessSwimPayloadAccounts = async ( solanaChainConfig: SolanaChainConfig, walletPublicKey: PublicKey, signedVaa: Buffer, - swimUsdAtaPublicKey: PublicKey, - userTokenAccounts: readonly PublicKey[], poolTokenAccounts: readonly PublicKey[], governanceFeeKey: PublicKey, toTokenNumber: number, @@ -252,12 +278,10 @@ export const createProcessSwimPayloadAccounts = async ( claim, propellerProgramId, ); - const propellerRedeemer = ( - await PublicKey.findProgramAddress( - [Buffer.from("redeemer")], - propellerProgramId, - ) - )[0]; + const [propellerRedeemer] = await PublicKey.findProgramAddress( + [Buffer.from("redeemer")], + propellerProgramId, + ); const propellerRedeemerEscrowAccount = await getAssociatedTokenAddress( swimUsdMintPublicKey, propellerRedeemer, @@ -266,11 +290,15 @@ export const createProcessSwimPayloadAccounts = async ( const twoPoolConfig = solanaChainConfig.pools[0]; const twoPoolProgramId = new PublicKey(twoPoolConfig.contract); const twoPoolAddress = new PublicKey(twoPoolConfig.address); - const [tokenIdMap] = await getToTokenNumberMapAddr( + const [tokenIdMap] = await getToTokenNumberMapPda( propeller, toTokenNumber, propellerProgramId, ); + const userTokenAccounts = getUserTokenAccounts( + walletPublicKey, + solanaChainConfig, + ); return { propeller, payer: walletPublicKey, @@ -285,9 +313,9 @@ export const createProcessSwimPayloadAccounts = async ( lpMint: swimUsdMintPublicKey, governanceFee: governanceFeeKey, userTransferAuthority: walletPublicKey, - userTokenAccount0: userTokenAccounts[0], - userTokenAccount1: userTokenAccounts[1], - userLpTokenAccount: swimUsdAtaPublicKey, + userTokenAccount0: userTokenAccounts[TokenProjectId.Usdc], + userTokenAccount1: userTokenAccounts[TokenProjectId.Usdt], + userLpTokenAccount: userTokenAccounts[TokenProjectId.SwimUsd], tokenProgram: TOKEN_PROGRAM_ID, twoPoolProgram: twoPoolProgramId, systemProgram: SystemProgram.programId, diff --git a/packages/solana/src/supportedTokenProjectIds.ts b/packages/solana/src/supportedTokenProjectIds.ts new file mode 100644 index 000000000..f20b31717 --- /dev/null +++ b/packages/solana/src/supportedTokenProjectIds.ts @@ -0,0 +1,16 @@ +import { TokenProjectId } from "@swim-io/token-projects"; + +export type SupportedTokenProjectId = + | TokenProjectId.SwimUsd + | TokenProjectId.Usdc + | TokenProjectId.Usdt; + +export const SUPPORTED_TOKEN_PROJECT_IDS = [ + TokenProjectId.SwimUsd, + TokenProjectId.Usdc, + TokenProjectId.Usdt, +]; + +export const isSupportedTokenProjectId = ( + id: TokenProjectId, +): id is SupportedTokenProjectId => SUPPORTED_TOKEN_PROJECT_IDS.includes(id); From 3e12b07d52aad61812e8f79234eb7bbf8b88b621 Mon Sep 17 00:00:00 2001 From: Chiu Date: Tue, 25 Oct 2022 23:52:46 +0800 Subject: [PATCH 4/7] chore(ui): combine to generateCompleteTransferTxs --- ...ChainEvmToSolanaSwapInteractionMutation.ts | 94 ++++---- packages/solana/src/client.ts | 206 +++++++++++------- 2 files changed, 163 insertions(+), 137 deletions(-) diff --git a/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts b/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts index a8d8c4258..f4992e168 100644 --- a/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts +++ b/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts @@ -1,4 +1,3 @@ -import type { ChainId } from "@certusone/wormhole-sdk"; import { getEmitterAddressEth, parseSequenceFromLogEth, @@ -209,14 +208,14 @@ export const useCrossChainEvmToSolanaSwapInteractionMutation = () => { ); if (interactionState.postVaaOnSolanaTxId === null) { const auxiliarySigner = Keypair.generate(); - const postVaaTxIdsGenerator = + const postVaaTxGenerator = solanaClient.generateCompleteWormholeMessageTxs({ interactionId: interaction.id, vaa: signedVaa, wallet: toWallet, auxiliarySigner, }); - for await (const result of postVaaTxIdsGenerator) { + for await (const result of postVaaTxGenerator) { updateInteractionState(interaction.id, (draft) => { if ( draft.interactionType !== InteractionType.SwapV2 || @@ -240,59 +239,44 @@ export const useCrossChainEvmToSolanaSwapInteractionMutation = () => { }); } } - if (interactionState.completeNativeWithPayloadTxId === null) { - const sourceWormholeChainId = EVM_ECOSYSTEMS[fromEcosystem] - .wormholeChainId as ChainId; - const completeNativeWithPayloadGenerator = - solanaClient.generateCompleteNativeWithPayloadTx({ - wallet: toWallet, - interactionId: interaction.id, - sourceChainConfig: fromChainConfig, - sourceWormholeChainId, - signedVaa: Buffer.from(signedVaa), - }); - - for await (const result of completeNativeWithPayloadGenerator) { - updateInteractionState(interaction.id, (draft) => { - if ( - draft.interactionType !== InteractionType.SwapV2 || - draft.swapType !== SwapType.CrossChainEvmToSolana - ) { - throw new Error("Interaction type mismatch"); - } - draft.completeNativeWithPayloadTxId = result.tx.id; - }); - } + const tokenProject = TOKEN_PROJECTS_BY_ID[toTokenSpec.projectId]; + if (tokenProject.tokenNumber === null) { + throw new Error(`Token number for ${tokenProject.symbol} not found`); } - if (interactionState.processSwimPayloadTxId === null) { - const tokenProject = TOKEN_PROJECTS_BY_ID[toTokenSpec.projectId]; - if (tokenProject.tokenNumber === null) { - throw new Error(`Token number for ${tokenProject.symbol} not found`); - } - const minimumOutputAmount = humanDecimalToAtomicString( - toTokenData.value, - toTokenData.tokenConfig, - toTokenData.ecosystemId, - ); - const processSwimPayloadGenerator = - solanaClient.generateProcessSwimPayloadTx({ - wallet: toWallet, - interactionId: interaction.id, - signedVaa: Buffer.from(signedVaa), - targetTokenNumber: tokenProject.tokenNumber, - minimumOutputAmount, - }); - for await (const result of processSwimPayloadGenerator) { - updateInteractionState(interaction.id, (draft) => { - if ( - draft.interactionType !== InteractionType.SwapV2 || - draft.swapType !== SwapType.CrossChainEvmToSolana - ) { - throw new Error("Interaction type mismatch"); - } - draft.processSwimPayloadTxId = result.tx.id; - }); - } + const minimumOutputAmount = humanDecimalToAtomicString( + toTokenData.value, + toTokenData.tokenConfig, + toTokenData.ecosystemId, + ); + const completeTransferTxGenerator = + solanaClient.generateCompleteTransferTxs({ + wallet: toWallet, + interactionId: interaction.id, + signedVaa: Buffer.from(signedVaa), + sourceChainConfig: fromChainConfig, + sourceWormholeChainId: ecosystems[fromEcosystem].wormholeChainId, + targetTokenNumber: tokenProject.tokenNumber, + minimumOutputAmount, + }); + for await (const result of completeTransferTxGenerator) { + updateInteractionState(interaction.id, (draft) => { + if ( + draft.interactionType !== InteractionType.SwapV2 || + draft.swapType !== SwapType.CrossChainEvmToSolana + ) { + throw new Error("Interaction type mismatch"); + } + switch (result.type) { + case SolanaTxType.SwimCompleteNativeWithPayload: + draft.completeNativeWithPayloadTxId = result.tx.id; + break; + case SolanaTxType.SwimProcessSwimPayload: + draft.processSwimPayloadTxId = result.tx.id; + break; + default: + throw new Error(`Unexpected transaction type: ${result.tx.id}`); + } + }); } }, ); diff --git a/packages/solana/src/client.ts b/packages/solana/src/client.ts index 73c6138a5..cc6ca049f 100644 --- a/packages/solana/src/client.ts +++ b/packages/solana/src/client.ts @@ -1,5 +1,8 @@ import type { ChainId } from "@certusone/wormhole-sdk"; -import { createVerifySignaturesInstructionsSolana } from "@certusone/wormhole-sdk"; +import { + createVerifySignaturesInstructionsSolana, + getIsTransferCompletedSolana, +} from "@certusone/wormhole-sdk"; import { AnchorProvider, Program } from "@project-serum/anchor"; import { createMemoInstruction } from "@solana/spl-memo"; import { @@ -40,9 +43,9 @@ import BN from "bn.js"; import Decimal from "decimal.js"; import { - getProcessSwimPayloadAccounts, getAddAccounts, getCompleteNativeWithPayloadAccounts, + getProcessSwimPayloadAccounts, getPropellerTransferAccounts, } from "./getAccounts"; import type { @@ -359,109 +362,49 @@ export class SolanaClient extends Client< }; } - public async *generateCompleteNativeWithPayloadTx({ + public async *generateCompleteTransferTxs({ wallet, interactionId, + signedVaa, sourceWormholeChainId, sourceChainConfig, - signedVaa, + targetTokenNumber, + minimumOutputAmount, }: { readonly wallet: SolanaWalletAdapter; readonly interactionId: string; + readonly signedVaa: Buffer; readonly sourceWormholeChainId: ChainId; readonly sourceChainConfig: ChainConfig; - readonly signedVaa: Buffer; + readonly targetTokenNumber: number; + readonly minimumOutputAmount: string; }): AsyncGenerator< TxGeneratorResult, any, unknown > { - const walletPublicKey = wallet.publicKey; - if (walletPublicKey === null) { - throw new Error("Missing Solana wallet public key"); - } - const routingContract = this.getRoutingContract(wallet); - const accounts = await getCompleteNativeWithPayloadAccounts( - this.chainConfig, - walletPublicKey, + const completeNativeWithPayloadTx = await this.completeNativeWithPayload({ + wallet, + interactionId, signedVaa, sourceWormholeChainId, sourceChainConfig, - ); - const setComputeUnitLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ - units: 900_000, }); - const txRequest = await routingContract.methods - .completeNativeWithPayload() - .accounts(accounts) - .preInstructions([setComputeUnitLimitIx]) - .postInstructions([createMemoInstruction(interactionId)]) - .transaction(); - // eslint-disable-next-line functional/immutable-data - txRequest.feePayer = walletPublicKey; - const txId = await this.sendAndConfirmTx( - (tx) => wallet.signTransaction(tx), - txRequest, - ); - const tx = await this.getTx(txId); - yield { - tx, - type: SolanaTxType.SwimCompleteNativeWithPayload, - }; - } - - public async *generateProcessSwimPayloadTx({ - wallet, - interactionId, - signedVaa, - targetTokenNumber, - minimumOutputAmount, - }: { - readonly wallet: SolanaWalletAdapter; - readonly interactionId: string; - readonly signedVaa: Buffer; - readonly targetTokenNumber: number; - readonly minimumOutputAmount: string; - }): AsyncGenerator< - TxGeneratorResult, - any, - unknown - > { - const [twoPoolConfig] = this.chainConfig.pools; - const walletPublicKey = wallet.publicKey; - if (walletPublicKey === null) { - throw new Error("Missing Solana wallet public key"); + if (completeNativeWithPayloadTx !== null) { + yield { + tx: completeNativeWithPayloadTx, + type: SolanaTxType.SwimCompleteNativeWithPayload, + }; } - const routingContract = this.getRoutingContract(wallet); - const poolTokenAccountPublicKeys = [ - ...twoPoolConfig.tokenAccounts.values(), - ].map((address) => new PublicKey(address)); - const accounts = await getProcessSwimPayloadAccounts( - this.chainConfig, - new PublicKey(walletPublicKey), + const processSwimPayloadTx = await this.processSwimPayloadTx({ + wallet, + interactionId, signedVaa, - poolTokenAccountPublicKeys, - new PublicKey(twoPoolConfig.governanceFeeAccount), targetTokenNumber, - ); - const setComputeUnitLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ - units: 900_000, + minimumOutputAmount, }); - const txRequest = await routingContract.methods - .processSwimPayload(targetTokenNumber, new BN(minimumOutputAmount)) - .accounts(accounts) - .preInstructions([setComputeUnitLimitIx]) - .postInstructions([createMemoInstruction(interactionId)]) - .transaction(); - // eslint-disable-next-line functional/immutable-data - txRequest.feePayer = walletPublicKey; - const txId = await this.sendAndConfirmTx( - (tx) => wallet.signTransaction(tx), - txRequest, - ); - const tx = await this.getTx(txId); yield { - tx, + tx: processSwimPayloadTx, type: SolanaTxType.SwimProcessSwimPayload, }; } @@ -1021,4 +964,103 @@ export class SolanaClient extends Client< }, txRequest); return await this.getTx(txId); } + + private async completeNativeWithPayload({ + wallet, + interactionId, + sourceWormholeChainId, + sourceChainConfig, + signedVaa, + }: { + readonly wallet: SolanaWalletAdapter; + readonly interactionId: string; + readonly sourceWormholeChainId: ChainId; + readonly sourceChainConfig: ChainConfig; + readonly signedVaa: Buffer; + }): Promise { + const walletPublicKey = wallet.publicKey; + if (walletPublicKey === null) { + throw new Error("Missing Solana wallet public key"); + } + const routingContract = this.getRoutingContract(wallet); + const accounts = await getCompleteNativeWithPayloadAccounts( + this.chainConfig, + walletPublicKey, + signedVaa, + sourceWormholeChainId, + sourceChainConfig, + ); + const completed = await getIsTransferCompletedSolana( + this.chainConfig.wormhole.portal, + signedVaa, + this.connection, + ); + if (completed) { + return null; + } + const setComputeUnitLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ + units: 900_000, + }); + const txRequest = await routingContract.methods + .completeNativeWithPayload() + .accounts(accounts) + .preInstructions([setComputeUnitLimitIx]) + .postInstructions([createMemoInstruction(interactionId)]) + .transaction(); + // eslint-disable-next-line functional/immutable-data + txRequest.feePayer = walletPublicKey; + const txId = await this.sendAndConfirmTx( + (tx) => wallet.signTransaction(tx), + txRequest, + ); + return await this.getTx(txId); + } + + private async processSwimPayloadTx({ + wallet, + interactionId, + signedVaa, + targetTokenNumber, + minimumOutputAmount, + }: { + readonly wallet: SolanaWalletAdapter; + readonly interactionId: string; + readonly signedVaa: Buffer; + readonly targetTokenNumber: number; + readonly minimumOutputAmount: string; + }): Promise { + const [twoPoolConfig] = this.chainConfig.pools; + const walletPublicKey = wallet.publicKey; + if (walletPublicKey === null) { + throw new Error("Missing Solana wallet public key"); + } + const routingContract = this.getRoutingContract(wallet); + const poolTokenAccountPublicKeys = [ + ...twoPoolConfig.tokenAccounts.values(), + ].map((address) => new PublicKey(address)); + const accounts = await getProcessSwimPayloadAccounts( + this.chainConfig, + new PublicKey(walletPublicKey), + signedVaa, + poolTokenAccountPublicKeys, + new PublicKey(twoPoolConfig.governanceFeeAccount), + targetTokenNumber, + ); + const setComputeUnitLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ + units: 900_000, + }); + const txRequest = await routingContract.methods + .processSwimPayload(targetTokenNumber, new BN(minimumOutputAmount)) + .accounts(accounts) + .preInstructions([setComputeUnitLimitIx]) + .postInstructions([createMemoInstruction(interactionId)]) + .transaction(); + // eslint-disable-next-line functional/immutable-data + txRequest.feePayer = walletPublicKey; + const txId = await this.sendAndConfirmTx( + (tx) => wallet.signTransaction(tx), + txRequest, + ); + return await this.getTx(txId); + } } From 2b137da1fb6a4fc2203f401575b7fbce37c8d11a Mon Sep 17 00:00:00 2001 From: Chiu Date: Wed, 26 Oct 2022 13:23:50 +0800 Subject: [PATCH 5/7] chore(ui): address comment, fix nit --- .../buildEuiStepsForInteraction.tsx | 4 +- .../src/fixtures/swim/interactionStateV2.ts | 5 +- .../useCreateInteractionStateV2.test.ts.snap | 2 +- .../useCreateInteractionStateV2.ts | 2 +- ...ChainEvmToSolanaSwapInteractionMutation.ts | 72 +++++++++---------- apps/ui/src/models/swim/interactionStateV2.ts | 2 +- packages/solana/src/client.ts | 22 +++--- 7 files changed, 56 insertions(+), 53 deletions(-) diff --git a/apps/ui/src/components/molecules/InteractionStateComponentV2/buildEuiStepsForInteraction.tsx b/apps/ui/src/components/molecules/InteractionStateComponentV2/buildEuiStepsForInteraction.tsx index 6f4daaa90..1b843d284 100644 --- a/apps/ui/src/components/molecules/InteractionStateComponentV2/buildEuiStepsForInteraction.tsx +++ b/apps/ui/src/components/molecules/InteractionStateComponentV2/buildEuiStepsForInteraction.tsx @@ -314,7 +314,7 @@ const buildClaimTokenOnSolanaStep = ( interactionStatus: InteractionStatusV2, ): EuiStepProps => { const { - verifySignaturesTxId, + verifySignaturesTxIds, postVaaOnSolanaTxId, completeNativeWithPayloadTxId, processSwimPayloadTxId, @@ -338,7 +338,7 @@ const buildClaimTokenOnSolanaStep = ( isLoading={status === "loading"} tokenConfig={toTokenData.tokenConfig} transactions={[ - verifySignaturesTxId, + ...verifySignaturesTxIds, postVaaOnSolanaTxId, completeNativeWithPayloadTxId, processSwimPayloadTxId, diff --git a/apps/ui/src/fixtures/swim/interactionStateV2.ts b/apps/ui/src/fixtures/swim/interactionStateV2.ts index ff8f91011..4f607f6b0 100644 --- a/apps/ui/src/fixtures/swim/interactionStateV2.ts +++ b/apps/ui/src/fixtures/swim/interactionStateV2.ts @@ -371,7 +371,7 @@ export const CROSS_CHAIN_EVM_TO_SOLANA_SWAP_INTERACTION_STATE_INIT: CrossChainEv approvalTxIds: [], crossChainInitiateTxId: null, auxiliarySignerPublicKey: null, - verifySignaturesTxId: null, + verifySignaturesTxIds: [], postVaaOnSolanaTxId: null, completeNativeWithPayloadTxId: null, processSwimPayloadTxId: null, @@ -399,8 +399,9 @@ export const CROSS_CHAIN_EVM_TO_SOLANA_SWAP_INTERACTION_STATE_SWAP_AND_TRANSFER_ export const CROSS_CHAIN_EVM_TO_SOLANA_SWAP_INTERACTION_STATE_POST_VAA_COMPLETED: CrossChainEvmToSolanaSwapInteractionState = { ...CROSS_CHAIN_EVM_TO_SOLANA_SWAP_INTERACTION_STATE_SWAP_AND_TRANSFER_COMPLETED, - verifySignaturesTxId: + verifySignaturesTxIds: [ "53r98E5EiffkmJ6WVA2VKmq78LVCT4zcRVxo76EWoUFiNpdxbno7UVeUT6oQgsVM3xeU99mQmnUjFVscz7PC1gK8", + ], postVaaOnSolanaTxId: "53r98E5EiffkmJ6WVA2VKmq78LVCT4zcRVxo76EWoUFiNpdxbno7UVeUT6oQgsVM3xeU99mQmnUjFVscz7PC1gK9", }; diff --git a/apps/ui/src/hooks/interaction/__snapshots__/useCreateInteractionStateV2.test.ts.snap b/apps/ui/src/hooks/interaction/__snapshots__/useCreateInteractionStateV2.test.ts.snap index 6e20d8dcc..67dd95bf9 100644 --- a/apps/ui/src/hooks/interaction/__snapshots__/useCreateInteractionStateV2.test.ts.snap +++ b/apps/ui/src/hooks/interaction/__snapshots__/useCreateInteractionStateV2.test.ts.snap @@ -433,7 +433,7 @@ Object { }, }, "swapType": "CrossChainEvmToSolana", - "verifySignaturesTxId": null, + "verifySignaturesTxIds": [], "version": 2, } `; diff --git a/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.ts b/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.ts index 12925ecfa..e74ee9c83 100644 --- a/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.ts +++ b/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.ts @@ -213,7 +213,7 @@ const createSwapInteractionState = ( approvalTxIds: [], crossChainInitiateTxId: null, auxiliarySignerPublicKey: null, - verifySignaturesTxId: null, + verifySignaturesTxIds: [], postVaaOnSolanaTxId: null, completeNativeWithPayloadTxId: null, processSwimPayloadTxId: null, diff --git a/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts b/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts index f4992e168..63825f4e9 100644 --- a/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts +++ b/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts @@ -98,40 +98,6 @@ export const useCrossChainEvmToSolanaSwapInteractionMutation = () => { fromChainConfig.routingContractAddress, evmClient.provider, ); - const splTokenAccounts = await Promise.all( - Object.keys(requiredSplTokenAccounts).map(async (mint) => { - const { tokenAccount, creationTxId } = - await findOrCreateSplTokenAccount({ - env: interaction.env, - solanaClient, - wallet: toWallet, - queryClient, - splTokenMintAddress: mint, - splTokenAccounts: existingSplTokenAccounts, - }); - // Update interactionState - if (creationTxId !== null) { - updateInteractionState(interaction.id, (draft) => { - if ( - draft.interactionType !== InteractionType.SwapV2 || - draft.swapType !== SwapType.SingleChainSolana - ) { - throw new Error("Interaction type mismatch"); - } - draft.requiredSplTokenAccounts[mint].txId = creationTxId; - }); - } - return tokenAccount; - }), - ); - const swimUsdAccount = findTokenAccountForMint( - swimUsd.nativeDetails.address, - toWallet.address, - splTokenAccounts, - ); - if (swimUsdAccount === null) { - throw new Error("SwimUsd account not found"); - } const memo = Buffer.from(interaction.id, "hex"); let crossChainInitiateTxId = interactionState.crossChainInitiateTxId; if (crossChainInitiateTxId === null) { @@ -206,6 +172,40 @@ export const useCrossChainEvmToSolanaSwapInteractionMutation = () => { undefined, retries, ); + const splTokenAccounts = await Promise.all( + Object.keys(requiredSplTokenAccounts).map(async (mint) => { + const { tokenAccount, creationTxId } = + await findOrCreateSplTokenAccount({ + env: interaction.env, + solanaClient, + wallet: toWallet, + queryClient, + splTokenMintAddress: mint, + splTokenAccounts: existingSplTokenAccounts, + }); + // Update interactionState + if (creationTxId !== null) { + updateInteractionState(interaction.id, (draft) => { + if ( + draft.interactionType !== InteractionType.SwapV2 || + draft.swapType !== SwapType.SingleChainSolana + ) { + throw new Error("Interaction type mismatch"); + } + draft.requiredSplTokenAccounts[mint].txId = creationTxId; + }); + } + return tokenAccount; + }), + ); + const swimUsdAccount = findTokenAccountForMint( + swimUsd.nativeDetails.address, + toWallet.address, + splTokenAccounts, + ); + if (swimUsdAccount === null) { + throw new Error("SwimUsd account not found"); + } if (interactionState.postVaaOnSolanaTxId === null) { const auxiliarySigner = Keypair.generate(); const postVaaTxGenerator = @@ -226,7 +226,7 @@ export const useCrossChainEvmToSolanaSwapInteractionMutation = () => { switch (result.type) { case SolanaTxType.WormholeVerifySignatures: - draft.verifySignaturesTxId = result.tx.id; + draft.verifySignaturesTxIds.push(result.tx.id); break; case SolanaTxType.WormholePostVaa: draft.postVaaOnSolanaTxId = result.tx.id; @@ -249,7 +249,7 @@ export const useCrossChainEvmToSolanaSwapInteractionMutation = () => { toTokenData.ecosystemId, ); const completeTransferTxGenerator = - solanaClient.generateCompleteTransferTxs({ + solanaClient.generateCompleteSwimSwapTxs({ wallet: toWallet, interactionId: interaction.id, signedVaa: Buffer.from(signedVaa), diff --git a/apps/ui/src/models/swim/interactionStateV2.ts b/apps/ui/src/models/swim/interactionStateV2.ts index ba019298d..3913c9ec5 100644 --- a/apps/ui/src/models/swim/interactionStateV2.ts +++ b/apps/ui/src/models/swim/interactionStateV2.ts @@ -71,7 +71,7 @@ export interface CrossChainEvmToSolanaSwapInteractionState { readonly approvalTxIds: readonly EvmTx["id"][]; readonly crossChainInitiateTxId: EvmTx["id"] | null; readonly auxiliarySignerPublicKey: string | null; - readonly verifySignaturesTxId: SolanaTx["id"] | null; + readonly verifySignaturesTxIds: SolanaTx["id"][]; readonly postVaaOnSolanaTxId: SolanaTx["id"] | null; readonly completeNativeWithPayloadTxId: SolanaTx["id"] | null; readonly processSwimPayloadTxId: SolanaTx["id"] | null; diff --git a/packages/solana/src/client.ts b/packages/solana/src/client.ts index cc6ca049f..b471c9ca3 100644 --- a/packages/solana/src/client.ts +++ b/packages/solana/src/client.ts @@ -362,7 +362,7 @@ export class SolanaClient extends Client< }; } - public async *generateCompleteTransferTxs({ + public async *generateCompleteSwimSwapTxs({ wallet, interactionId, signedVaa, @@ -909,6 +909,8 @@ export class SolanaClient extends Client< .postInstructions([revokeIx, memoIx]) .signers([auxiliarySigner]) .transaction(); + // eslint-disable-next-line functional/immutable-data + txRequest.feePayer = walletPublicKey; const txId = await this.sendAndConfirmTx(async (tx) => { tx.partialSign(auxiliarySigner); return wallet.signTransaction(tx); @@ -978,6 +980,14 @@ export class SolanaClient extends Client< readonly sourceChainConfig: ChainConfig; readonly signedVaa: Buffer; }): Promise { + const completed = await getIsTransferCompletedSolana( + this.chainConfig.wormhole.portal, + signedVaa, + this.connection, + ); + if (completed) { + return null; + } const walletPublicKey = wallet.publicKey; if (walletPublicKey === null) { throw new Error("Missing Solana wallet public key"); @@ -990,14 +1000,6 @@ export class SolanaClient extends Client< sourceWormholeChainId, sourceChainConfig, ); - const completed = await getIsTransferCompletedSolana( - this.chainConfig.wormhole.portal, - signedVaa, - this.connection, - ); - if (completed) { - return null; - } const setComputeUnitLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 900_000, }); @@ -1040,7 +1042,7 @@ export class SolanaClient extends Client< ].map((address) => new PublicKey(address)); const accounts = await getProcessSwimPayloadAccounts( this.chainConfig, - new PublicKey(walletPublicKey), + walletPublicKey, signedVaa, poolTokenAccountPublicKeys, new PublicKey(twoPoolConfig.governanceFeeAccount), From 75a19f3538cb9bef5044b342b72a131e480587b5 Mon Sep 17 00:00:00 2001 From: Chiu Date: Wed, 26 Oct 2022 17:32:16 +0800 Subject: [PATCH 6/7] chore(ui): move postVaa to SDK --- ...ChainEvmToSolanaSwapInteractionMutation.ts | 102 ++++------------- apps/ui/src/models/swim/interactionStateV2.ts | 2 +- packages/solana/src/client.ts | 105 ++++++++++++++---- packages/solana/src/protocol.ts | 1 + 4 files changed, 105 insertions(+), 105 deletions(-) diff --git a/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts b/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts index 63825f4e9..263a06d31 100644 --- a/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts +++ b/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts @@ -6,13 +6,9 @@ import { Keypair } from "@solana/web3.js"; import { getTokenDetails } from "@swim-io/core"; import { EVM_ECOSYSTEMS, isEvmEcosystemId } from "@swim-io/evm"; import { Routing__factory } from "@swim-io/evm-contracts"; -import { - SOLANA_ECOSYSTEM_ID, - SolanaTxType, - findTokenAccountForMint, -} from "@swim-io/solana"; +import { SOLANA_ECOSYSTEM_ID, SolanaTxType } from "@swim-io/solana"; import { TOKEN_PROJECTS_BY_ID } from "@swim-io/token-projects"; -import { useMutation, useQueryClient } from "react-query"; +import { useMutation } from "react-query"; import shallow from "zustand/shallow.js"; import { getWormholeRetries } from "../../config"; @@ -22,19 +18,15 @@ import type { CrossChainEvmToSolanaSwapInteractionState } from "../../models"; import { InteractionType, SwapType, - findOrCreateSplTokenAccount, getSignedVaaWithRetry, humanDecimalToAtomicString, } from "../../models"; import { useWallets } from "../crossEcosystem"; import { useGetEvmClient } from "../evm"; -import { useSolanaClient, useUserSolanaTokenAccountsQuery } from "../solana"; +import { useSolanaClient } from "../solana"; import { useSwimUsd } from "../swim"; export const useCrossChainEvmToSolanaSwapInteractionMutation = () => { - const queryClient = useQueryClient(); - const { data: existingSplTokenAccounts = [] } = - useUserSolanaTokenAccountsQuery(); const { updateInteractionState } = useInteractionStateV2(); const wallets = useWallets(); const solanaClient = useSolanaClient(); @@ -52,7 +44,7 @@ export const useCrossChainEvmToSolanaSwapInteractionMutation = () => { if (wormhole === null) { throw new Error("No Wormhole RPC configured"); } - const { interaction, requiredSplTokenAccounts } = interactionState; + const { interaction } = interactionState; const { fromTokenData, toTokenData, firstMinimumOutputAmount } = interaction.params; if (firstMinimumOutputAmount === null) { @@ -172,73 +164,7 @@ export const useCrossChainEvmToSolanaSwapInteractionMutation = () => { undefined, retries, ); - const splTokenAccounts = await Promise.all( - Object.keys(requiredSplTokenAccounts).map(async (mint) => { - const { tokenAccount, creationTxId } = - await findOrCreateSplTokenAccount({ - env: interaction.env, - solanaClient, - wallet: toWallet, - queryClient, - splTokenMintAddress: mint, - splTokenAccounts: existingSplTokenAccounts, - }); - // Update interactionState - if (creationTxId !== null) { - updateInteractionState(interaction.id, (draft) => { - if ( - draft.interactionType !== InteractionType.SwapV2 || - draft.swapType !== SwapType.SingleChainSolana - ) { - throw new Error("Interaction type mismatch"); - } - draft.requiredSplTokenAccounts[mint].txId = creationTxId; - }); - } - return tokenAccount; - }), - ); - const swimUsdAccount = findTokenAccountForMint( - swimUsd.nativeDetails.address, - toWallet.address, - splTokenAccounts, - ); - if (swimUsdAccount === null) { - throw new Error("SwimUsd account not found"); - } - if (interactionState.postVaaOnSolanaTxId === null) { - const auxiliarySigner = Keypair.generate(); - const postVaaTxGenerator = - solanaClient.generateCompleteWormholeMessageTxs({ - interactionId: interaction.id, - vaa: signedVaa, - wallet: toWallet, - auxiliarySigner, - }); - for await (const result of postVaaTxGenerator) { - updateInteractionState(interaction.id, (draft) => { - if ( - draft.interactionType !== InteractionType.SwapV2 || - draft.swapType !== SwapType.CrossChainEvmToSolana - ) { - throw new Error("Interaction type mismatch"); - } - - switch (result.type) { - case SolanaTxType.WormholeVerifySignatures: - draft.verifySignaturesTxIds.push(result.tx.id); - break; - case SolanaTxType.WormholePostVaa: - draft.postVaaOnSolanaTxId = result.tx.id; - draft.auxiliarySignerPublicKey = - auxiliarySigner.publicKey.toBase58(); - break; - default: - throw new Error(`Unexpected transaction type: ${result.tx.id}`); - } - }); - } - } + const auxiliarySigner = Keypair.generate(); const tokenProject = TOKEN_PROJECTS_BY_ID[toTokenSpec.projectId]; if (tokenProject.tokenNumber === null) { throw new Error(`Token number for ${tokenProject.symbol} not found`); @@ -267,14 +193,28 @@ export const useCrossChainEvmToSolanaSwapInteractionMutation = () => { throw new Error("Interaction type mismatch"); } switch (result.type) { + case SolanaTxType.SwimCreateSplTokenAccount: { + const mint = result.tx.original.meta?.preTokenBalances?.[0].mint; + if (!mint) { + throw new Error("Token account mint not found"); + } + draft.requiredSplTokenAccounts[mint].txId = result.tx.id; + break; + } + case SolanaTxType.WormholeVerifySignatures: + draft.verifySignaturesTxIds.push(result.tx.id); + break; + case SolanaTxType.WormholePostVaa: + draft.postVaaOnSolanaTxId = result.tx.id; + draft.auxiliarySignerPublicKey = + auxiliarySigner.publicKey.toBase58(); + break; case SolanaTxType.SwimCompleteNativeWithPayload: draft.completeNativeWithPayloadTxId = result.tx.id; break; case SolanaTxType.SwimProcessSwimPayload: draft.processSwimPayloadTxId = result.tx.id; break; - default: - throw new Error(`Unexpected transaction type: ${result.tx.id}`); } }); } diff --git a/apps/ui/src/models/swim/interactionStateV2.ts b/apps/ui/src/models/swim/interactionStateV2.ts index 3913c9ec5..3f266d8d1 100644 --- a/apps/ui/src/models/swim/interactionStateV2.ts +++ b/apps/ui/src/models/swim/interactionStateV2.ts @@ -71,7 +71,7 @@ export interface CrossChainEvmToSolanaSwapInteractionState { readonly approvalTxIds: readonly EvmTx["id"][]; readonly crossChainInitiateTxId: EvmTx["id"] | null; readonly auxiliarySignerPublicKey: string | null; - readonly verifySignaturesTxIds: SolanaTx["id"][]; + readonly verifySignaturesTxIds: readonly SolanaTx["id"][]; readonly postVaaOnSolanaTxId: SolanaTx["id"] | null; readonly completeNativeWithPayloadTxId: SolanaTx["id"] | null; readonly processSwimPayloadTxId: SolanaTx["id"] | null; diff --git a/packages/solana/src/client.ts b/packages/solana/src/client.ts index b471c9ca3..1627e04f1 100644 --- a/packages/solana/src/client.ts +++ b/packages/solana/src/client.ts @@ -57,7 +57,10 @@ import { SOLANA_ECOSYSTEM_ID, SolanaTxType } from "./protocol"; import type { TokenAccount } from "./serialization"; import { deserializeTokenAccount } from "./serialization"; import type { SupportedTokenProjectId } from "./supportedTokenProjectIds"; -import { isSupportedTokenProjectId } from "./supportedTokenProjectIds"; +import { + SUPPORTED_TOKEN_PROJECT_IDS, + isSupportedTokenProjectId, +} from "./supportedTokenProjectIds"; import { createApproveAndRevokeIxs, createTx, @@ -370,7 +373,8 @@ export class SolanaClient extends Client< sourceChainConfig, targetTokenNumber, minimumOutputAmount, - }: { + auxiliarySigner = Keypair.generate(), + }: WithOptionalAuxiliarySigner<{ readonly wallet: SolanaWalletAdapter; readonly interactionId: string; readonly signedVaa: Buffer; @@ -378,19 +382,51 @@ export class SolanaClient extends Client< readonly sourceChainConfig: ChainConfig; readonly targetTokenNumber: number; readonly minimumOutputAmount: string; - }): AsyncGenerator< - TxGeneratorResult, + }>): AsyncGenerator< + TxGeneratorResult< + ParsedTransactionWithMeta, + SolanaTx, + | SolanaTxType.SwimCreateSplTokenAccount + | SolanaTxType.WormholeVerifySignatures + | SolanaTxType.WormholePostVaa + | SolanaTxType.SwimCompleteNativeWithPayload + | SolanaTxType.SwimProcessSwimPayload + >, any, unknown > { - const completeNativeWithPayloadTx = await this.completeNativeWithPayload({ - wallet, - interactionId, + const splTokenMintAddresses = SUPPORTED_TOKEN_PROJECT_IDS.map( + (tokenProjectId) => + getTokenDetails(this.chainConfig, tokenProjectId).address, + ); + const createSplTokenAccountsGenerator = + this.generateCreateSplTokenAccountTxs(wallet, splTokenMintAddresses); + for await (const result of createSplTokenAccountsGenerator) { + yield result; + } + const isTransferCompleted = await getIsTransferCompletedSolana( + this.chainConfig.wormhole.portal, signedVaa, - sourceWormholeChainId, - sourceChainConfig, - }); - if (completeNativeWithPayloadTx !== null) { + this.connection, + ); + if (!isTransferCompleted) { + const completeWormholeMessageGenerator = + this.generateCompleteWormholeMessageTxs({ + wallet, + interactionId, + vaa: signedVaa, + auxiliarySigner, + }); + for await (const result of completeWormholeMessageGenerator) { + yield result; + } + const completeNativeWithPayloadTx = await this.completeNativeWithPayload({ + wallet, + interactionId, + signedVaa, + sourceWormholeChainId, + sourceChainConfig, + }); yield { tx: completeNativeWithPayloadTx, type: SolanaTxType.SwimCompleteNativeWithPayload, @@ -431,9 +467,6 @@ export class SolanaClient extends Client< if (senderPublicKey === null) { throw new Error("Missing Solana wallet"); } - if (!isSupportedTokenProjectId(sourceTokenId)) { - throw new Error("Invalid source token id"); - } const sourceTokenDetails = getTokenDetails(this.chainConfig, sourceTokenId); const inputAmountAtomic = humanToAtomic( @@ -582,6 +615,40 @@ export class SolanaClient extends Client< return this.sendAndConfirmTx(wallet.signTransaction.bind(wallet), tx); } + public async *generateCreateSplTokenAccountTxs( + wallet: SolanaWalletAdapter, + splTokenMintAddresses: readonly string[], + ): AsyncGenerator< + TxGeneratorResult< + ParsedTransactionWithMeta, + SolanaTx, + SolanaTxType.SwimCreateSplTokenAccount + > + > { + if (!wallet.publicKey) { + throw new Error("No Solana wallet connected"); + } + for (const splTokenMintAddress of splTokenMintAddresses) { + const existingAccount = await this.connection.getTokenAccountsByOwner( + wallet.publicKey, + { + mint: new PublicKey(splTokenMintAddress), + }, + ); + if (existingAccount.value.length === 0) { + const txId = await this.createSplTokenAccount( + wallet, + splTokenMintAddress, + ); + const tx = await this.getTx(txId); + yield { + tx, + type: SolanaTxType.SwimCreateSplTokenAccount, + }; + } + } + } + public async getTokenAccountWithRetry( mint: string, owner: string, @@ -979,15 +1046,7 @@ export class SolanaClient extends Client< readonly sourceWormholeChainId: ChainId; readonly sourceChainConfig: ChainConfig; readonly signedVaa: Buffer; - }): Promise { - const completed = await getIsTransferCompletedSolana( - this.chainConfig.wormhole.portal, - signedVaa, - this.connection, - ); - if (completed) { - return null; - } + }): Promise { const walletPublicKey = wallet.publicKey; if (walletPublicKey === null) { throw new Error("Missing Solana wallet public key"); diff --git a/packages/solana/src/protocol.ts b/packages/solana/src/protocol.ts index 84e4fa96e..0bceff795 100644 --- a/packages/solana/src/protocol.ts +++ b/packages/solana/src/protocol.ts @@ -44,6 +44,7 @@ export enum SolanaTxType { WormholePostVaa = "wormhole:postVaa", SwimPropellerAdd = "swimPropeller:add", SwimPropellerTransfer = "swimPropeller:transfer", + SwimCreateSplTokenAccount = "swim:createSplTokenAccount", SwimCompleteNativeWithPayload = "swim:completeNativeWithPayload", SwimProcessSwimPayload = "swim:processSwimPayload", } From dd428fb2ab3d5be456ba3804210d8802c0e7b7c1 Mon Sep 17 00:00:00 2001 From: Chiu Date: Wed, 26 Oct 2022 19:13:59 +0800 Subject: [PATCH 7/7] chore(ui): compare ata address --- .../buildEuiStepsForInteraction.tsx | 2 +- .../useCreateInteractionStateV2.test.ts.snap | 2 +- ...ChainEvmToSolanaSwapInteractionMutation.ts | 25 ++++++++--------- packages/solana/src/client.ts | 27 ++++++++++++------- packages/solana/src/protocol.ts | 2 +- 5 files changed, 32 insertions(+), 26 deletions(-) diff --git a/apps/ui/src/components/molecules/InteractionStateComponentV2/buildEuiStepsForInteraction.tsx b/apps/ui/src/components/molecules/InteractionStateComponentV2/buildEuiStepsForInteraction.tsx index 1b843d284..c6be64cb0 100644 --- a/apps/ui/src/components/molecules/InteractionStateComponentV2/buildEuiStepsForInteraction.tsx +++ b/apps/ui/src/components/molecules/InteractionStateComponentV2/buildEuiStepsForInteraction.tsx @@ -479,8 +479,8 @@ export const buildEuiStepsForInteraction = ( } case SwapType.CrossChainEvmToSolana: { return [ - buildPrepareSplTokenAccountStep(state, status), buildSwapAndTransferStep(state, status), + buildPrepareSplTokenAccountStep(state, status), buildClaimTokenOnSolanaStep(state, status), ].filter(isNotNull); } diff --git a/apps/ui/src/hooks/interaction/__snapshots__/useCreateInteractionStateV2.test.ts.snap b/apps/ui/src/hooks/interaction/__snapshots__/useCreateInteractionStateV2.test.ts.snap index 67dd95bf9..33d70d33b 100644 --- a/apps/ui/src/hooks/interaction/__snapshots__/useCreateInteractionStateV2.test.ts.snap +++ b/apps/ui/src/hooks/interaction/__snapshots__/useCreateInteractionStateV2.test.ts.snap @@ -433,7 +433,7 @@ Object { }, }, "swapType": "CrossChainEvmToSolana", - "verifySignaturesTxIds": [], + "verifySignaturesTxIds": Array [], "version": 2, } `; diff --git a/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts b/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts index 263a06d31..2e3d3fd96 100644 --- a/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts +++ b/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts @@ -132,23 +132,20 @@ export const useCrossChainEvmToSolanaSwapInteractionMutation = () => { await fromWallet.switchNetwork(fromChainConfig.chainId); const crossChainInitiateResponse = await fromWallet.signer.sendTransaction(crossChainInitiateRequest); - const crossChainInitiateTx = await evmClient.getTx( - crossChainInitiateResponse, - ); - crossChainInitiateTxId = crossChainInitiateTx.id; - updateInteractionState(interaction.id, (draft) => { - if ( - draft.interactionType !== InteractionType.SwapV2 || - draft.swapType !== SwapType.CrossChainEvmToSolana - ) { - throw new Error("Interaction type mismatch"); - } - draft.crossChainInitiateTxId = crossChainInitiateTx.id; - }); + crossChainInitiateTxId = crossChainInitiateResponse.hash; } const crossChainInitiateTx = await evmClient.getTx( crossChainInitiateTxId, ); + updateInteractionState(interaction.id, (draft) => { + if ( + draft.interactionType !== InteractionType.SwapV2 || + draft.swapType !== SwapType.CrossChainEvmToSolana + ) { + throw new Error("Interaction type mismatch"); + } + draft.crossChainInitiateTxId = crossChainInitiateTx.id; + }); const wormholeSequence = parseSequenceFromLogEth( crossChainInitiateTx.original, fromChainConfig.wormhole.bridge, @@ -193,7 +190,7 @@ export const useCrossChainEvmToSolanaSwapInteractionMutation = () => { throw new Error("Interaction type mismatch"); } switch (result.type) { - case SolanaTxType.SwimCreateSplTokenAccount: { + case SolanaTxType.SplTokenCreateAccount: { const mint = result.tx.original.meta?.preTokenBalances?.[0].mint; if (!mint) { throw new Error("Token account mint not found"); diff --git a/packages/solana/src/client.ts b/packages/solana/src/client.ts index 1627e04f1..7d72d4292 100644 --- a/packages/solana/src/client.ts +++ b/packages/solana/src/client.ts @@ -386,7 +386,7 @@ export class SolanaClient extends Client< TxGeneratorResult< ParsedTransactionWithMeta, SolanaTx, - | SolanaTxType.SwimCreateSplTokenAccount + | SolanaTxType.SplTokenCreateAccount | SolanaTxType.WormholeVerifySignatures | SolanaTxType.WormholePostVaa | SolanaTxType.SwimCompleteNativeWithPayload @@ -467,7 +467,9 @@ export class SolanaClient extends Client< if (senderPublicKey === null) { throw new Error("Missing Solana wallet"); } - + if (!isSupportedTokenProjectId(sourceTokenId)) { + throw new Error("Invalid source token id"); + } const sourceTokenDetails = getTokenDetails(this.chainConfig, sourceTokenId); const inputAmountAtomic = humanToAtomic( inputAmount, @@ -622,20 +624,27 @@ export class SolanaClient extends Client< TxGeneratorResult< ParsedTransactionWithMeta, SolanaTx, - SolanaTxType.SwimCreateSplTokenAccount + SolanaTxType.SplTokenCreateAccount > > { if (!wallet.publicKey) { throw new Error("No Solana wallet connected"); } for (const splTokenMintAddress of splTokenMintAddresses) { - const existingAccount = await this.connection.getTokenAccountsByOwner( + const mint = new PublicKey(splTokenMintAddress); + const expectedAtaAddress = await getAssociatedTokenAddress( + mint, wallet.publicKey, - { - mint: new PublicKey(splTokenMintAddress), - }, ); - if (existingAccount.value.length === 0) { + const { value: existingAtaAccounts } = + await this.connection.getTokenAccountsByOwner(wallet.publicKey, { + mint, + }); + const expectedAtaAccount = + existingAtaAccounts.find( + ({ pubkey }) => pubkey.toBase58() == expectedAtaAddress.toBase58(), + ) ?? null; + if (expectedAtaAccount === null) { const txId = await this.createSplTokenAccount( wallet, splTokenMintAddress, @@ -643,7 +652,7 @@ export class SolanaClient extends Client< const tx = await this.getTx(txId); yield { tx, - type: SolanaTxType.SwimCreateSplTokenAccount, + type: SolanaTxType.SplTokenCreateAccount, }; } } diff --git a/packages/solana/src/protocol.ts b/packages/solana/src/protocol.ts index 0bceff795..0ee324b14 100644 --- a/packages/solana/src/protocol.ts +++ b/packages/solana/src/protocol.ts @@ -42,9 +42,9 @@ export enum SolanaTxType { PortalRedeem = "portal:redeem", WormholeVerifySignatures = "wormhole:verifySignatures", WormholePostVaa = "wormhole:postVaa", + SplTokenCreateAccount = "splToken:createAccount", SwimPropellerAdd = "swimPropeller:add", SwimPropellerTransfer = "swimPropeller:transfer", - SwimCreateSplTokenAccount = "swim:createSplTokenAccount", SwimCompleteNativeWithPayload = "swim:completeNativeWithPayload", SwimProcessSwimPayload = "swim:processSwimPayload", }