diff --git a/Anchor.toml b/Anchor.toml index 25511eb11..b889d96ee 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -46,6 +46,7 @@ generateExternalTypes = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/gener fakeFillWithRandomDistribution = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/fakeFillWithRandomDistribution.ts" addressToPublicKey = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/addressToPublicKey.ts" publicKeyToAddress = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/publicKeyToAddress.ts" +acrossPlusJupiter = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/acrossPlusJupiter.ts" [test.validator] url = "https://api.mainnet-beta.solana.com" diff --git a/deployments/README.md b/deployments/README.md index c0601b886..9dc40252d 100644 --- a/deployments/README.md +++ b/deployments/README.md @@ -133,7 +133,7 @@ This is because this `deployments.json` file is used by bots in [`@across-protoc ## Ink mainnet (57073) -| Contract Name | Address | -| ------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| Ink_SpokePool | [0xeF684C38F94F48775959ECf2012D7E864ffb9dd4](https://explorer.inkonchain.com/address/0xeF684C38F94F48775959ECf2012D7E864ffb9dd4) | -| MulticallHandler | [0x924a9f036260DdD5808007E1AA95f08eD08aA569](https://explorer.inkonchain.com/address/0x924a9f036260DdD5808007E1AA95f08eD08aA569) | +| Contract Name | Address | +| ---------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| Ink_SpokePool | [0xeF684C38F94F48775959ECf2012D7E864ffb9dd4](https://explorer.inkonchain.com/address/0xeF684C38F94F48775959ECf2012D7E864ffb9dd4) | +| MulticallHandler | [0x924a9f036260DdD5808007E1AA95f08eD08aA569](https://explorer.inkonchain.com/address/0x924a9f036260DdD5808007E1AA95f08eD08aA569) | diff --git a/deployments/ink/SpokePoolVerifier.json b/deployments/ink/SpokePoolVerifier.json index 7d25b4799..5344f10b1 100644 --- a/deployments/ink/SpokePoolVerifier.json +++ b/deployments/ink/SpokePoolVerifier.json @@ -112,4 +112,4 @@ "storage": [], "types": null } -} \ No newline at end of file +} diff --git a/deployments/ink/solcInputs/53ab13385da43753d9a657e7780a0560.json b/deployments/ink/solcInputs/53ab13385da43753d9a657e7780a0560.json index b640ddc74..b9b6c4373 100644 --- a/deployments/ink/solcInputs/53ab13385da43753d9a657e7780a0560.json +++ b/deployments/ink/solcInputs/53ab13385da43753d9a657e7780a0560.json @@ -33,13 +33,11 @@ "storageLayout", "evm.gasEstimates" ], - "": [ - "ast" - ] + "": ["ast"] } }, "metadata": { "useLiteralContent": true } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index ffb1b1d17..a917fbec6 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@uma/core": "^2.61.0", "axios": "^1.7.4", "bs58": "^6.0.0", + "cross-fetch": "^4.0.0", "prettier-plugin-rust": "^0.1.9", "yargs": "^17.7.2", "zksync-web3": "^0.14.3" diff --git a/scripts/svm/acrossPlusJupiter.ts b/scripts/svm/acrossPlusJupiter.ts new file mode 100644 index 000000000..f92eaec92 --- /dev/null +++ b/scripts/svm/acrossPlusJupiter.ts @@ -0,0 +1,348 @@ +// This script implements Across+ fill where relayed tokens are swapped on Jupiter and sent to the final recipient via +// the message handler. Note that Jupiter swap works only on mainnet, so extra care should be taken to select output +// token, amounts and final recipient since this is a fake fill and relayer would not be refunded. + +import * as anchor from "@coral-xyz/anchor"; +import { AnchorProvider, BN, Program, Wallet } from "@coral-xyz/anchor"; +import { AccountMeta, TransactionInstruction, PublicKey, AddressLookupTableAccount } from "@solana/web3.js"; +import fetch from "cross-fetch"; +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; +import { SvmSpoke } from "../../target/types/svm_spoke"; +import { MulticallHandler } from "../../target/types/multicall_handler"; +import { formatUsdc, parseUsdc } from "./utils/helpers"; +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + createApproveCheckedInstruction, + createAssociatedTokenAccountIdempotentInstruction, + createTransferCheckedInstruction, + getAssociatedTokenAddressSync, + getMinimumBalanceForRentExemptAccount, + getMint, + getOrCreateAssociatedTokenAccount, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; +import { + AcrossPlusMessageCoder, + calculateRelayHashUint8Array, + intToU8Array32, + loadFillV3RelayParams, + MulticallHandlerCoder, + prependComputeBudget, + sendTransactionWithLookupTable, + getSolanaChainId, + isSolanaDevnet, + SOLANA_SPOKE_STATE_SEED, + SOLANA_USDC_MAINNET, +} from "../../src/svm"; +import { CHAIN_IDs } from "../../utils/constants"; +import { FillDataParams, FillDataValues } from "../../src/types/svm"; + +const swapApiBaseUrl = "https://quote-api.jup.ag/v6/"; + +// Set up Solana provider and signer. +const provider = AnchorProvider.env(); +anchor.setProvider(provider); +const relayer = (provider.wallet as Wallet).payer; + +// Get Solana programs. +const svmSpokeIdl = require("../../target/idl/svm_spoke.json"); +const svmSpokeProgram = new Program(svmSpokeIdl, provider); +const handlerIdl = require("../../target/idl/multicall_handler.json"); +const handlerProgram = new Program(handlerIdl, provider); + +if (isSolanaDevnet(provider)) throw new Error("This script is only for mainnet"); + +// Parse arguments +const argv = yargs(hideBin(process.argv)) + .option("recipient", { type: "string", demandOption: true, describe: "Recipient public key" }) + .option("outputMint", { type: "string", demandOption: true, describe: "Token to receive from the swap" }) + .option("usdcValue", { type: "string", demandOption: true, describe: "USDC value bridged/swapped (formatted)" }) + .option("slippageBps", { type: "number", demandOption: false, describe: "Custom slippage in bps" }) + .option("maxAccounts", { type: "number", demandOption: false, describe: "Maximum swap accounts" }) + .option("priorityFeePrice", { type: "number", demandOption: false, describe: "Priority fee price in micro lamports" }) + .option("fillComputeUnit", { type: "number", demandOption: false, describe: "Compute unit limit in fill" }).argv; + +async function acrossPlusJupiter(): Promise { + const resolvedArgv = await argv; + const seed = SOLANA_SPOKE_STATE_SEED; // Seed is always 0 for the state account PDA in public networks. + const recipient = new PublicKey(resolvedArgv.recipient); + const outputMint = new PublicKey(resolvedArgv.outputMint); + const usdcAmount = parseUsdc(resolvedArgv.usdcValue); + const slippageBps = resolvedArgv.slippageBps || 100; // default to 1% + const maxAccounts = resolvedArgv.maxAccounts || 24; + const priorityFeePrice = resolvedArgv.priorityFeePrice; + const fillComputeUnit = resolvedArgv.fillComputeUnit || 400_000; + + const usdcMint = new PublicKey(SOLANA_USDC_MAINNET); // Only mainnet USDC is supported in this script. + + // Handler signer will swap tokens on Jupiter. + const [handlerSigner] = PublicKey.findProgramAddressSync([Buffer.from("handler_signer")], handlerProgram.programId); + + // Get ATAs for the output mint. + const outputMintInfo = await provider.connection.getAccountInfo(outputMint); + if (!outputMintInfo) throw new Error("Output mint account not found"); + const outputTokenProgram = new PublicKey(outputMintInfo.owner); + const recipientOutputTA = getAssociatedTokenAddressSync(outputMint, recipient, true, outputTokenProgram); + const handlerOutputTA = getAssociatedTokenAddressSync(outputMint, handlerSigner, true, outputTokenProgram); + + // Will need lamports to potentially create ATA both for the recipient and the handler signer. + const valueAmount = (await getMinimumBalanceForRentExemptAccount(provider.connection)) * 2; + + console.log("Filling Across+ swap..."); + console.table([ + { Property: "svmSpokeProgramProgramId", Value: svmSpokeProgram.programId.toString() }, + { Property: "handlerProgramId", Value: handlerProgram.programId.toString() }, + { Property: "recipient", Value: recipient.toString() }, + { Property: "recipientTA", Value: recipientOutputTA.toString() }, + { Property: "valueAmount", Value: valueAmount.toString() }, + { Property: "relayerPublicKey", Value: relayer.publicKey.toString() }, + { Property: "inputMint", Value: usdcMint.toString() }, + { Property: "outputMint", Value: outputMint.toString() }, + { Property: "usdcValue (formatted)", Value: formatUsdc(usdcAmount) }, + { Property: "slippageBps", Value: slippageBps }, + { Property: "maxAccounts", Value: maxAccounts }, + { Property: "handlerSigner", Value: handlerSigner.toString() }, + ]); + + // Get quote from Jupiter. + const quoteResponse = await ( + await fetch( + swapApiBaseUrl + + "quote?inputMint=" + + usdcMint.toString() + + "&outputMint=" + + outputMint.toString() + + "&amount=" + + usdcAmount + + "&slippageBps=" + + slippageBps + + "&maxAccounts=" + + maxAccounts + ) + ).json(); + if (quoteResponse.error) { + throw new Error("Failed to get quote: " + quoteResponse.error); + } + + // Create swap instructions on behalf of the handler signer. We do not enable unwrapping of WSOL as that would require + // additional logic to handle transferring SOL from the handler signer to the recipient. + const wrapAndUnwrapSol = false; + const instructions = await ( + await fetch(swapApiBaseUrl + "swap-instructions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ quoteResponse, userPublicKey: handlerSigner.toString(), wrapAndUnwrapSol }), + }) + ).json(); + if (instructions.error) { + throw new Error("Failed to get swap instructions: " + instructions.error); + } + + // Helper to load Jupiter ALTs. + const getAddressLookupTableAccounts = async (keys: string[]): Promise => { + const addressLookupTableAccountInfos = await provider.connection.getMultipleAccountsInfo( + keys.map((key) => new PublicKey(key)) + ); + + return addressLookupTableAccountInfos.reduce((acc: AddressLookupTableAccount[], accountInfo, index) => { + const addressLookupTableAddress = keys[index]; + if (accountInfo) { + const addressLookupTableAccount = new AddressLookupTableAccount({ + key: new PublicKey(addressLookupTableAddress), + state: AddressLookupTableAccount.deserialize(accountInfo.data), + }); + acc.push(addressLookupTableAccount); + } + + return acc; + }, []); + }; + + const addressLookupTableAccounts = await getAddressLookupTableAccounts(instructions.addressLookupTableAddresses); + + // Helper to deserialize instruction and check if it would fit in inner CPI limit. + const deserializeInstruction = (instruction: any) => { + const transactionInstruction = new TransactionInstruction({ + programId: new PublicKey(instruction.programId), + keys: instruction.accounts.map((key: any) => ({ + pubkey: new PublicKey(key.pubkey), + isSigner: key.isSigner, + isWritable: key.isWritable, + })), + data: Buffer.from(instruction.data, "base64"), + }); + const innerCpiLimit = 1280; + const innerCpiSize = transactionInstruction.keys.length * 34 + transactionInstruction.data.length; + if (innerCpiSize > innerCpiLimit) { + throw new Error( + `Instruction too large for inner CPI: ${innerCpiSize} > ${innerCpiLimit}, try lowering maxAccounts` + ); + } + return transactionInstruction; + }; + + // Ignore Jupiter setup instructions as we need to create ATA both for the recipient and the handler signer. + const createHandlerATAInstruction = createAssociatedTokenAccountIdempotentInstruction( + handlerSigner, + handlerOutputTA, + handlerSigner, + outputMint, + outputTokenProgram + ); + const createRecipientATAInstruction = createAssociatedTokenAccountIdempotentInstruction( + handlerSigner, + recipientOutputTA, + recipient, + outputMint, + outputTokenProgram + ); + + // Construct ix to transfer minimum output tokens from handler to the recipient ATA. Note that all remaining tokens + // can be stolen by anyone. This could be improved by creating a sweeper program that reads actual handler ATA balance + // and transfers all of them to the recipient ATA. + const outputDecimals = (await getMint(provider.connection, outputMint, undefined, outputTokenProgram)).decimals; + const transferInstruction = createTransferCheckedInstruction( + handlerOutputTA, + outputMint, + recipientOutputTA, + handlerSigner, + quoteResponse.otherAmountThreshold, + outputDecimals, + undefined, + outputTokenProgram + ); + + // Encode all instructions with handler PDA as the payer for ATA initialization. + const multicallHandlerCoder = new MulticallHandlerCoder( + [ + createHandlerATAInstruction, + deserializeInstruction(instructions.swapInstruction), + createRecipientATAInstruction, + transferInstruction, + ], + handlerSigner + ); + const handlerMessage = multicallHandlerCoder.encode(); + const message = new AcrossPlusMessageCoder({ + handler: handlerProgram.programId, + readOnlyLen: multicallHandlerCoder.readOnlyLen, + valueAmount: new BN(valueAmount), // Must exactly cover ATA creation. + accounts: multicallHandlerCoder.compiledMessage.accountKeys, + handlerMessage, + }); + const encodedMessage = message.encode(); + + // Define the state account PDA + const [statePda] = PublicKey.findProgramAddressSync( + [Buffer.from("state"), seed.toArrayLike(Buffer, "le", 8)], + svmSpokeProgram.programId + ); + + // This script works only on mainnet. + const solanaChainId = new BN(getSolanaChainId("mainnet").toString()); + + // Construct relay data. + const relayData = { + depositor: recipient, // This is not a real deposit, so use recipient as depositor. + recipient: handlerSigner, + exclusiveRelayer: PublicKey.default, + inputToken: usdcMint, // This is not a real deposit, so use the same USDC as input token. + outputToken: usdcMint, // USDC is output token for the bridge and input token for the swap. + inputAmount: new BN(usdcAmount.toString()), // This is not a real deposit, so use the same USDC amount as input amount. + outputAmount: new BN(usdcAmount.toString()), + originChainId: new BN(CHAIN_IDs.MAINNET), // This is not a real deposit, so use MAINNET as origin chain id. + depositId: intToU8Array32(new BN(Math.random() * 2 ** 32)), // This is not a real deposit, use random deposit id. + fillDeadline: Math.floor(Date.now() / 1000) + 60, // Current time + 1 minute + exclusivityDeadline: Math.floor(Date.now() / 1000) + 30, // Current time + 30 seconds + message: encodedMessage, + }; + const relayHashUint8Array = calculateRelayHashUint8Array(relayData, solanaChainId); + console.log("Relay Data:"); + console.table( + Object.entries(relayData) + .map(([key, value]) => ({ + key, + value: value.toString(), + })) + .filter((entry) => entry.key !== "message") // Message is printed separately. + ); + console.log("Relay message:", relayData.message.toString("hex")); + + // Define the fill status account PDA + const [fillStatusPda] = PublicKey.findProgramAddressSync( + [Buffer.from("fills"), relayHashUint8Array], + svmSpokeProgram.programId + ); + + // Create ATA for the relayer and handler USDC token accounts + const relayerUsdcTA = getAssociatedTokenAddressSync(usdcMint, relayer.publicKey, true); + const handlerUsdcTA = ( + await getOrCreateAssociatedTokenAccount(provider.connection, relayer, usdcMint, handlerSigner, true, "confirmed") + ).address; + + // Delegate state PDA to pull relayer USDC tokens. + const usdcDecimals = (await getMint(provider.connection, usdcMint)).decimals; + const approveIx = await createApproveCheckedInstruction( + relayerUsdcTA, + usdcMint, + statePda, + relayer.publicKey, + BigInt(usdcAmount.toString()), + usdcDecimals + ); + + // Prepare fill instruction. + const fillV3RelayValues: FillDataValues = [ + Array.from(relayHashUint8Array), + relayData, + solanaChainId, + relayer.publicKey, + ]; + await loadFillV3RelayParams( + svmSpokeProgram, + relayer, + fillV3RelayValues[1], + fillV3RelayValues[2], + fillV3RelayValues[3], + priorityFeePrice + ); + const [instructionParams] = PublicKey.findProgramAddressSync( + [Buffer.from("instruction_params"), relayer.publicKey.toBuffer()], + svmSpokeProgram.programId + ); + + const fillV3RelayParams: FillDataParams = [fillV3RelayValues[0], null, null, null]; + const fillAccounts = { + state: statePda, + signer: relayer.publicKey, + instructionParams, + mint: usdcMint, + relayerTokenAccount: relayerUsdcTA, + recipientTokenAccount: handlerUsdcTA, + fillStatus: fillStatusPda, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + program: svmSpokeProgram.programId, + }; + const fillRemainingAccounts: AccountMeta[] = [ + { pubkey: handlerProgram.programId, isSigner: false, isWritable: false }, + ...multicallHandlerCoder.compiledKeyMetas, + ]; + const fillIx = await svmSpokeProgram.methods + .fillV3Relay(...fillV3RelayParams) + .accounts(fillAccounts) + .remainingAccounts(fillRemainingAccounts) + .instruction(); + + // Fill using the ALT with the provided compute budget settings. + const txSignature = await sendTransactionWithLookupTable( + provider.connection, + prependComputeBudget([approveIx, fillIx], priorityFeePrice, fillComputeUnit), + relayer, + addressLookupTableAccounts + ); + console.log("Fill transaction signature:", txSignature); +} + +acrossPlusJupiter(); diff --git a/scripts/svm/fakeFillWithRandomDistribution.ts b/scripts/svm/fakeFillWithRandomDistribution.ts index 5159795ba..b622fedd9 100644 --- a/scripts/svm/fakeFillWithRandomDistribution.ts +++ b/scripts/svm/fakeFillWithRandomDistribution.ts @@ -220,7 +220,7 @@ async function fillV3RelayToRandom(): Promise { .instruction(); // Fill using the ALT. - const { txSignature } = await sendTransactionWithLookupTable( + const txSignature = await sendTransactionWithLookupTable( provider.connection, [approveInstruction, fillInstruction], signer diff --git a/scripts/svm/utils/helpers.ts b/scripts/svm/utils/helpers.ts index c65133480..7c4390636 100644 --- a/scripts/svm/utils/helpers.ts +++ b/scripts/svm/utils/helpers.ts @@ -14,6 +14,10 @@ export const formatUsdc = (amount: BigNumber): string => { return ethers.utils.formatUnits(amount, 6); }; +export const parseUsdc = (amount: string): BigNumber => { + return ethers.utils.parseUnits(amount, 6); +}; + export function constructEmptyPoolRebalanceTree(chainId: BigNumber, groupIndex: number) { const poolRebalanceLeaf = { chainId, diff --git a/src/svm/instructionParamsUtils.ts b/src/svm/instructionParamsUtils.ts index dd5f8cf63..0a3887af6 100644 --- a/src/svm/instructionParamsUtils.ts +++ b/src/svm/instructionParamsUtils.ts @@ -1,8 +1,9 @@ -import { Keypair, TransactionInstruction, Transaction, sendAndConfirmTransaction, PublicKey } from "@solana/web3.js"; +import { Keypair, TransactionInstruction, sendAndConfirmTransaction, PublicKey } from "@solana/web3.js"; import { Program, BN } from "@coral-xyz/anchor"; import { RelayData, SlowFillLeaf, RelayerRefundLeafSolana } from "../types/svm"; import { SvmSpoke } from "../../target/types/svm_spoke"; import { LargeAccountsCoder } from "./coders"; +import { createTransactionWithComputeBudget } from "./transactionUtils"; /** * Loads execute relayer refund leaf parameters. @@ -43,7 +44,11 @@ export async function loadExecuteRelayerRefundLeafParams( /** * Closes the instruction parameters account. */ -export async function closeInstructionParams(program: Program, signer: Keypair) { +export async function closeInstructionParams( + program: Program, + signer: Keypair, + priorityFeePrice?: number | bigint +) { const [instructionParams] = PublicKey.findProgramAddressSync( [Buffer.from("instruction_params"), signer.publicKey.toBuffer()], program.programId @@ -51,7 +56,14 @@ export async function closeInstructionParams(program: Program, signer: const accountInfo = await program.provider.connection.getAccountInfo(instructionParams); if (accountInfo !== null) { const closeIx = await program.methods.closeInstructionParams().accounts({ signer: signer.publicKey }).instruction(); - await sendAndConfirmTransaction(program.provider.connection, new Transaction().add(closeIx), [signer]); + await sendAndConfirmTransaction( + program.provider.connection, + createTransactionWithComputeBudget([closeIx], priorityFeePrice), + [signer], + { + commitment: "confirmed", + } + ); } } @@ -99,10 +111,11 @@ export async function loadFillV3RelayParams( signer: Keypair, relayData: RelayData, repaymentChainId: BN, - repaymentAddress: PublicKey + repaymentAddress: PublicKey, + priorityFeePrice?: number | bigint ) { // Close the instruction params account if the caller has used it before. - await closeInstructionParams(program, signer); + await closeInstructionParams(program, signer, priorityFeePrice); // Execute load instructions sequentially. const { loadInstructions } = await createFillV3RelayParamsInstructions( @@ -113,16 +126,28 @@ export async function loadFillV3RelayParams( repaymentAddress ); for (let i = 0; i < loadInstructions.length; i += 1) { - await sendAndConfirmTransaction(program.provider.connection, new Transaction().add(loadInstructions[i]), [signer]); + await sendAndConfirmTransaction( + program.provider.connection, + createTransactionWithComputeBudget([loadInstructions[i]], priorityFeePrice), + [signer], + { + commitment: "confirmed", + } + ); } } /** * Loads requestV3 slow fill parameters. */ -export async function loadRequestV3SlowFillParams(program: Program, signer: Keypair, relayData: RelayData) { +export async function loadRequestV3SlowFillParams( + program: Program, + signer: Keypair, + relayData: RelayData, + priorityFeePrice?: number | bigint +) { // Close the instruction params account if the caller has used it before. - await closeInstructionParams(program, signer); + await closeInstructionParams(program, signer, priorityFeePrice); // Execute load instructions sequentially. const maxInstructionParamsFragment = 900; // Should not exceed message size limit when writing to the data account. @@ -159,10 +184,11 @@ export async function loadExecuteV3SlowRelayLeafParams( signer: Keypair, slowFillLeaf: SlowFillLeaf, rootBundleId: number, - proof: number[][] + proof: number[][], + priorityFeePrice?: number | bigint ) { // Close the instruction params account if the caller has used it before. - await closeInstructionParams(program, signer); + await closeInstructionParams(program, signer, priorityFeePrice); // Execute load instructions sequentially. const maxInstructionParamsFragment = 900; // Should not exceed message size limit when writing to the data account. diff --git a/src/svm/transactionUtils.ts b/src/svm/transactionUtils.ts index 94f3a1030..03e555d64 100644 --- a/src/svm/transactionUtils.ts +++ b/src/svm/transactionUtils.ts @@ -3,20 +3,55 @@ import { AddressLookupTableProgram, Connection, Keypair, - PublicKey, TransactionInstruction, TransactionMessage, VersionedTransaction, + AddressLookupTableAccount, + Commitment, + Transaction, + ComputeBudgetProgram, } from "@solana/web3.js"; +/** + * Confirms transaction using the latest block, defaulting to confirmed commitment. + */ +export async function confirmTransaction(connection: Connection, txSignature: string, commitment?: Commitment) { + const block = await connection.getLatestBlockhash(); + await connection.confirmTransaction( + { signature: txSignature, blockhash: block.blockhash, lastValidBlockHeight: block.lastValidBlockHeight }, + commitment || "confirmed" + ); +} + /** * Sends a transaction using an Address Lookup Table for large numbers of accounts. */ export async function sendTransactionWithLookupTable( connection: Connection, instructions: TransactionInstruction[], - sender: Keypair -): Promise<{ txSignature: string; lookupTableAddress: PublicKey }> { + sender: Keypair, + lookupTables?: AddressLookupTableAccount[] +): Promise { + // If lookup tables were provided, just send transaction using them. + if (lookupTables) { + const versionedTx = new VersionedTransaction( + new TransactionMessage({ + payerKey: sender.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions, + }).compileToV0Message(lookupTables) + ); + + // Sign and submit the versioned transaction. + versionedTx.sign([sender]); + const txSignature = await connection.sendTransaction(versionedTx); + + // Confirm the versioned transaction + await confirmTransaction(connection, txSignature); + + return txSignature; + } + // Maximum number of accounts that can be added to Address Lookup Table (ALT) in a single transaction. const maxExtendedAccounts = 30; @@ -39,7 +74,8 @@ export async function sendTransactionWithLookupTable( // Submit the ALT creation transaction await web3.sendAndConfirmTransaction(connection, new web3.Transaction().add(lookupTableInstruction), [sender], { - skipPreflight: true, // Avoids recent slot mismatch in simulation. + commitment: "confirmed", + skipPreflight: true, }); // Extend the ALT with all accounts making sure not to exceed the maximum number of accounts per transaction. @@ -52,12 +88,16 @@ export async function sendTransactionWithLookupTable( }); await web3.sendAndConfirmTransaction(connection, new web3.Transaction().add(extendInstruction), [sender], { - skipPreflight: true, // Avoids recent slot mismatch in simulation. + commitment: "confirmed", + skipPreflight: true, }); } - // Avoids invalid ALT index as ALT might not be active yet on the following tx. - await new Promise((resolve) => setTimeout(resolve, 1000)); + // Wait for slot to advance. LUTs only active after slot advance. + const initialSlot = await connection.getSlot(); + while ((await connection.getSlot()) === initialSlot) { + await new Promise((resolve) => setTimeout(resolve, 50)); + } // Fetch the AddressLookupTableAccount const lookupTableAccount = (await connection.getAddressLookupTable(lookupTableAddress)).value; @@ -76,5 +116,33 @@ export async function sendTransactionWithLookupTable( versionedTx.sign([sender]); const txSignature = await connection.sendTransaction(versionedTx); - return { txSignature, lookupTableAddress }; + // Confirm the versioned transaction + await confirmTransaction(connection, txSignature); + + return txSignature; +} + +/* + * Creates a transaction with optional ComputeBudget instructions. + */ +export function createTransactionWithComputeBudget( + instructions: TransactionInstruction[], + priorityFeePrice?: number | bigint, + computeUnitLimit?: number +) { + return new Transaction().add(...prependComputeBudget(instructions, priorityFeePrice, computeUnitLimit)); +} + +/* + * Prepends optional ComputeBudget instructions to the transaction instructions. + */ +export function prependComputeBudget( + instructions: TransactionInstruction[], + priorityFeePrice?: number | bigint, + computeUnitLimit?: number +) { + if (computeUnitLimit) instructions.unshift(ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnitLimit })); + if (priorityFeePrice) + instructions.unshift(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: priorityFeePrice })); + return instructions; } diff --git a/test/svm/SvmSpoke.SlowFill.AcrossPlus.ts b/test/svm/SvmSpoke.SlowFill.AcrossPlus.ts index ebcc860dc..162e3bd5a 100644 --- a/test/svm/SvmSpoke.SlowFill.AcrossPlus.ts +++ b/test/svm/SvmSpoke.SlowFill.AcrossPlus.ts @@ -448,7 +448,7 @@ describe("svm_spoke.slow_fill.across_plus", () => { [relayer] ); } - const { txSignature } = await sendTransactionWithLookupTable(connection, [executeIx], relayer); + const txSignature = await sendTransactionWithLookupTable(connection, [executeIx], relayer); await connection.confirmTransaction(txSignature, "confirmed"); await connection.getTransaction(txSignature, { commitment: "confirmed", diff --git a/yarn.lock b/yarn.lock index 3ae6d88a9..340e82638 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6088,6 +6088,13 @@ cross-fetch@^3.1.4, cross-fetch@^3.1.5: dependencies: node-fetch "^2.6.12" +cross-fetch@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.1.0.tgz#8f69355007ee182e47fa692ecbaa37a52e43c3d2" + integrity sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw== + dependencies: + node-fetch "^2.7.0" + cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"