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..c6be64cb0 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, + verifySignaturesTxIds, + postVaaOnSolanaTxId, + completeNativeWithPayloadTxId, + processSwimPayloadTxId, interaction: { params: { toTokenData }, }, @@ -333,21 +334,16 @@ const buildClaimTokenOnSolanaStep = ( status, children: ( <> - - {!isSwimUsd(toTokenData.tokenConfig) && ( - - )} ), }; @@ -483,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/fixtures/swim/interactionStateV2.ts b/apps/ui/src/fixtures/swim/interactionStateV2.ts index 45e51589f..4f607f6b0 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, + verifySignaturesTxIds: [], + postVaaOnSolanaTxId: null, + completeNativeWithPayloadTxId: null, + processSwimPayloadTxId: null, }; export const CROSS_CHAIN_EVM_TO_SOLANA_SWAP_INTERACTION_STATE_EXISTING_SPL_TOKEN_ACCOUNTS: CrossChainEvmToSolanaSwapInteractionState = @@ -398,16 +399,17 @@ 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: [ + verifySignaturesTxIds: [ "53r98E5EiffkmJ6WVA2VKmq78LVCT4zcRVxo76EWoUFiNpdxbno7UVeUT6oQgsVM3xeU99mQmnUjFVscz7PC1gK8", - "53r98E5EiffkmJ6WVA2VKmq78LVCT4zcRVxo76EWoUFiNpdxbno7UVeUT6oQgsVM3xeU99mQmnUjFVscz7PC1gK9", ], + 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/__snapshots__/useCreateInteractionStateV2.test.ts.snap b/apps/ui/src/hooks/interaction/__snapshots__/useCreateInteractionStateV2.test.ts.snap index 523290309..33d70d33b 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", + "verifySignaturesTxIds": Array [], "version": 2, } `; diff --git a/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.ts b/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.ts index 09ad899a4..e74ee9c83 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, + 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 04dec0977..2e3d3fd96 100644 --- a/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts +++ b/apps/ui/src/hooks/interaction/useCrossChainEvmToSolanaSwapInteractionMutation.ts @@ -1,42 +1,220 @@ +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 } from "@swim-io/solana"; +import { TOKEN_PROJECTS_BY_ID } from "@swim-io/token-projects"; import { useMutation } 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, + getSignedVaaWithRetry, + humanDecimalToAtomicString, +} from "../../models"; +import { useWallets } from "../crossEcosystem"; +import { useGetEvmClient } from "../evm"; +import { useSolanaClient } from "../solana"; +import { useSwimUsd } from "../swim"; export const useCrossChainEvmToSolanaSwapInteractionMutation = () => { 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) => { + if (swimUsd === null) { + throw new Error("SwimUsd not found"); + } + if (wormhole === null) { + throw new Error("No Wormhole RPC configured"); + } 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"); + 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 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; - }); - - // TODO: Handle cross chain evm to solana swap, - + 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); + crossChainInitiateTxId = crossChainInitiateResponse.hash; + } + const crossChainInitiateTx = await evmClient.getTx( + crossChainInitiateTxId, + ); updateInteractionState(interaction.id, (draft) => { - if (draft.interactionType !== InteractionType.SwapV2) { + if ( + draft.interactionType !== InteractionType.SwapV2 || + draft.swapType !== SwapType.CrossChainEvmToSolana + ) { throw new Error("Interaction type mismatch"); } - if (draft.swapType !== SwapType.CrossChainEvmToSolana) { - throw new Error("Swap type mismatch"); - } - // TODO: update txId - // draft.postVaaOnSolanaTxIds = txIds; - // draft.claimTokenOnSolanaTxId = txId; + draft.crossChainInitiateTxId = crossChainInitiateTx.id; }); + 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, + ); + 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`); + } + const minimumOutputAmount = humanDecimalToAtomicString( + toTokenData.value, + toTokenData.tokenConfig, + toTokenData.ecosystemId, + ); + const completeTransferTxGenerator = + solanaClient.generateCompleteSwimSwapTxs({ + 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.SplTokenCreateAccount: { + 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; + } + }); + } }, ); }; 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..3f266d8d1 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 verifySignaturesTxIds: readonly SolanaTx["id"][]; + 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..7d72d4292 100644 --- a/packages/solana/src/client.ts +++ b/packages/solana/src/client.ts @@ -1,10 +1,11 @@ import type { ChainId } from "@certusone/wormhole-sdk"; -import { createVerifySignaturesInstructionsSolana } from "@certusone/wormhole-sdk"; -import type { Accounts } from "@project-serum/anchor"; +import { + createVerifySignaturesInstructionsSolana, + getIsTransferCompletedSolana, +} from "@certusone/wormhole-sdk"; import { AnchorProvider, Program } from "@project-serum/anchor"; import { createMemoInstruction } from "@solana/spl-memo"; import { - TOKEN_PROGRAM_ID, createAssociatedTokenAccountInstruction, getAssociatedTokenAddress, getAssociatedTokenAddressSync, @@ -24,9 +25,9 @@ import { Keypair, LAMPORTS_PER_SOL, PublicKey, - SYSVAR_CLOCK_PUBKEY, } from "@solana/web3.js"; import type { + ChainConfig, CompletePortalTransferParams, InitiatePortalTransferParams, InitiatePropellerParams, @@ -37,11 +38,16 @@ 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 { + getAddAccounts, + getCompleteNativeWithPayloadAccounts, + getProcessSwimPayloadAccounts, + getPropellerTransferAccounts, +} from "./getAccounts"; import type { SolanaChainConfig, SolanaEcosystemId, @@ -50,6 +56,11 @@ import type { import { SOLANA_ECOSYSTEM_ID, SolanaTxType } from "./protocol"; import type { TokenAccount } from "./serialization"; import { deserializeTokenAccount } from "./serialization"; +import type { SupportedTokenProjectId } from "./supportedTokenProjectIds"; +import { + SUPPORTED_TOKEN_PROJECT_IDS, + isSupportedTokenProjectId, +} from "./supportedTokenProjectIds"; import { createApproveAndRevokeIxs, createTx, @@ -81,21 +92,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; @@ -369,6 +365,86 @@ export class SolanaClient extends Client< }; } + public async *generateCompleteSwimSwapTxs({ + wallet, + interactionId, + signedVaa, + sourceWormholeChainId, + sourceChainConfig, + targetTokenNumber, + minimumOutputAmount, + auxiliarySigner = Keypair.generate(), + }: WithOptionalAuxiliarySigner<{ + readonly wallet: SolanaWalletAdapter; + readonly interactionId: string; + readonly signedVaa: Buffer; + readonly sourceWormholeChainId: ChainId; + readonly sourceChainConfig: ChainConfig; + readonly targetTokenNumber: number; + readonly minimumOutputAmount: string; + }>): AsyncGenerator< + TxGeneratorResult< + ParsedTransactionWithMeta, + SolanaTx, + | SolanaTxType.SplTokenCreateAccount + | SolanaTxType.WormholeVerifySignatures + | SolanaTxType.WormholePostVaa + | SolanaTxType.SwimCompleteNativeWithPayload + | SolanaTxType.SwimProcessSwimPayload + >, + any, + unknown + > { + 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, + 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, + }; + } + const processSwimPayloadTx = await this.processSwimPayloadTx({ + wallet, + interactionId, + signedVaa, + targetTokenNumber, + minimumOutputAmount, + }); + yield { + tx: processSwimPayloadTx, + type: SolanaTxType.SwimProcessSwimPayload, + }; + } + public async *generateInitiatePropellerTxs({ wallet, interactionId, @@ -394,26 +470,13 @@ export class SolanaClient extends Client< if (!isSupportedTokenProjectId(sourceTokenId)) { throw new Error("Invalid source token id"); } - const sourceTokenDetails = getTokenDetails(this.chainConfig, sourceTokenId); const inputAmountAtomic = humanToAtomic( inputAmount, 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) { @@ -554,6 +617,47 @@ 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.SplTokenCreateAccount + > + > { + if (!wallet.publicKey) { + throw new Error("No Solana wallet connected"); + } + for (const splTokenMintAddress of splTokenMintAddresses) { + const mint = new PublicKey(splTokenMintAddress); + const expectedAtaAddress = await getAssociatedTokenAddress( + mint, + wallet.publicKey, + ); + 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, + ); + const tx = await this.getTx(txId); + yield { + tx, + type: SolanaTxType.SplTokenCreateAccount, + }; + } + } + } + public async getTokenAccountWithRetry( mint: string, owner: string, @@ -806,90 +910,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({ @@ -901,32 +939,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 = this.getAddAccounts( - userTokenAccounts[TokenProjectId.SwimUsd], - [ - userTokenAccounts[TokenProjectId.Usdc], - userTokenAccounts[TokenProjectId.Usdt], - ], + const addAccounts = getAddAccounts( + this.chainConfig, + walletPublicKey, auxiliarySigner.publicKey, new PublicKey(this.chainConfig.swimUsdDetails.address), [...twoPoolConfig.tokenAccounts.values()].map( @@ -934,9 +959,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, @@ -953,6 +985,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); @@ -981,7 +1015,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, @@ -1007,4 +1042,95 @@ 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 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, + 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); + } } diff --git a/packages/solana/src/getAccounts.ts b/packages/solana/src/getAccounts.ts new file mode 100644 index 000000000..1c6cd885b --- /dev/null +++ b/packages/solana/src/getAccounts.ts @@ -0,0 +1,324 @@ +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, + 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"; +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, + 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, + poolTokenAccount0: poolTokenAccounts[0], + poolTokenAccount1: poolTokenAccounts[1], + lpMint, + governanceFee: poolGovernanceFeeAccount, + userTransferAuthority: auxiliarySigner, + userTokenAccount0: userTokenAccounts[TokenProjectId.Usdc], + userTokenAccount1: userTokenAccounts[TokenProjectId.Usdt], + userLpTokenAccount: userTokenAccounts[TokenProjectId.SwimUsd], + 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 getCompleteNativeWithPayloadAccounts = async ( + solanaChainConfig: SolanaChainConfig, + walletPublicKey: PublicKey, + signedVaa: Buffer, + 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 swimUsdAtaPublicKey = await getAssociatedTokenAddress( + swimUsdMintPublicKey, + walletPublicKey, + ); + 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), + ); + 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 getToTokenNumberMapPda = 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 getProcessSwimPayloadAccounts = async ( + solanaChainConfig: SolanaChainConfig, + walletPublicKey: PublicKey, + signedVaa: Buffer, + 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, + ); + 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 getToTokenNumberMapPda( + propeller, + toTokenNumber, + propellerProgramId, + ); + const userTokenAccounts = getUserTokenAccounts( + walletPublicKey, + solanaChainConfig, + ); + 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[TokenProjectId.Usdc], + userTokenAccount1: userTokenAccounts[TokenProjectId.Usdt], + userLpTokenAccount: userTokenAccounts[TokenProjectId.SwimUsd], + 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..0ee324b14 100644 --- a/packages/solana/src/protocol.ts +++ b/packages/solana/src/protocol.ts @@ -42,8 +42,11 @@ export enum SolanaTxType { PortalRedeem = "portal:redeem", WormholeVerifySignatures = "wormhole:verifySignatures", WormholePostVaa = "wormhole:postVaa", + SplTokenCreateAccount = "splToken:createAccount", SwimPropellerAdd = "swimPropeller:add", SwimPropellerTransfer = "swimPropeller:transfer", + SwimCompleteNativeWithPayload = "swim:completeNativeWithPayload", + SwimProcessSwimPayload = "swim:processSwimPayload", } export type SolanaTx = Tx; 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); 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:^"