Skip to content

Commit

Permalink
fix: spl priority fees and transaction confirmation with blockHeight
Browse files Browse the repository at this point in the history
  • Loading branch information
Justkant committed Nov 18, 2024
1 parent 8f1f0e3 commit 290295f
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 50 deletions.
75 changes: 57 additions & 18 deletions libs/coin-modules/coin-solana/src/api/chain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import {
FetchMiddleware,
VersionedMessage,
PublicKey,
sendAndConfirmRawTransaction,
SignaturesForAddressOptions,
StakeProgram,
TransactionInstruction,
ComputeBudgetProgram,
VersionedTransaction,
TransactionMessage,
SendTransactionError,
BlockhashWithExpiryBlockHeight,
Commitment,
GetLatestBlockhashConfig,
} from "@solana/web3.js";
import { makeLRUCache, minutes } from "@ledgerhq/live-network/cache";
import { getEnv } from "@ledgerhq/live-env";
Expand All @@ -23,6 +26,7 @@ import { Awaited } from "../../logic";
import { getStakeActivation } from "./stake-activation";

export const LATEST_BLOCKHASH_MOCK = "EEbZs6DmDyDjucyYbo3LwVJU7pQYuVopYcYTSEZXskW3";
export const LAST_VALID_BLOCK_HEIGHT_MOCK = 280064048;

