diff --git a/libs/coin-modules/coin-solana/src/api/cached.ts b/libs/coin-modules/coin-solana/src/api/cached.ts index 44f7e5b99f10..828471f14d2d 100644 --- a/libs/coin-modules/coin-solana/src/api/cached.ts +++ b/libs/coin-modules/coin-solana/src/api/cached.ts @@ -1,4 +1,5 @@ import { makeLRUCache, minutes, seconds } from "@ledgerhq/live-network/cache"; +import { PublicKey, TransactionInstruction, TransactionMessage } from "@solana/web3.js"; import hash from "object-hash"; import { ChainAPI } from "./chain"; @@ -8,6 +9,17 @@ const cacheKeyAssocTokenAccAddress = (owner: string, mint: string) => `${owner}: const cacheKeyMinimumBalanceForRentExemption = (dataLengt: number) => dataLengt.toString(); const cacheKeyTransactions = (signatures: string[]) => hash([...signatures].sort()); +const cacheKeyInstructions = (ixs: TransactionInstruction[], payer: PublicKey) => { + return hash( + new TransactionMessage({ + instructions: ixs, + payerKey: payer, + recentBlockhash: payer.toString(), + }) + .compileToLegacyMessage() + .serialize(), + ); +}; const cacheKeyByArgs = (...args: any[]) => hash(args); @@ -80,6 +92,12 @@ export function cached(api: ChainAPI): ChainAPI { seconds(30), ), + getSimulationComputeUnits: makeLRUCache( + api.getSimulationComputeUnits, + cacheKeyInstructions, + seconds(30), + ), + config: api.config, }; } diff --git a/libs/coin-modules/coin-solana/src/api/chain/index.ts b/libs/coin-modules/coin-solana/src/api/chain/index.ts index 796d9ec27df4..7ad46f725e2b 100644 --- a/libs/coin-modules/coin-solana/src/api/chain/index.ts +++ b/libs/coin-modules/coin-solana/src/api/chain/index.ts @@ -12,6 +12,10 @@ import { SignaturesForAddressOptions, StakeProgram, GetRecentPrioritizationFeesConfig, + TransactionInstruction, + ComputeBudgetProgram, + VersionedTransaction, + TransactionMessage, } from "@solana/web3.js"; import { makeLRUCache, minutes } from "@ledgerhq/live-network/cache"; import { getEnv } from "@ledgerhq/live-env"; @@ -73,9 +77,14 @@ export type ChainAPI = Readonly<{ getEpochInfo: () => ReturnType; getRecentPrioritizationFees: ( - config?: GetRecentPrioritizationFeesConfig, + accounts: string[], ) => ReturnType; + getSimulationComputeUnits: ( + instructions: Array, + payer: PublicKey, + ) => Promise; + config: Config; }>; @@ -212,8 +221,37 @@ export function getChainAPI( getEpochInfo: () => connection().getEpochInfo().catch(remapErrors), - getRecentPrioritizationFees: (config?: GetRecentPrioritizationFeesConfig) => { - return connection().getRecentPrioritizationFees(config).catch(remapErrors); + getRecentPrioritizationFees: (accounts: string[]) => { + return connection() + .getRecentPrioritizationFees({ + lockedWritableAccounts: accounts.map(acc => new PublicKey(acc)), + }) + .catch(remapErrors); + }, + + getSimulationComputeUnits: async (instructions, payer) => { + // https://solana.com/developers/guides/advanced/how-to-request-optimal-compute + const testInstructions = [ + // Set an arbitrarily high number in simulation + // so we can be sure the transaction will succeed + // and get the real compute units used + ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }), + ...instructions, + ]; + const testTransaction = new VersionedTransaction( + new TransactionMessage({ + instructions: testInstructions, + payerKey: payer, + // RecentBlockhash can by any public key during simulation + // since 'replaceRecentBlockhash' is set to 'true' below + recentBlockhash: PublicKey.default.toString(), + }).compileToV0Message(), + ); + const rpcResponse = await connection().simulateTransaction(testTransaction, { + replaceRecentBlockhash: true, + sigVerify: false, + }); + return rpcResponse.value.err ? null : rpcResponse.value.unitsConsumed || null; }, config, diff --git a/libs/coin-modules/coin-solana/src/api/chain/web3.ts b/libs/coin-modules/coin-solana/src/api/chain/web3.ts index 1d6cd4b35968..fff6da9b0e03 100644 --- a/libs/coin-modules/coin-solana/src/api/chain/web3.ts +++ b/libs/coin-modules/coin-solana/src/api/chain/web3.ts @@ -14,7 +14,7 @@ import { ComputeBudgetProgram, } from "@solana/web3.js"; import chunk from "lodash/chunk"; -import uniqBy from "lodash/uniqBy"; +import uniq from "lodash/uniq"; import { ChainAPI } from "."; import { Awaited } from "../../logic"; import { @@ -153,7 +153,7 @@ export const buildTransferInstructions = async ( instructions.push(memoIx); } - return appendMaybePriorityFeeInstruction(api, [fromPublicKey, toPublicKey], instructions); + return appendMaybePriorityFeeInstructions(api, instructions, fromPublicKey); }; export const buildTokenTransferInstructions = async ( @@ -272,18 +272,31 @@ export async function getStakeAccountAddressWithSeed({ return pubkey.toBase58(); } -export async function getPriorityFee(api: ChainAPI, accounts: PublicKey[]): Promise { - const uniqAccs = uniqBy(accounts, acc => acc.toBase58()); - const recentFees = await api.getRecentPrioritizationFees({ - lockedWritableAccounts: uniqAccs, - }); - +export async function getPriorityFee(api: ChainAPI, accounts: string[]): Promise { + const recentFees = await api.getRecentPrioritizationFees(uniq(accounts)); return median(recentFees.map(item => item.prioritizationFee)); } +export async function appendMaybePriorityFeeInstructions( + api: ChainAPI, + ixs: TransactionInstruction[], + payer: PublicKey, +): Promise { + const instructions = [...ixs]; + const writableAccs = instructions + .map(ix => ix.keys.filter(acc => acc.isWritable).map(acc => acc.pubkey.toBase58())) + .flat(); + const priorityFeeIx = await buildMaybePriorityFeeInstruction(api, writableAccs); + if (priorityFeeIx) instructions.unshift(priorityFeeIx); + const computeUnitsIx = await buildComputeUnitInstruction(api, instructions, payer); + + if (computeUnitsIx) instructions.unshift(computeUnitsIx); + return instructions; +} + export async function buildMaybePriorityFeeInstruction( api: ChainAPI, - accounts: PublicKey[], + accounts: string[], ): Promise { const priorityFee = await getPriorityFee(api, accounts); if (priorityFee === 0) return null; @@ -292,14 +305,16 @@ export async function buildMaybePriorityFeeInstruction( microLamports: priorityFee, }); } - -export async function appendMaybePriorityFeeInstruction( +export async function buildComputeUnitInstruction( api: ChainAPI, - accounts: PublicKey[], ixs: TransactionInstruction[], -): Promise { - const priorityFeeIx = await buildMaybePriorityFeeInstruction(api, accounts); - return priorityFeeIx ? [priorityFeeIx, ...ixs] : ixs; + payer: PublicKey, +): Promise { + const computeUnits = await api.getSimulationComputeUnits(ixs, payer); + // adding 10% more CPU to make sure it will work + return computeUnits + ? ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits * 0.1 + computeUnits }) + : null; } export function buildCreateAssociatedTokenAccountInstruction({ @@ -336,7 +351,7 @@ export async function buildStakeDelegateInstructions( votePubkey: voteAcc, }); - return appendMaybePriorityFeeInstruction(api, [withdrawAuthority, stakeAcc], tx.instructions); + return appendMaybePriorityFeeInstructions(api, tx.instructions, withdrawAuthority); } export async function buildStakeUndelegateInstructions( @@ -350,7 +365,7 @@ export async function buildStakeUndelegateInstructions( stakePubkey: stakeAcc, }); - return appendMaybePriorityFeeInstruction(api, [withdrawAuthority, stakeAcc], tx.instructions); + return appendMaybePriorityFeeInstructions(api, tx.instructions, withdrawAuthority); } export async function buildStakeWithdrawInstructions( @@ -367,7 +382,7 @@ export async function buildStakeWithdrawInstructions( toPubkey: recipient, }); - return appendMaybePriorityFeeInstruction(api, [withdrawAuthority, stakeAcc], tx.instructions); + return appendMaybePriorityFeeInstructions(api, tx.instructions, withdrawAuthority); } export async function buildStakeSplitInstructions( @@ -385,7 +400,7 @@ export async function buildStakeSplitInstructions( basePubkey: basePk, seed, }); - return appendMaybePriorityFeeInstruction(api, [basePk, stakePk], splitIx.instructions); + return appendMaybePriorityFeeInstructions(api, splitIx.instructions, basePk); } export async function buildStakeCreateAccountInstructions( @@ -421,5 +436,5 @@ export async function buildStakeCreateAccountInstructions( votePubkey: new PublicKey(delegate.voteAccAddress), }), ); - return appendMaybePriorityFeeInstruction(api, [fromPubkey, stakePubkey], tx.instructions); + return appendMaybePriorityFeeInstructions(api, tx.instructions, fromPubkey); } diff --git a/libs/coin-modules/coin-solana/src/bridge.integration.test.ts b/libs/coin-modules/coin-solana/src/bridge.integration.test.ts index fbda3455567f..124ae2f7bdfb 100644 --- a/libs/coin-modules/coin-solana/src/bridge.integration.test.ts +++ b/libs/coin-modules/coin-solana/src/bridge.integration.test.ts @@ -964,6 +964,7 @@ const baseAPI = { }, ]); }, + getSimulationComputeUnits: (_ixs: any[], _payer: any) => Promise.resolve(1000), } as ChainAPI; type StakeTestSpec = { diff --git a/libs/coin-modules/coin-solana/src/tx-fees.ts b/libs/coin-modules/coin-solana/src/tx-fees.ts index 4a05372c9f44..b25bdbfb363f 100644 --- a/libs/coin-modules/coin-solana/src/tx-fees.ts +++ b/libs/coin-modules/coin-solana/src/tx-fees.ts @@ -2,9 +2,10 @@ import { ChainAPI } from "./api"; import { buildTransactionWithAPI } from "./buildTransaction"; import createTransaction from "./createTransaction"; import { Transaction, TransactionModel } from "./types"; -import { assertUnreachable } from "./utils"; +import { LEDGER_VALIDATOR, assertUnreachable } from "./utils"; import { VersionedTransaction as OnChainTransaction } from "@solana/web3.js"; import { log } from "@ledgerhq/logs"; +import { getStakeAccountAddressWithSeed } from "./api/chain/web3"; const DEFAULT_TX_FEE = 5000; @@ -13,7 +14,7 @@ export async function estimateTxFee( address: string, kind: TransactionModel["kind"], ) { - const tx = createDummyTx(address, kind); + const tx = await createDummyTx(address, kind); const [onChainTx] = await buildTransactionWithAPI(address, tx, api); let fee = await api.getFeeForMessage(onChainTx.message); @@ -76,7 +77,7 @@ const createDummyTransferTx = (address: string): Transaction => { }; }; -const createDummyStakeCreateAccountTx = (address: string): Transaction => { +const createDummyStakeCreateAccountTx = async (address: string): Promise => { return { ...createTransaction({} as any), model: { @@ -87,12 +88,12 @@ const createDummyStakeCreateAccountTx = (address: string): Transaction => { kind: "stake.createAccount", amount: 0, delegate: { - voteAccAddress: randomAddresses[0], + voteAccAddress: LEDGER_VALIDATOR.voteAccount, }, fromAccAddress: address, seed: "", - stakeAccAddress: randomAddresses[1], - stakeAccRentExemptAmount: 0, + stakeAccAddress: await getStakeAccountAddressWithSeed({ fromAddress: address, seed: "" }), + stakeAccRentExemptAmount: 2282880, }, ...commandDescriptorCommons, }, @@ -149,7 +150,7 @@ const createDummyStakeWithdrawTx = (address: string): Transaction => { amount: 0, authorizedAccAddr: address, stakeAccAddr: randomAddresses[0], - toAccAddr: randomAddresses[1], + toAccAddr: address, }, ...commandDescriptorCommons, },