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..fb5b5b66c 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; 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);