export type Config = {
readonly endpoint: string;
Expand All @@ -31,7 +35,9 @@ export type Config = {
export type ChainAPI = Readonly<{
getBalance: (address: string) => Promise<number>;

getLatestBlockhash: () => Promise<string>;
getLatestBlockhash: (
commitmentOrConfig?: Commitment | GetLatestBlockhashConfig,
) => Promise<BlockhashWithExpiryBlockHeight>;

getFeeForMessage: (message: VersionedMessage) => Promise<number | null>;

Expand Down Expand Up @@ -66,7 +72,10 @@ export type ChainAPI = Readonly<{
address: string,
) => Promise<Awaited<ReturnType<Connection["getParsedAccountInfo"]>>["value"]>;

sendRawTransaction: (buffer: Buffer) => ReturnType<Connection["sendRawTransaction"]>;
sendRawTransaction: (
buffer: Buffer,
recentBlockhash?: BlockhashWithExpiryBlockHeight,
) => ReturnType<Connection["sendRawTransaction"]>;

findAssocTokenAccAddress: (owner: string, mint: string) => Promise<string>;

Expand Down Expand Up @@ -105,23 +114,24 @@ export function getChainAPI(
fetch(url, options);
};

let _connection: Connection;
const connection = () => {
return new Connection(config.endpoint, {
...(fetchMiddleware ? { fetchMiddleware } : {}),
commitment: "finalized",
confirmTransactionInitialTimeout: getEnv("SOLANA_TX_CONFIRMATION_TIMEOUT") || 0,
});
if (!_connection) {
_connection = new Connection(config.endpoint, {
...(fetchMiddleware ? { fetchMiddleware } : {}),
commitment: "finalized",
confirmTransactionInitialTimeout: getEnv("SOLANA_TX_CONFIRMATION_TIMEOUT") || 0,
});
}
return _connection;
};

return {
getBalance: (address: string) =>
connection().getBalance(new PublicKey(address)).catch(remapErrors),

getLatestBlockhash: () =>
connection()
.getLatestBlockhash()
.then(r => r.blockhash)
.catch(remapErrors),
getLatestBlockhash: (commitmentOrConfig?: Commitment | GetLatestBlockhashConfig) =>
connection().getLatestBlockhash(commitmentOrConfig).catch(remapErrors),

getFeeForMessage: (msg: VersionedMessage) =>
connection()
Expand Down Expand Up @@ -201,10 +211,39 @@ export function getChainAPI(
.then(r => r.value)
.catch(remapErrors),

sendRawTransaction: (buffer: Buffer) => {
return sendAndConfirmRawTransaction(connection(), buffer, {
commitment: "confirmed",
}).catch(remapErrors);
sendRawTransaction: (buffer: Buffer, recentBlockhash?: BlockhashWithExpiryBlockHeight) => {
return (async () => {
const conn = connection();

const commitment = "confirmed";

const signature = await conn.sendRawTransaction(buffer, {
preflightCommitment: commitment,
});

if (!recentBlockhash) {
recentBlockhash = await conn.getLatestBlockhash(commitment);
}
const { value: status } = await conn.confirmTransaction(
{
blockhash: recentBlockhash.blockhash,
lastValidBlockHeight: recentBlockhash.lastValidBlockHeight,
signature,
},
commitment,
);
if (status.err) {
if (signature != null) {
throw new SendTransactionError({
action: "send",
signature: signature,
transactionMessage: `Status: (${JSON.stringify(status)})`,
});
}
throw new Error(`Raw transaction ${signature} failed (${JSON.stringify(status)})`);
}
return signature;
})().catch(remapErrors);
},

findAssocTokenAccAddress: (owner: string, mint: string) => {
Expand Down Expand Up @@ -245,7 +284,7 @@ export function getChainAPI(
// RecentBlockhash can by any public key during simulation
// since 'replaceRecentBlockhash' is set to 'true' below
recentBlockhash: PublicKey.default.toString(),
}).compileToV0Message(),
}).compileToLegacyMessage(),
);
const rpcResponse = await connection().simulateTransaction(testTransaction, {
replaceRecentBlockhash: true,
Expand Down
21 changes: 11 additions & 10 deletions libs/coin-modules/coin-solana/src/api/chain/web3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export const buildTokenTransferInstructions = async (
);
}

return instructions;
return appendMaybePriorityFeeInstructions(api, instructions, ownerPubkey);
};

export async function findAssociatedTokenAccountPubkey(
Expand Down Expand Up @@ -288,10 +288,12 @@ export async function appendMaybePriorityFeeInstructions(
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);
const [priorityFeeIx, computeUnitsIx] = await Promise.all([
buildMaybePriorityFeeInstruction(api, writableAccs),
buildComputeUnitInstruction(api, instructions, payer),
]);

if (priorityFeeIx) instructions.unshift(priorityFeeIx);
if (computeUnitsIx) instructions.unshift(computeUnitsIx);
return instructions;
}
Expand Down Expand Up @@ -319,11 +321,10 @@ export async function buildComputeUnitInstruction(
: null;
}

export function buildCreateAssociatedTokenAccountInstruction({
mint,
owner,
associatedTokenAccountAddress,
}: TokenCreateATACommand): TransactionInstruction[] {
export function buildCreateAssociatedTokenAccountInstruction(
api: ChainAPI,
{ mint, owner, associatedTokenAccountAddress }: TokenCreateATACommand,
): Promise<TransactionInstruction[]> {
const ownerPubKey = new PublicKey(owner);
const mintPubkey = new PublicKey(mint);
const associatedTokenAccPubkey = new PublicKey(associatedTokenAccountAddress);
Expand All @@ -337,7 +338,7 @@ export function buildCreateAssociatedTokenAccountInstruction({
),
];

return instructions;
return appendMaybePriorityFeeInstructions(api, instructions, ownerPubKey);
}

export async function buildStakeDelegateInstructions(
Expand Down
8 changes: 6 additions & 2 deletions libs/coin-modules/coin-solana/src/bridge.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import createTransaction from "./createTransaction";
import { compact } from "lodash/fp";
import { SYSTEM_ACCOUNT_RENT_EXEMPT, assertUnreachable } from "./utils";
import { getEnv } from "@ledgerhq/live-env";
import { ChainAPI, LATEST_BLOCKHASH_MOCK } from "./api";
import { ChainAPI, LAST_VALID_BLOCK_HEIGHT_MOCK, LATEST_BLOCKHASH_MOCK } from "./api";
import {
SolanaStakeAccountIsNotDelegatable,
SolanaStakeAccountValidatorIsUnchangeable,
Expand Down Expand Up @@ -964,7 +964,11 @@ const baseTx = {
} as Transaction;

const baseAPI = {
getLatestBlockhash: () => Promise.resolve(LATEST_BLOCKHASH_MOCK),
getLatestBlockhash: () =>
Promise.resolve({
blockhash: LATEST_BLOCKHASH_MOCK,
lastValidBlockHeight: LAST_VALID_BLOCK_HEIGHT_MOCK,
}),
getFeeForMessage: (_msg: unknown) => Promise.resolve(testOnChainData.fees.lamportsPerSignature),
getRecentPrioritizationFees: (_: string[]) => {
return Promise.resolve([
Expand Down
8 changes: 6 additions & 2 deletions libs/coin-modules/coin-solana/src/broadcast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { patchOperationWithHash } from "@ledgerhq/coin-framework/operation";
import type { Account, Operation, SignedOperation } from "@ledgerhq/types-live";
import { ChainAPI } from "./api";
import { SolanaTxConfirmationTimeout, SolanaTxSimulationFailedWhilePendingOp } from "./errors";
import { BlockhashWithExpiryBlockHeight } from "@solana/web3.js";

export const broadcastWithAPI = async (
{
Expand All @@ -14,10 +15,13 @@ export const broadcastWithAPI = async (
},
api: ChainAPI,
): Promise<Operation> => {
const { signature, operation } = signedOperation;
const { signature, operation, rawData } = signedOperation;

try {
const txSignature = await api.sendRawTransaction(Buffer.from(signature, "hex"));
const txSignature = await api.sendRawTransaction(
Buffer.from(signature, "hex"),
rawData?.recentBlockhash as BlockhashWithExpiryBlockHeight,
);
return patchOperationWithHash(operation, txSignature);
} catch (e: any) {
// heuristics to make some errors more user friendly
Expand Down
21 changes: 15 additions & 6 deletions libs/coin-modules/coin-solana/src/buildTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,39 @@ import {
VersionedTransaction as OnChainTransaction,
TransactionInstruction,
TransactionMessage,
BlockhashWithExpiryBlockHeight,
} from "@solana/web3.js";
import { ChainAPI } from "./api";

export const buildTransactionWithAPI = async (
address: string,
transaction: Transaction,
api: ChainAPI,
): Promise<readonly [OnChainTransaction, (signature: Buffer) => OnChainTransaction]> => {
const instructions = await buildInstructions(api, transaction);

const recentBlockhash = await api.getLatestBlockhash();
): Promise<
readonly [
OnChainTransaction,
BlockhashWithExpiryBlockHeight,
(signature: Buffer) => OnChainTransaction,
]
> => {
const [instructions, recentBlockhash] = await Promise.all([
buildInstructions(api, transaction),
api.getLatestBlockhash(),
]);

const feePayer = new PublicKey(address);

const tm = new TransactionMessage({
payerKey: feePayer,
recentBlockhash,
recentBlockhash: recentBlockhash.blockhash,
instructions,
});

const tx = new OnChainTransaction(tm.compileToLegacyMessage());

return [
tx,
recentBlockhash,
(signature: Buffer) => {
tx.addSignature(new PublicKey(address), signature);
return tx;
Expand Down Expand Up @@ -70,7 +79,7 @@ async function buildInstructionsForCommand(
case "token.transfer":
return buildTokenTransferInstructions(api, command);
case "token.createATA":
return buildCreateAssociatedTokenAccountInstruction(command);
return buildCreateAssociatedTokenAccountInstruction(api, command);
case "stake.createAccount":
return buildStakeCreateAccountInstructions(api, command);
case "stake.delegate":
Expand Down
8 changes: 4 additions & 4 deletions libs/coin-modules/coin-solana/src/logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export function decodeAccountIdWithTokenAccountAddress(accountIdWithTokenAccount
};
}

export function toTokenId(mint: string): string {
return `solana/spl/${mint}`;
export function toTokenId(currencyId: string, mint: string): string {
return `${currencyId}/spl/${mint}`;
}

export function toTokenMint(tokenId: string): string {
Expand All @@ -48,8 +48,8 @@ export function toSubAccMint(subAcc: TokenAccount): string {
return toTokenMint(subAcc.token.id);
}

export function tokenIsListedOnLedger(mint: string): boolean {
return findTokenById(toTokenId(mint))?.type === "TokenCurrency";
export function tokenIsListedOnLedger(currencyId: string, mint: string): boolean {
return findTokenById(toTokenId(currencyId, mint))?.type === "TokenCurrency";
}

export function stakeActions(stake: SolanaStake): StakeAction[] {
Expand Down
5 changes: 4 additions & 1 deletion libs/coin-modules/coin-solana/src/signOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const buildSignOperation =
({ account, deviceId, transaction }) =>
new Observable(subscriber => {
const main = async () => {
const [tx, signOnChainTransaction] = await buildTransactionWithAPI(
const [tx, recentBlockhash, signOnChainTransaction] = await buildTransactionWithAPI(
account.freshAddress,
transaction,
await api(),
Expand All @@ -78,6 +78,9 @@ export const buildSignOperation =
signedOperation: {
operation: buildOptimisticOperation(account, transaction),
signature: Buffer.from(signedTx.serialize()).toString("hex"),
rawData: {
recentBlockhash,
},
},
});
};
Expand Down
7 changes: 5 additions & 2 deletions libs/coin-modules/coin-solana/src/synchronization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const getAccountShapeWithAPI = async (
const nextSubAccs: TokenAccount[] = [];

for (const [mint, accs] of onChainTokenAccsByMint.entries()) {
if (!tokenIsListedOnLedger(mint)) {
if (!tokenIsListedOnLedger(currency.id, mint)) {
continue;
}

Expand All @@ -114,6 +114,7 @@ export const getAccountShapeWithAPI = async (
const nextSubAcc =
subAcc === undefined
? newSubAcc({
currencyId: currency.id,
mainAccountId,
assocTokenAcc,
txs,
Expand Down Expand Up @@ -245,10 +246,12 @@ export const getAccountShapeWithAPI = async (
};

function newSubAcc({
currencyId,
mainAccountId,
assocTokenAcc,
txs,
}: {
currencyId: string;
mainAccountId: string;
assocTokenAcc: OnChainTokenAccount;
txs: TransactionDescriptor[];
Expand All @@ -257,7 +260,7 @@ function newSubAcc({

const creationDate = new Date((firstTx.info.blockTime ?? Date.now() / 1000) * 1000);

const tokenId = toTokenId(assocTokenAcc.info.mint.toBase58());
const tokenId = toTokenId(currencyId, assocTokenAcc.info.mint.toBase58());
const tokenCurrency = getTokenById(tokenId);

const accosTokenAccPubkey = assocTokenAcc.onChainAcc.pubkey;
Expand Down
6 changes: 3 additions & 3 deletions libs/coin-modules/coin-solana/src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ function formatCommand(mainAccount: Account, tx: Transaction, command: Command)
case "token.transfer":
return formatTokenTransfer(mainAccount, tx, command);
case "token.createATA":
return formatCreateATA(command);
return formatCreateATA(mainAccount, command);
case "stake.createAccount":
return formatStakeCreateAccount(mainAccount, tx, command);
case "stake.delegate":
Expand Down Expand Up @@ -147,8 +147,8 @@ function formatTokenTransfer(mainAccount: Account, tx: Transaction, command: Tok
return "\n" + str;
}

function formatCreateATA(command: TokenCreateATACommand) {
const token = getTokenById(toTokenId(command.mint));
function formatCreateATA(mainAccount: Account, command: TokenCreateATACommand) {
const token = getTokenById(toTokenId(mainAccount.currency.id, command.mint));
const str = [` OPT IN TOKEN: ${token.ticker}`].filter(Boolean).join("\n");
return "\n" + str;
}
Expand Down
4 changes: 2 additions & 2 deletions libs/coin-modules/coin-solana/src/tx-fees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,9 +223,9 @@ async function waitNextBlockhash(api: ChainAPI, currentBlockhash: string) {
log("info", `sleeping for ${sleepTimeMS} ms, waiting for a new blockhash`);
await sleep(sleepTimeMS);
const blockhash = await api.getLatestBlockhash();
if (blockhash !== currentBlockhash) {
if (blockhash.blockhash !== currentBlockhash) {
log("info", "got a new blockhash");
return blockhash;
return blockhash.blockhash;
}
log("info", "got same blockhash");
}
Expand Down

0 comments on commit 290295f

Please sign in to comment.