From 5e88a9447b8d889b2a9d80c57e7fa4711d3bb37a Mon Sep 17 00:00:00 2001 From: Kevin Peters Date: Mon, 18 Mar 2024 12:50:54 -0500 Subject: [PATCH] solana send and receive work --- package-lock.json | 58 ++++---- wormhole-connect/package.json | 3 +- .../src/routes/cctpManual/cctpManual.ts | 30 ++-- .../src/routes/cctpManual/chains/solana.ts | 139 ++++++++++-------- wormhole-connect/src/utils/wallet/index.ts | 4 +- .../src/views/Bridge/RouteOptions.tsx | 2 +- 6 files changed, 128 insertions(+), 108 deletions(-) diff --git a/package-lock.json b/package-lock.json index c17f4c88f..2867745fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13416,36 +13416,6 @@ "@xtuc/long": "4.2.2" } }, - "node_modules/@wormhole-foundation/connect-sdk": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@wormhole-foundation/connect-sdk/-/connect-sdk-0.3.3.tgz", - "integrity": "sha512-RBWuNNFjIkIhLrfDDHsQ88Z+LkVIm5p4CoeBlII1zULC1AwoqcO1b0yUILghJOs0Tv0ZK+Z41bLjwbFC58YtZQ==", - "dependencies": { - "@wormhole-foundation/sdk-base": "^0.3.3", - "@wormhole-foundation/sdk-definitions": "^0.3.3", - "axios": "^1.4.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@wormhole-foundation/sdk-base": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@wormhole-foundation/sdk-base/-/sdk-base-0.3.3.tgz", - "integrity": "sha512-aLeWQ7Ier4rBru6rK3m6fru4WfUaSLRyY0ieH6cTKBoBMvHQejgGpgYY0wOFU9ZU5ycdzgSqUE7iHPocRCRCkw==", - "dependencies": { - "@scure/base": "^1.1.3" - } - }, - "node_modules/@wormhole-foundation/sdk-definitions": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@wormhole-foundation/sdk-definitions/-/sdk-definitions-0.3.3.tgz", - "integrity": "sha512-baIo9y46Kt6uzAUJOuYPaiy+l5hKmXm8k+YOsdUhxnpRzsZGsVmdlFOcMnr1W23e+Ua8L2MW17c9Ty+jAYgukw==", - "dependencies": { - "@noble/hashes": "^1.3.1", - "@wormhole-foundation/sdk-base": "^0.3.3" - } - }, "node_modules/@wormhole-foundation/wormhole-connect": { "resolved": "wormhole-connect", "link": true @@ -37552,7 +37522,12 @@ "@reduxjs/toolkit": "^1.9.1", "@solana/wallet-adapter-wallets": "^0.19.25", "@solana/web3.js": "^1.73.0", +<<<<<<< HEAD "@wormhole-foundation/connect-sdk": "^0.3.3", +======= + "@wormhole-foundation/sdk-definitions": "^0.5.0", + "@wormhole-foundation/wormhole-connect-sdk": "0.1.0-beta.0", +>>>>>>> c1ad6003 (solana send and receive work) "@xlabs-libs/wallet-aggregator-aptos": "^0.0.1-alpha.14", "@xlabs-libs/wallet-aggregator-core": "^0.0.1-alpha.18", "@xlabs-libs/wallet-aggregator-cosmos": "^0.0.1-alpha.14", @@ -37652,6 +37627,29 @@ "peerDependencies": { "@babel/core": "^7.0.0-0" } + }, + "wormhole-connect/node_modules/@types/node": { + "version": "16.18.68", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.68.tgz", + "integrity": "sha512-sG3hPIQwJLoewrN7cr0dwEy+yF5nD4D/4FxtQpFciRD/xwUzgD+G05uxZHv5mhfXo4F9Jkp13jjn0CC2q325sg==", + "dev": true + }, + "wormhole-connect/node_modules/@wormhole-foundation/sdk-base": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@wormhole-foundation/sdk-base/-/sdk-base-0.5.0.tgz", + "integrity": "sha512-5+Y0Wrw6eihkbUqVpXDowvD3Xs3IIF0gvLK6JQ3xsuya/LiJeP8S7YdLVRjnGE5rEAvsf46yrgxuzy5hQ3xuVQ==", + "dependencies": { + "@scure/base": "^1.1.3" + } + }, + "wormhole-connect/node_modules/@wormhole-foundation/sdk-definitions": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@wormhole-foundation/sdk-definitions/-/sdk-definitions-0.5.0.tgz", + "integrity": "sha512-Qm8quIquf9Sx7SGUVxch9+EZRlkMdz9vYFLBUru+PCqxnHoVg0yobhUHCMi+9wn4n2BWU3F8UC/zv/VDJmMrGg==", + "dependencies": { + "@noble/hashes": "^1.3.1", + "@wormhole-foundation/sdk-base": "0.5.0" + } } } } diff --git a/wormhole-connect/package.json b/wormhole-connect/package.json index 48083d6ab..36eb7323c 100644 --- a/wormhole-connect/package.json +++ b/wormhole-connect/package.json @@ -30,7 +30,8 @@ "@reduxjs/toolkit": "^1.9.1", "@solana/wallet-adapter-wallets": "^0.19.25", "@solana/web3.js": "^1.73.0", - "@wormhole-foundation/connect-sdk": "^0.3.3", + "@wormhole-foundation/sdk-definitions": "^0.5.0", + "@wormhole-foundation/wormhole-connect-sdk": "0.1.0-beta.0", "@xlabs-libs/wallet-aggregator-aptos": "^0.0.1-alpha.14", "@xlabs-libs/wallet-aggregator-core": "^0.0.1-alpha.18", "@xlabs-libs/wallet-aggregator-cosmos": "^0.0.1-alpha.14", diff --git a/wormhole-connect/src/routes/cctpManual/cctpManual.ts b/wormhole-connect/src/routes/cctpManual/cctpManual.ts index b1f87929a..87c80b9ba 100644 --- a/wormhole-connect/src/routes/cctpManual/cctpManual.ts +++ b/wormhole-connect/src/routes/cctpManual/cctpManual.ts @@ -14,11 +14,11 @@ import { getDisplayName, calculateUSDPrice, } from 'utils'; -import { isEvmChain, toChainId } from 'utils/sdk'; +import { isEvmChain, toChainId, toChainName } from 'utils/sdk'; import { TransferWallet, signAndSendTransaction } from 'utils/wallet'; import { NO_INPUT } from 'utils/style'; import { toDecimals } from '../../utils/balance'; -import { getSolanaAssociatedTokenAccount } from '../../utils/solana'; +import { getAssociatedTokenAddress } from '@solana/spl-token'; import { BaseRoute } from '../bridge/baseRoute'; import { ManualCCTPMessage, @@ -40,6 +40,8 @@ import { tryGetCircleAttestation, } from './utils'; import { TokenPrices } from 'store/tokenPrices'; +import { getNativeVersionOfToken } from 'store/transferInput'; +import { PublicKey } from '@solana/web3.js'; export class CCTPManualRoute extends BaseRoute { readonly NATIVE_GAS_DROPOFF_SUPPORTED: boolean = false; @@ -248,15 +250,21 @@ export class CCTPManualRoute extends BaseRoute { destToken: string, routeOptions: any, ): Promise { - const recipientAccount = - config.wh.toChainName(recipientChain) === 'solana' - ? await getSolanaAssociatedTokenAccount( - token, - sendingChain, - recipientAddress, - ) - : recipientAddress; - + // TODO: make sure sending to the ATA is right + let recipientAccount = recipientAddress; + if (toChainName(recipientChain) === 'solana') { + const tokenKey = getNativeVersionOfToken('USDC', 'solana'); + const tokenConfig = config.tokens[tokenKey]; + if (!tokenConfig || !tokenConfig.tokenId) { + throw new Error('Solana USDC not found'); + } + recipientAccount = ( + await getAssociatedTokenAddress( + new PublicKey(tokenConfig.tokenId.address), + new PublicKey(recipientAddress), + ) + ).toString(); + } const tx = await this.getImplementation(sendingChain).send( token, amount, diff --git a/wormhole-connect/src/routes/cctpManual/chains/solana.ts b/wormhole-connect/src/routes/cctpManual/chains/solana.ts index b55f15a41..54f9a3ac2 100644 --- a/wormhole-connect/src/routes/cctpManual/chains/solana.ts +++ b/wormhole-connect/src/routes/cctpManual/chains/solana.ts @@ -1,8 +1,9 @@ import { CHAIN_ID_SOLANA } from '@certusone/wormhole-sdk'; -import { BN, EventParser, Program, utils } from '@project-serum/anchor'; +import { BN, Program, utils } from '@project-serum/anchor'; import { TOKEN_PROGRAM_ID, getAssociatedTokenAddress } from '@solana/spl-token'; import { AccountMeta, + Keypair, PublicKey, SystemProgram, Transaction, @@ -31,26 +32,17 @@ import { import { TokenMessenger, TokenMessengerIdl } from '../idl/TokenMessenger'; import ManualCCTP from './abstract'; import { getChainNameCCTP, getDomainCCTP } from '../utils/chains'; +import { CircleBridge } from '@wormhole-foundation/sdk-definitions'; +import { hexlify } from 'ethers/lib/utils'; const CCTP_NONCE_OFFSET = 12; -const NONCES_PER_ACCOUNT = 6400; +const MAX_NONCES_PER_ACCOUNT = 6400n; interface FindProgramAddressResponse { publicKey: PublicKey; bump: number; } -interface DepositForBurnLogData { - nonce: BN; - burnToken: PublicKey; - amount: BN; - depositor: PublicKey; - mintRecipient: PublicKey; - destinationDomain: number; - destinationTokenMessenger: PublicKey; - destinationCaller: PublicKey; -} - const findProgramAddress = ( label: string, programId: PublicKey, @@ -161,6 +153,10 @@ export class ManualCCTPSolanaImpl implements ManualCCTP { 'sender_authority', tokenMessengerMinterProgram.programId, ); + const eventAuthority = findProgramAddress( + '__event_authority', + tokenMessengerMinterProgram.programId, + ); const associatedTokenAccount = await getAssociatedTokenAddress( tokenMint, @@ -169,8 +165,9 @@ export class ManualCCTPSolanaImpl implements ManualCCTP { const destContext = wh.getContext(recipientChain); const recipient = destContext.formatAddress(recipientAddress); + const messageSentKeypair = Keypair.generate(); - return tokenMessengerMinterProgram.methods + const tx = await tokenMessengerMinterProgram.methods .depositForBurn({ amount: new BN(parsedAmt.toString()), destinationDomain, @@ -178,6 +175,7 @@ export class ManualCCTPSolanaImpl implements ManualCCTP { }) .accounts({ owner: senderAddress, + eventRentPayer: senderAddress, senderAuthorityPda: authorityPda.publicKey, burnTokenAccount: associatedTokenAccount, messageTransmitter: messageTransmitterAccount.publicKey, @@ -186,11 +184,22 @@ export class ManualCCTPSolanaImpl implements ManualCCTP { tokenMinter: tokenMinter.publicKey, localToken: localToken.publicKey, burnTokenMint: tokenMint, + messageSentEventData: messageSentKeypair.publicKey, messageTransmitterProgram: messageTransmitterProgram.programId, tokenMessengerMinterProgram: tokenMessengerMinterProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + eventAuthority: eventAuthority.publicKey, + program: tokenMessengerMinterProgram.programId, }) .transaction(); + + const { blockhash } = + await solanaContext().connection!.getLatestBlockhash(); + tx.recentBlockhash = blockhash; + tx.feePayer = new PublicKey(senderAddress); + tx.partialSign(messageSentKeypair); + return tx; } async redeem( @@ -265,13 +274,20 @@ export class ManualCCTPSolanaImpl implements ManualCCTP { const authorityPda = findProgramAddress( 'message_transmitter_authority', messageTransmitterProgram.programId, + [tokenMessengerMinterProgram.programId], + ).publicKey; + const eventAuthority = findProgramAddress( + '__event_authority', + messageTransmitterProgram.programId, ).publicKey; + const tokenMessengerEventAuthority = findProgramAddress( + '__event_authority', + tokenMessengerMinterProgram.programId, + ); // Calculate the nonce PDA. const firstNonce = - ((nonce - BigInt(1)) / BigInt(NONCES_PER_ACCOUNT)) * - BigInt(NONCES_PER_ACCOUNT) + - BigInt(1); + ((nonce - 1n) / MAX_NONCES_PER_ACCOUNT) * MAX_NONCES_PER_ACCOUNT + 1n; const usedNonces = findProgramAddress( 'used_nonces', messageTransmitterProgram.programId, @@ -320,6 +336,18 @@ export class ManualCCTPSolanaImpl implements ManualCCTP { isWritable: false, pubkey: TOKEN_PROGRAM_ID, }); + accountMetas.push({ + isSigner: false, + isWritable: false, + pubkey: tokenMessengerEventAuthority.publicKey, + }); + accountMetas.push({ + isSigner: false, + isWritable: false, + pubkey: tokenMessengerMinterProgram.programId, + }); + + console.log(userTokenAccount.toString()); return ( messageTransmitterProgram.methods @@ -335,6 +363,8 @@ export class ManualCCTPSolanaImpl implements ManualCCTP { usedNonces, receiver: tokenMessengerMinterProgram.programId, systemProgram: SystemProgram.programId, + eventAuthority, + program: messageTransmitterProgram.programId, }) // Add remainingAccounts needed for TokenMessengerMinter CPI .remainingAccounts(accountMetas) @@ -346,58 +376,40 @@ export class ManualCCTPSolanaImpl implements ManualCCTP { id: string, chain: ChainName | ChainId, ): Promise { - const context = wh.getContext( - CHAIN_ID_SOLANA, - ) as SolanaContext; + const context = solanaContext(); const connection = context.connection; + if (!connection) throw new Error('No connection'); - const tx = await connection?.getTransaction(id); - if (!tx || !tx.meta) throw new Error('Transaction not found'); + const tx = await connection.getParsedTransaction(id); + if (!tx) throw new Error('Transaction not found'); const accounts = tx.transaction.message.accountKeys; - - const messageTransmitter = getMessageTransmitter(); - const tokenMessenger = getTokenMessenger(); - - // this log contains the cctp message information - const messageTransmitterParser = new EventParser( - messageTransmitter.programId, - messageTransmitter.coder, + const msgSentAccount = accounts[1]?.pubkey; + if (!msgSentAccount) throw new Error('No message sent account'); + const data = await connection.getAccountInfo(msgSentAccount); + if (!data) throw new Error('No message sent data'); + // TODO: why 44? + const circleMsgArray = new Uint8Array(data.data.slice(44)); + const [circleMsg, hash] = CircleBridge.deserialize(circleMsgArray); + console.log(circleMsg, hash); + const tokenAddress = await context.parseAssetAddress( + circleMsg.payload.burnToken.toString(), ); - const messageLogs = [ - ...messageTransmitterParser.parseLogs(tx.meta.logMessages || []), - ]; - const messageBytes = messageLogs[0].data.message as Buffer; - const message = ethUtils.hexlify(messageBytes); - - // this log contains the transfer information - const tokenMessengerParser = new EventParser( - tokenMessenger.programId, - tokenMessenger.coder, - ); - const tokenLogs = [ - ...tokenMessengerParser.parseLogs(tx.meta.logMessages || []), - ]; - const parsedCCTPLog = tokenLogs.find((l) => l.name === 'DepositForBurn'); - if (!parsedCCTPLog) { - throw new Error('DepositForBurn log not found'); - } - const data = parsedCCTPLog.data as any as DepositForBurnLogData; - - const toChain: ChainName = getChainNameCCTP(data.destinationDomain); - const destContext = wh.getContext(toChain); - const recipient = destContext.parseAddress( - ethUtils.hexlify(data.mintRecipient.toBuffer()), - ); - const tokenAddress = data.burnToken.toString(); const tokenId: TokenId = { address: tokenAddress, chain: 'solana' }; const token = getTokenById(tokenId); const decimals = await wh.fetchTokenDecimals(tokenId, 'solana'); + const toChain = getChainNameCCTP(circleMsg.destinationDomain); + const destContext = wh.getContext(toChain); + const recipient = destContext.parseAddress( + circleMsg.payload.mintRecipient.toString(), + ); return { sendTx: id, - sender: accounts[0].toString(), - amount: data.amount.toString(), + sender: await context.parseAddress( + circleMsg.payload.messageSender.toString(), + ), + amount: circleMsg.payload.amount.toString(), payloadID: PayloadType.Manual, recipient, toChain, @@ -408,9 +420,9 @@ export class ManualCCTPSolanaImpl implements ManualCCTP { tokenDecimals: decimals, tokenKey: token?.key || '', receivedTokenKey: getNativeVersionOfToken('USDC', toChain), - gasFee: tx.meta.fee.toString(), + gasFee: tx.meta?.fee.toString(), block: tx.slot, - message, + message: hexlify(circleMsgArray), // manual CCTP does not use wormhole emitterAddress: '', @@ -466,9 +478,7 @@ export class ManualCCTPSolanaImpl implements ManualCCTP { // Calculate the nonce PDA. const firstNonce = - ((nonce - BigInt(1)) / BigInt(NONCES_PER_ACCOUNT)) * - BigInt(NONCES_PER_ACCOUNT) + - BigInt(1); + ((nonce - 1n) / MAX_NONCES_PER_ACCOUNT) * MAX_NONCES_PER_ACCOUNT + 1n; const usedNoncesAddress = findProgramAddress( 'used_nonces', messageTransmitterProgram.programId, @@ -478,7 +488,8 @@ export class ManualCCTPSolanaImpl implements ManualCCTP { // usedNonces is an u64 100 elements array, where each bit acts a flag // to know whether a nonce has been used or not const { usedNonces } = - await messageTransmitterProgram.account.UsedNonces.fetch( + // @ts-ignore + await messageTransmitterProgram.account.usedNonces.fetch( usedNoncesAddress, ); diff --git a/wormhole-connect/src/utils/wallet/index.ts b/wormhole-connect/src/utils/wallet/index.ts index de3a39e29..4ec888ed7 100644 --- a/wormhole-connect/src/utils/wallet/index.ts +++ b/wormhole-connect/src/utils/wallet/index.ts @@ -124,7 +124,9 @@ export const signAndSendTransaction = async ( } case Context.SOLANA: { const { signAndSendTransaction } = await import('utils/wallet/solana'); - const tx = await signAndSendTransaction(transaction, wallet); + const tx = await signAndSendTransaction(transaction, wallet, { + skipPreflight: true, + }); return tx.id; } case Context.SUI: { diff --git a/wormhole-connect/src/views/Bridge/RouteOptions.tsx b/wormhole-connect/src/views/Bridge/RouteOptions.tsx index eeebd12fd..70c51fff9 100644 --- a/wormhole-connect/src/views/Bridge/RouteOptions.tsx +++ b/wormhole-connect/src/views/Bridge/RouteOptions.tsx @@ -22,7 +22,7 @@ import Options from 'components/Options'; import { isGatewayChain } from 'utils/cosmos'; import { isPorticoRoute } from 'routes/porticoBridge/utils'; import Price from 'components/Price'; -import { finality, chainIdToChain } from '@wormhole-foundation/connect-sdk'; +import { finality, chainIdToChain } from '@wormhole-foundation/sdk-base'; const useStyles = makeStyles()((theme: any) => ({ link: {