Skip to content

Commit

Permalink
feat(LLC): calc and possibly append priority fee to solana instructions
Browse files Browse the repository at this point in the history
  • Loading branch information
mikhd committed May 7, 2024
1 parent cdf3bde commit 37e13fd
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 85 deletions.
6 changes: 6 additions & 0 deletions libs/coin-modules/coin-solana/src/api/cached.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ export function cached(api: ChainAPI): ChainAPI {

getEpochInfo: makeLRUCache(api.getEpochInfo, cacheKeyEmpty, minutes(1)),

getRecentPrioritizationFees: makeLRUCache(
api.getRecentPrioritizationFees,
cacheKeyByArgs,
seconds(30),
),

config: api.config,
};
}
11 changes: 10 additions & 1 deletion libs/coin-modules/coin-solana/src/api/chain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
sendAndConfirmRawTransaction,
SignaturesForAddressOptions,
StakeProgram,
GetRecentPrioritizationFeesConfig,
} from "@solana/web3.js";
import { getEnv } from "@ledgerhq/live-env";
import { Awaited } from "../../logic";
Expand Down Expand Up @@ -70,6 +71,10 @@ export type ChainAPI = Readonly<{

getEpochInfo: () => ReturnType<Connection["getEpochInfo"]>;

getRecentPrioritizationFees: (
config?: GetRecentPrioritizationFeesConfig,
) => ReturnType<Connection["getRecentPrioritizationFees"]>;

config: Config;
}>;

