diff --git a/examples/oft-solana/tasks/solana/createOFT.ts b/examples/oft-solana/tasks/solana/createOFT.ts index 0fe81d8ee..08d64383c 100644 --- a/examples/oft-solana/tasks/solana/createOFT.ts +++ b/examples/oft-solana/tasks/solana/createOFT.ts @@ -26,7 +26,7 @@ import { OFT_DECIMALS as DEFAULT_SHARED_DECIMALS, oft, types } from '@layerzerol import { checkMultisigSigners, createMintAuthorityMultisig } from './multisig' import { assertAccountInitialized } from './utils' -import { deriveConnection, deriveKeys, getExplorerTxLink, output } from './index' +import { addComputeUnitInstructions, deriveConnection, deriveKeys, getExplorerTxLink, output } from './index' const DEFAULT_LOCAL_DECIMALS = 9 @@ -102,6 +102,8 @@ interface CreateOFTTaskArgs { * The URI for the token metadata. */ uri: string + + computeUnitPriceScaleFactor: number } // Define a Hardhat task for creating OFT on Solana @@ -135,6 +137,7 @@ task('lz:oft:solana:create', 'Mints new SPL Token and creates new OFT Store acco devtoolsTypes.string ) .addParam('uri', 'URI for token metadata', '', devtoolsTypes.string) + .addParam('computeUnitPriceScaleFactor', 'The compute unit price scale factor', 4, devtoolsTypes.float, true) .setAction( async ({ amount, @@ -151,6 +154,7 @@ task('lz:oft:solana:create', 'Mints new SPL Token and creates new OFT Store acco onlyOftStore, tokenProgram: tokenProgramStr, uri, + computeUnitPriceScaleFactor, }: CreateOFTTaskArgs) => { const isMABA = !!mintStr if (tokenProgramStr !== TOKEN_PROGRAM_ID.toBase58() && !isMABA) { @@ -221,34 +225,49 @@ task('lz:oft:solana:create', 'Mints new SPL Token and creates new OFT Store acco }) ) } + txBuilder = await addComputeUnitInstructions( + connection, + umi, + eid, + txBuilder, + umiWalletSigner, + computeUnitPriceScaleFactor + ) const createTokenTx = await txBuilder.sendAndConfirm(umi) await assertAccountInitialized(connection, toWeb3JsPublicKey(mint.publicKey)) console.log(`createTokenTx: ${getExplorerTxLink(bs58.encode(createTokenTx.signature), isTestnet)}`) } const lockboxSigner = createSignerFromKeypair({ eddsa: eddsa }, lockBox) - const { signature } = await transactionBuilder() - .add( - oft.initOft( - { - payer: umiWalletSigner, - admin: umiWalletKeyPair.publicKey, - mint: mint.publicKey, - escrow: lockboxSigner, - }, - types.OFTType.Native, - sharedDecimals, - { - oft: programId, - token: tokenProgramId, - } - ) + let txBuilder = transactionBuilder().add( + oft.initOft( + { + payer: umiWalletSigner, + admin: umiWalletKeyPair.publicKey, + mint: mint.publicKey, + escrow: lockboxSigner, + }, + types.OFTType.Native, + sharedDecimals, + { + oft: programId, + token: tokenProgramId, + } ) - .sendAndConfirm(umi) + ) + txBuilder = await addComputeUnitInstructions( + connection, + umi, + eid, + txBuilder, + umiWalletSigner, + computeUnitPriceScaleFactor + ) + const { signature } = await txBuilder.sendAndConfirm(umi) console.log(`initOftTx: ${getExplorerTxLink(bs58.encode(signature), isTestnet)}`) if (!isMABA) { - const { signature } = await transactionBuilder() + let txBuilder = transactionBuilder() .add( setAuthority(umi, { owned: mint.publicKey, @@ -265,7 +284,15 @@ task('lz:oft:solana:create', 'Mints new SPL Token and creates new OFT Store acco authorityType: 1, }) ) - .sendAndConfirm(umi) + txBuilder = await addComputeUnitInstructions( + connection, + umi, + eid, + txBuilder, + umiWalletSigner, + computeUnitPriceScaleFactor + ) + const { signature } = await txBuilder.sendAndConfirm(umi) console.log(`setAuthorityTx: ${getExplorerTxLink(bs58.encode(signature), isTestnet)}`) } output(eid, programIdStr, mint.publicKey, mintAuthorityPublicKey.toBase58(), escrowPK, oftStorePda) diff --git a/examples/oft-solana/tasks/solana/createOFTAdapter.ts b/examples/oft-solana/tasks/solana/createOFTAdapter.ts index 13348dead..078c7d030 100644 --- a/examples/oft-solana/tasks/solana/createOFTAdapter.ts +++ b/examples/oft-solana/tasks/solana/createOFTAdapter.ts @@ -8,7 +8,7 @@ import { types as devtoolsTypes } from '@layerzerolabs/devtools-evm-hardhat' import { EndpointId } from '@layerzerolabs/lz-definitions' import { OFT_DECIMALS, oft, types } from '@layerzerolabs/oft-v2-solana-sdk' -import { deriveConnection, deriveKeys, getExplorerTxLink, output } from './index' +import { addComputeUnitInstructions, deriveConnection, deriveKeys, getExplorerTxLink, output } from './index' interface CreateOFTAdapterTaskArgs { /** @@ -30,6 +30,8 @@ interface CreateOFTAdapterTaskArgs { * The Token Program public key. */ tokenProgram: string + + computeUnitPriceScaleFactor: number } // Define a Hardhat task for creating OFTAdapter on Solana @@ -38,14 +40,16 @@ task('lz:oft-adapter:solana:create', 'Creates new OFT Adapter (OFT Store PDA)') .addParam('programId', 'The OFT program ID') .addParam('eid', 'Solana mainnet or testnet', undefined, devtoolsTypes.eid) .addParam('tokenProgram', 'The Token Program public key', TOKEN_PROGRAM_ID.toBase58(), devtoolsTypes.string, true) + .addParam('computeUnitPriceScaleFactor', 'The compute unit price scale factor', 4, devtoolsTypes.float, true) .setAction( async ({ eid, mint: mintStr, programId: programIdStr, tokenProgram: tokenProgramStr, + computeUnitPriceScaleFactor, }: CreateOFTAdapterTaskArgs) => { - const { connection, umi, umiWalletKeyPair } = await deriveConnection(eid) + const { connection, umi, umiWalletKeyPair, umiWalletSigner } = await deriveConnection(eid) const { programId, lockBox, escrowPK, oftStorePda, eddsa } = deriveKeys(programIdStr) const tokenProgram = publicKey(tokenProgramStr) @@ -55,21 +59,31 @@ task('lz:oft-adapter:solana:create', 'Creates new OFT Adapter (OFT Store PDA)') const mintAuthority = mintPDA.mintAuthority - const initOftIx = oft.initOft( - { - payer: createSignerFromKeypair({ eddsa: eddsa }, umiWalletKeyPair), - admin: umiWalletKeyPair.publicKey, - mint, - escrow: createSignerFromKeypair({ eddsa: eddsa }, lockBox), - }, - types.OFTType.Adapter, - OFT_DECIMALS, - { - oft: programId, - token: tokenProgram ? publicKey(tokenProgram) : undefined, - } + let txBuilder = transactionBuilder().add( + oft.initOft( + { + payer: createSignerFromKeypair({ eddsa: eddsa }, umiWalletKeyPair), + admin: umiWalletKeyPair.publicKey, + mint: mint, + escrow: createSignerFromKeypair({ eddsa: eddsa }, lockBox), + }, + types.OFTType.Adapter, + OFT_DECIMALS, + { + oft: programId, + token: tokenProgram ? publicKey(tokenProgram) : undefined, + } + ) + ) + txBuilder = await addComputeUnitInstructions( + connection, + umi, + eid, + txBuilder, + umiWalletSigner, + computeUnitPriceScaleFactor ) - const { signature } = await transactionBuilder().add(initOftIx).sendAndConfirm(umi) + const { signature } = await txBuilder.sendAndConfirm(umi) console.log(`initOftTx: ${getExplorerTxLink(bs58.encode(signature), eid == EndpointId.SOLANA_V2_TESTNET)}`) output(eid, programIdStr, mint, mintAuthority ? mintAuthority.toBase58() : '', escrowPK, oftStorePda) diff --git a/examples/oft-solana/tasks/solana/getPrioFees.ts b/examples/oft-solana/tasks/solana/getPrioFees.ts index c903838a4..d3dff02c8 100644 --- a/examples/oft-solana/tasks/solana/getPrioFees.ts +++ b/examples/oft-solana/tasks/solana/getPrioFees.ts @@ -20,7 +20,7 @@ interface GetPrioFeesTaskArgs { task('lz:solana:get-priority-fees', 'Fetches prioritization fees from the Solana network') .addParam('eid', 'The endpoint ID for the Solana network', undefined, devtoolsTypes.eid) - .addParam( + .addOptionalParam( 'address', 'The address (program ID or account address) that will be written to', undefined, diff --git a/examples/oft-solana/tasks/solana/index.ts b/examples/oft-solana/tasks/solana/index.ts index d74a0c93e..69b2c6a89 100644 --- a/examples/oft-solana/tasks/solana/index.ts +++ b/examples/oft-solana/tasks/solana/index.ts @@ -1,15 +1,43 @@ +import assert from 'assert' import { existsSync, mkdirSync, writeFileSync } from 'node:fs' -import { mplToolbox } from '@metaplex-foundation/mpl-toolbox' -import { EddsaInterface, createSignerFromKeypair, publicKey, signerIdentity } from '@metaplex-foundation/umi' +import { + fetchAddressLookupTable, + mplToolbox, + setComputeUnitLimit, + setComputeUnitPrice, +} from '@metaplex-foundation/mpl-toolbox' +import { + AddressLookupTableInput, + EddsaInterface, + Instruction, + KeypairSigner, + PublicKey, + TransactionBuilder, + Umi, + createSignerFromKeypair, + publicKey, + signerIdentity, + transactionBuilder, +} from '@metaplex-foundation/umi' import { createUmi } from '@metaplex-foundation/umi-bundle-defaults' import { createWeb3JsEddsa } from '@metaplex-foundation/umi-eddsa-web3js' +import { toWeb3JsInstruction, toWeb3JsPublicKey } from '@metaplex-foundation/umi-web3js-adapters' +import { AddressLookupTableAccount, Connection } from '@solana/web3.js' +import { getSimulationComputeUnits } from '@solana-developers/helpers' import bs58 from 'bs58' +import { formatEid } from '@layerzerolabs/devtools' import { EndpointId, endpointIdToNetwork } from '@layerzerolabs/lz-definitions' import { OftPDA } from '@layerzerolabs/oft-v2-solana-sdk' import { createSolanaConnectionFactory } from '../common/utils' +import getFee from '../utils/getFee' + +const LOOKUP_TABLE_ADDRESS: Partial> = { + [EndpointId.SOLANA_V2_MAINNET]: publicKey('AokBxha6VMLLgf97B5VYHEtqztamWmYERBmmFvjuTzJB'), + [EndpointId.SOLANA_V2_TESTNET]: publicKey('9thqPdbR27A1yLWw2spwJLySemiGMXxPnEvfmXVk4KuK'), +} const getFromEnv = (key: string): string => { const value = process.env[key] @@ -107,3 +135,79 @@ export const getLayerZeroScanLink = (hash: string, isTestnet = false) => export const getExplorerTxLink = (hash: string, isTestnet = false) => `https://explorer.solana.com/tx/${hash}?cluster=${isTestnet ? 'devnet' : 'mainnet-beta'}` + +export const getAddressLookupTable = async (connection: Connection, umi: Umi, fromEid: EndpointId) => { + // Lookup Table Address and Priority Fee Calculation + const lookupTableAddress = LOOKUP_TABLE_ADDRESS[fromEid] + assert(lookupTableAddress != null, `No lookup table found for ${formatEid(fromEid)}`) + const addressLookupTableInput: AddressLookupTableInput = await fetchAddressLookupTable(umi, lookupTableAddress) + if (!addressLookupTableInput) { + throw new Error(`No address lookup table found for ${lookupTableAddress}`) + } + const { value: lookupTableAccount } = await connection.getAddressLookupTable(toWeb3JsPublicKey(lookupTableAddress)) + if (!lookupTableAccount) { + throw new Error(`No address lookup table account found for ${lookupTableAddress}`) + } + return { + lookupTableAddress, + addressLookupTableInput, + lookupTableAccount, + } +} + +export const getComputeUnitPriceAndLimit = async ( + connection: Connection, + ixs: Instruction[], + wallet: KeypairSigner, + lookupTableAccount: AddressLookupTableAccount +) => { + const { averageFeeExcludingZeros } = await getFee(connection) + const priorityFee = Math.round(averageFeeExcludingZeros) + const computeUnitPrice = BigInt(priorityFee) + + const computeUnits = await getSimulationComputeUnits( + connection, + ixs.map((ix) => toWeb3JsInstruction(ix)), + toWeb3JsPublicKey(wallet.publicKey), + [lookupTableAccount] + ) + + if (!computeUnits) { + throw new Error('Unable to compute units') + } + + return { + computeUnitPrice, + computeUnits, + } +} + +export const addComputeUnitInstructions = async ( + connection: Connection, + umi: Umi, + eid: EndpointId, + txBuilder: TransactionBuilder, + umiWalletSigner: KeypairSigner, + computeUnitPriceScaleFactor: number +) => { + const computeUnitLimitScaleFactor = 1.1 // hardcoded to 1.1 as the estimations are not perfect and can fall slightly short of the actual CU usage on-chain + const { addressLookupTableInput, lookupTableAccount } = await getAddressLookupTable(connection, umi, eid) + const { computeUnitPrice, computeUnits } = await getComputeUnitPriceAndLimit( + connection, + txBuilder.getInstructions(), + umiWalletSigner, + lookupTableAccount + ) + // Since transaction builders are immutable, we must be careful to always assign the result of the add and prepend + // methods to a new variable. + const newTxBuilder = transactionBuilder() + .add( + setComputeUnitPrice(umi, { + microLamports: computeUnitPrice * BigInt(Math.floor(computeUnitPriceScaleFactor)), + }) + ) + .add(setComputeUnitLimit(umi, { units: computeUnits * computeUnitLimitScaleFactor })) + .setAddressLookupTables([addressLookupTableInput]) + .add(txBuilder) + return newTxBuilder +} diff --git a/examples/oft-solana/tasks/solana/sendOFT.ts b/examples/oft-solana/tasks/solana/sendOFT.ts index 0631148a8..6270c2080 100644 --- a/examples/oft-solana/tasks/solana/sendOFT.ts +++ b/examples/oft-solana/tasks/solana/sendOFT.ts @@ -1,19 +1,16 @@ -import assert from 'assert' - -import { fetchAddressLookupTable, findAssociatedTokenPda, setComputeUnitLimit } from '@metaplex-foundation/mpl-toolbox' -import { AddressLookupTableInput, PublicKey, TransactionBuilder, publicKey } from '@metaplex-foundation/umi' +import { findAssociatedTokenPda } from '@metaplex-foundation/mpl-toolbox' +import { publicKey, transactionBuilder } from '@metaplex-foundation/umi' import { fromWeb3JsPublicKey } from '@metaplex-foundation/umi-web3js-adapters' import { TOKEN_PROGRAM_ID } from '@solana/spl-token' import bs58 from 'bs58' import { task } from 'hardhat/config' -import { formatEid } from '@layerzerolabs/devtools' import { types } from '@layerzerolabs/devtools-evm-hardhat' import { EndpointId } from '@layerzerolabs/lz-definitions' import { addressToBytes32 } from '@layerzerolabs/lz-v2-utilities' import { oft } from '@layerzerolabs/oft-v2-solana-sdk' -import { deriveConnection, getExplorerTxLink, getLayerZeroScanLink } from './index' +import { addComputeUnitInstructions, deriveConnection, getExplorerTxLink, getLayerZeroScanLink } from './index' interface Args { amount: number @@ -24,11 +21,7 @@ interface Args { mint: string escrow: string tokenProgram: string -} - -const LOOKUP_TABLE_ADDRESS: Partial> = { - [EndpointId.SOLANA_V2_MAINNET]: publicKey('AokBxha6VMLLgf97B5VYHEtqztamWmYERBmmFvjuTzJB'), - [EndpointId.SOLANA_V2_TESTNET]: publicKey('9thqPdbR27A1yLWw2spwJLySemiGMXxPnEvfmXVk4KuK'), + computeUnitPriceScaleFactor: number } // Define a Hardhat task for sending OFT from Solana @@ -41,6 +34,7 @@ task('lz:oft:solana:send', 'Send tokens from Solana to a target EVM chain') .addParam('programId', 'The OFT program ID', undefined, types.string) .addParam('escrow', 'The OFT escrow public key', undefined, types.string) .addParam('tokenProgram', 'The Token Program public key', TOKEN_PROGRAM_ID.toBase58(), types.string, true) + .addParam('computeUnitPriceScaleFactor', 'The compute unit price scale factor', 4, types.float, true) .setAction( async ({ amount, @@ -51,8 +45,9 @@ task('lz:oft:solana:send', 'Send tokens from Solana to a target EVM chain') programId: programIdStr, escrow: escrowStr, tokenProgram: tokenProgramStr, + computeUnitPriceScaleFactor, }: Args) => { - const { umi, umiWalletSigner } = await deriveConnection(fromEid) + const { connection, umi, umiWalletSigner } = await deriveConnection(fromEid) const oftProgramId = publicKey(programIdStr) const mint = publicKey(mintStr) @@ -116,18 +111,19 @@ task('lz:oft:solana:send', 'Send tokens from Solana to a target EVM chain') token: tokenProgramId, } ) - const lookupTableAddress = LOOKUP_TABLE_ADDRESS[fromEid] - assert(lookupTableAddress != null, `No lookup table found for ${formatEid(fromEid)}`) - const addressLookupTableInput: AddressLookupTableInput = await fetchAddressLookupTable( + + let txBuilder = transactionBuilder().add([ix]) + txBuilder = await addComputeUnitInstructions( + connection, umi, - lookupTableAddress + fromEid, + txBuilder, + umiWalletSigner, + computeUnitPriceScaleFactor ) - - const { signature } = await new TransactionBuilder([ix]) - .add(setComputeUnitLimit(umi, { units: 500_000 })) - .setAddressLookupTables([addressLookupTableInput]) - .sendAndConfirm(umi) + const { signature } = await txBuilder.sendAndConfirm(umi) const transactionSignatureBase58 = bs58.encode(signature) + console.log(`✅ Sent ${amount} token(s) to destination EID: ${toEid}!`) const isTestnet = fromEid == EndpointId.SOLANA_V2_TESTNET console.log( diff --git a/examples/oft-solana/tasks/solana/setAuthority.ts b/examples/oft-solana/tasks/solana/setAuthority.ts index 8682c252a..e9f6a1de6 100644 --- a/examples/oft-solana/tasks/solana/setAuthority.ts +++ b/examples/oft-solana/tasks/solana/setAuthority.ts @@ -11,7 +11,7 @@ import { OftPDA } from '@layerzerolabs/oft-v2-solana-sdk' import { checkMultisigSigners, createMintAuthorityMultisig } from './multisig' -import { deriveConnection, getExplorerTxLink } from './index' +import { addComputeUnitInstructions, deriveConnection, getExplorerTxLink } from './index' interface SetAuthorityTaskArgs { /** @@ -50,6 +50,8 @@ interface SetAuthorityTaskArgs { * using this flag, as it is not reversible. */ onlyOftStore: boolean + + computeUnitPriceScaleFactor: number } /** @@ -104,6 +106,7 @@ task('lz:oft:solana:setauthority', 'Create a new Mint Authority SPL multisig and TOKEN_PROGRAM_ID.toBase58(), devtoolsTypes.string ) + .addParam('computeUnitPriceScaleFactor', 'The compute unit price scale factor', 4, devtoolsTypes.float, true) .setAction( async ({ eid, @@ -113,6 +116,7 @@ task('lz:oft:solana:setauthority', 'Create a new Mint Authority SPL multisig and tokenProgram: tokenProgramStr, additionalMinters: additionalMintersAsStrings, onlyOftStore, + computeUnitPriceScaleFactor, }: SetAuthorityTaskArgs) => { const { connection, umi, umiWalletKeyPair, umiWalletSigner } = await deriveConnection(eid) const oftStorePda = getOftStore(programIdStr, escrowStr) @@ -176,13 +180,20 @@ task('lz:oft:solana:setauthority', 'Create a new Mint Authority SPL multisig and })) as unknown as AccountMeta[], data: ix.data, } - const { signature } = await transactionBuilder() - .add({ - instruction: umiInstruction, - signers: [umiWalletSigner], // Include all required signers here - bytesCreatedOnChain: 0, - }) - .sendAndConfirm(umi) + let txBuilder = transactionBuilder().add({ + instruction: umiInstruction, + signers: [umiWalletSigner], // Include all required signers here + bytesCreatedOnChain: 0, + }) + txBuilder = await addComputeUnitInstructions( + connection, + umi, + eid, + txBuilder, + umiWalletSigner, + computeUnitPriceScaleFactor + ) + const { signature } = await txBuilder.sendAndConfirm(umi) console.log( `SetAuthorityTx(${getAuthorityTypeString(authorityType)}): ${getExplorerTxLink(bs58.encode(signature), eid == EndpointId.SOLANA_V2_TESTNET)}` ) diff --git a/examples/oft-solana/tasks/utils/getFee.ts b/examples/oft-solana/tasks/utils/getFee.ts index efc927b41..ef83ec4cd 100644 --- a/examples/oft-solana/tasks/utils/getFee.ts +++ b/examples/oft-solana/tasks/utils/getFee.ts @@ -12,14 +12,14 @@ interface Config { const getPrioritizationFees = async ( connection: Connection, - programId: string + programId?: string // TODO: change to array of addresses / public keys to match lockedWritableAccounts' type ): Promise<{ averageFeeIncludingZeros: number averageFeeExcludingZeros: number medianFee: number }> => { try { - const publicKey = new PublicKey(programId) // the account that will be written to + const publicKey = new PublicKey(programId || PublicKey.default) // the account that will be written to const config: Config = { lockedWritableAccounts: [publicKey],