Expand All @@ -86,7 +91,7 @@ export function getChainAPI(
logger === undefined
? undefined
: (url, options, fetch) => {
logger(url, options);
logger(url.toString(), options);
fetch(url, options);
};

Expand Down Expand Up @@ -198,6 +203,10 @@ export function getChainAPI(

getEpochInfo: () => connection().getEpochInfo().catch(remapErrors),

getRecentPrioritizationFees: (config?: GetRecentPrioritizationFeesConfig) => {
return connection().getRecentPrioritizationFees(config).catch(remapErrors);
},

config,
};
}
168 changes: 97 additions & 71 deletions libs/coin-modules/coin-solana/src/api/chain/web3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import {
StakeProgram,
SystemProgram,
TransactionInstruction,
ComputeBudgetProgram,
} from "@solana/web3.js";
import chunk from "lodash/chunk";
import uniqBy from "lodash/uniqBy";
import { ChainAPI } from ".";
import { Awaited } from "../../logic";
import {
Expand All @@ -25,7 +27,7 @@ import {
TokenTransferCommand,
TransferCommand,
} from "../../types";
import { drainSeqAsyncGen } from "../../utils";
import { drainSeqAsyncGen, median } from "../../utils";
import { parseTokenAccountInfo, tryParseAsTokenAccount, tryParseAsVoteAccount } from "./account";
import { parseStakeAccountInfo } from "./account/parser";
import { StakeAccountInfo } from "./account/stake";
Expand Down Expand Up @@ -127,12 +129,10 @@ export function getTransactions(
return drainSeqAsyncGen(getTransactionsGen(address, untilTxSignature, api));
}

export const buildTransferInstructions = ({
sender,
recipient,
amount,
memo,
}: TransferCommand): TransactionInstruction[] => {
export const buildTransferInstructions = async (
api: ChainAPI,
{ sender, recipient, amount, memo }: TransferCommand,
): Promise<TransactionInstruction[]> => {
const fromPublicKey = new PublicKey(sender);
const toPublicKey = new PublicKey(recipient);

Expand All @@ -153,12 +153,13 @@ export const buildTransferInstructions = ({
instructions.push(memoIx);
}

return instructions;
return appendMaybePriorityFeeInstruction(api, [fromPublicKey, toPublicKey], instructions);
};

export const buildTokenTransferInstructions = (
export const buildTokenTransferInstructions = async (
api: ChainAPI,
command: TokenTransferCommand,
): TransactionInstruction[] => {
): Promise<TransactionInstruction[]> => {
const {
ownerAddress,
ownerAssociatedTokenAccountAddress,
Expand Down Expand Up @@ -271,6 +272,36 @@ export async function getStakeAccountAddressWithSeed({
return pubkey.toBase58();
}

export async function getPriorityFee(api: ChainAPI, accounts: PublicKey[]): Promise<number> {
const uniqAccs = uniqBy(accounts, acc => acc.toBase58());
const recentFees = await api.getRecentPrioritizationFees({
lockedWritableAccounts: uniqAccs,
});

return median(recentFees.map(item => item.prioritizationFee));
}

export async function buildMaybePriorityFeeInstruction(
api: ChainAPI,
accounts: PublicKey[],
): Promise<TransactionInstruction | null> {
const priorityFee = await getPriorityFee(api, accounts);
if (priorityFee === 0) return null;

return ComputeBudgetProgram.setComputeUnitPrice({
microLamports: priorityFee,
});
}

export async function appendMaybePriorityFeeInstruction(
api: ChainAPI,
accounts: PublicKey[],
ixs: TransactionInstruction[],
): Promise<TransactionInstruction[]> {
const priorityFeeIx = await buildMaybePriorityFeeInstruction(api, accounts);
return priorityFeeIx ? [priorityFeeIx, ...ixs] : ixs;
}

export function buildCreateAssociatedTokenAccountInstruction({
mint,
owner,
Expand All @@ -292,86 +323,82 @@ export function buildCreateAssociatedTokenAccountInstruction({
return instructions;
}

export function buildStakeDelegateInstructions({
authorizedAccAddr,
stakeAccAddr,
voteAccAddr,
}: StakeDelegateCommand): TransactionInstruction[] {
export async function buildStakeDelegateInstructions(
api: ChainAPI,
{ authorizedAccAddr, stakeAccAddr, voteAccAddr }: StakeDelegateCommand,
): Promise<TransactionInstruction[]> {
const withdrawAuthority = new PublicKey(authorizedAccAddr);
const stakeAcc = new PublicKey(stakeAccAddr);
const voteAcc = new PublicKey(voteAccAddr);
const tx = StakeProgram.delegate({
authorizedPubkey: new PublicKey(authorizedAccAddr),
stakePubkey: new PublicKey(stakeAccAddr),
votePubkey: new PublicKey(voteAccAddr),
authorizedPubkey: withdrawAuthority,
stakePubkey: stakeAcc,
votePubkey: voteAcc,
});

return tx.instructions;
return appendMaybePriorityFeeInstruction(api, [withdrawAuthority, stakeAcc], tx.instructions);
}

export function buildStakeUndelegateInstructions({
authorizedAccAddr,
stakeAccAddr,
}: StakeUndelegateCommand): TransactionInstruction[] {
export async function buildStakeUndelegateInstructions(
api: ChainAPI,
{ authorizedAccAddr, stakeAccAddr }: StakeUndelegateCommand,
): Promise<TransactionInstruction[]> {
const withdrawAuthority = new PublicKey(authorizedAccAddr);
const stakeAcc = new PublicKey(stakeAccAddr);
const tx = StakeProgram.deactivate({
authorizedPubkey: new PublicKey(authorizedAccAddr),
stakePubkey: new PublicKey(stakeAccAddr),
authorizedPubkey: withdrawAuthority,
stakePubkey: stakeAcc,
});

return tx.instructions;
return appendMaybePriorityFeeInstruction(api, [withdrawAuthority, stakeAcc], tx.instructions);
}

export function buildStakeWithdrawInstructions({
authorizedAccAddr,
stakeAccAddr,
amount,
toAccAddr,
}: StakeWithdrawCommand): TransactionInstruction[] {
export async function buildStakeWithdrawInstructions(
api: ChainAPI,
{ authorizedAccAddr, stakeAccAddr, amount, toAccAddr }: StakeWithdrawCommand,
): Promise<TransactionInstruction[]> {
const withdrawAuthority = new PublicKey(authorizedAccAddr);
const stakeAcc = new PublicKey(stakeAccAddr);
const recipient = new PublicKey(toAccAddr);
const tx = StakeProgram.withdraw({
authorizedPubkey: new PublicKey(authorizedAccAddr),
stakePubkey: new PublicKey(stakeAccAddr),
authorizedPubkey: withdrawAuthority,
stakePubkey: stakeAcc,
lamports: amount,
toPubkey: new PublicKey(toAccAddr),
toPubkey: recipient,
});

return tx.instructions;
return appendMaybePriorityFeeInstruction(api, [withdrawAuthority, stakeAcc], tx.instructions);
}

export function buildStakeSplitInstructions({
authorizedAccAddr,
stakeAccAddr,
seed,
amount,
splitStakeAccAddr,
}: StakeSplitCommand): TransactionInstruction[] {
// HACK: switch to split_with_seed when supported by @solana/web3.js
const splitIx = StakeProgram.split({
authorizedPubkey: new PublicKey(authorizedAccAddr),
export async function buildStakeSplitInstructions(
api: ChainAPI,
{ authorizedAccAddr, stakeAccAddr, seed, amount, splitStakeAccAddr }: StakeSplitCommand,
): Promise<TransactionInstruction[]> {
const basePk = new PublicKey(authorizedAccAddr);
const stakePk = new PublicKey(stakeAccAddr);
const splitStakePk = new PublicKey(splitStakeAccAddr);
const splitIx = StakeProgram.splitWithSeed({
authorizedPubkey: basePk,
lamports: amount,
stakePubkey: new PublicKey(stakeAccAddr),
splitStakePubkey: new PublicKey(splitStakeAccAddr),
}).instructions[1];

if (splitIx === undefined) {
throw new Error("expected split instruction");
}

const allocateIx = SystemProgram.allocate({
accountPubkey: new PublicKey(splitStakeAccAddr),
basePubkey: new PublicKey(authorizedAccAddr),
programId: StakeProgram.programId,
stakePubkey: stakePk,
splitStakePubkey: splitStakePk,
basePubkey: basePk,
seed,
space: StakeProgram.space,
});

return [allocateIx, splitIx];
return appendMaybePriorityFeeInstruction(api, [basePk, stakePk], splitIx.instructions);
}

export function buildStakeCreateAccountInstructions({
fromAccAddress,
stakeAccAddress,
seed,
amount,
stakeAccRentExemptAmount,
delegate,
}: StakeCreateAccountCommand): TransactionInstruction[] {
export async function buildStakeCreateAccountInstructions(
api: ChainAPI,
{
fromAccAddress,
stakeAccAddress,
seed,
amount,
stakeAccRentExemptAmount,
delegate,
}: StakeCreateAccountCommand,
): Promise<TransactionInstruction[]> {
const fromPubkey = new PublicKey(fromAccAddress);
const stakePubkey = new PublicKey(stakeAccAddress);

Expand All @@ -394,6 +421,5 @@ export function buildStakeCreateAccountInstructions({
votePubkey: new PublicKey(delegate.voteAccAddress),
}),
);

return tx.instructions;
return appendMaybePriorityFeeInstruction(api, [fromPubkey, stakePubkey], tx.instructions);
}
12 changes: 12 additions & 0 deletions libs/coin-modules/coin-solana/src/bridge.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,18 @@ const baseTx = {
const baseAPI = {
getLatestBlockhash: () => Promise.resolve(LATEST_BLOCKHASH_MOCK),
getFeeForMessage: (_msg: unknown) => Promise.resolve(testOnChainData.fees.lamportsPerSignature),
getRecentPrioritizationFees: (_: string[]) => {
return Promise.resolve([
{
slot: 122422797,
prioritizationFee: 0,
},
{
slot: 122422797,
prioritizationFee: 0,
},
]);
},
} as ChainAPI;

type StakeTestSpec = {
Expand Down
28 changes: 17 additions & 11 deletions libs/coin-modules/coin-solana/src/js-buildTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const buildTransactionWithAPI = async (
transaction: Transaction,
api: ChainAPI,
): Promise<readonly [OnChainTransaction, (signature: Buffer) => OnChainTransaction]> => {
const instructions = buildInstructions(transaction);
const instructions = await buildInstructions(api, transaction);

const recentBlockhash = await api.getLatestBlockhash();

Expand All @@ -46,35 +46,41 @@ export const buildTransactionWithAPI = async (
];
};

function buildInstructions(tx: Transaction): TransactionInstruction[] {
async function buildInstructions(
api: ChainAPI,
tx: Transaction,
): Promise<TransactionInstruction[]> {
const { commandDescriptor } = tx.model;
if (commandDescriptor === undefined) {
throw new Error("missing command descriptor");
}
if (Object.keys(commandDescriptor.errors).length > 0) {
throw new Error("can not build invalid command");
}
return buildInstructionsForCommand(commandDescriptor.command);
return buildInstructionsForCommand(api, commandDescriptor.command);
}

function buildInstructionsForCommand(command: Command): TransactionInstruction[] {
async function buildInstructionsForCommand(
api: ChainAPI,
command: Command,
): Promise<TransactionInstruction[]> {
switch (command.kind) {
case "transfer":
return buildTransferInstructions(command);
return buildTransferInstructions(api, command);
case "token.transfer":
return buildTokenTransferInstructions(command);
return buildTokenTransferInstructions(api, command);
case "token.createATA":
return buildCreateAssociatedTokenAccountInstruction(command);
case "stake.createAccount":
return buildStakeCreateAccountInstructions(command);
return buildStakeCreateAccountInstructions(api, command);
case "stake.delegate":
return buildStakeDelegateInstructions(command);
return buildStakeDelegateInstructions(api, command);
case "stake.undelegate":
return buildStakeUndelegateInstructions(command);
return buildStakeUndelegateInstructions(api, command);
case "stake.withdraw":
return buildStakeWithdrawInstructions(command);
return buildStakeWithdrawInstructions(api, command);
case "stake.split":
return buildStakeSplitInstructions(command);
return buildStakeSplitInstructions(api, command);
default:
return assertUnreachable(command);
}
Expand Down
4 changes: 2 additions & 2 deletions libs/coin-modules/coin-solana/src/js-synchronization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ function getMainAccOperationTypeFromTx(tx: ParsedTransaction): OperationType | u

const parsedIxs = instructions
.map(ix => parseQuiet(ix))
.filter(({ program }) => program !== "spl-memo");
.filter(({ program }) => program !== "spl-memo" && program !== "unknown");

if (parsedIxs.length === 3) {
const [first, second, third] = parsedIxs;
Expand Down Expand Up @@ -635,7 +635,7 @@ function getTokenAccOperationType({
const { instructions } = tx.message;
const [mainIx, ...otherIxs] = instructions
.map(ix => parseQuiet(ix))
.filter(({ program }) => program !== "spl-memo");
.filter(({ program }) => program !== "spl-memo" && program !== "unknown");

if (mainIx !== undefined && otherIxs.length === 0) {
switch (mainIx.program) {
Expand Down
Loading

0 comments on commit 37e13fd

Please sign in to comment.