Skip to content

Commit

Permalink
init: Smart Transactions for SW3js.2
Browse files Browse the repository at this point in the history
Goal is to replicate functionality of Solana Smart Transaction SDK using SW3js.2
https://www.quicknode.com/docs/quicknode-sdk/Solana/Add-on%20RPC%20Functions#additional-rpc-functions
  • Loading branch information
amilz committed Oct 23, 2024
1 parent db64d28 commit fc80b5b
Show file tree
Hide file tree
Showing 13 changed files with 317 additions and 1 deletion.
22 changes: 22 additions & 0 deletions solana/web3.js-2.0/optimized-tx/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# QuickNode Solana SDK

A sample TS SDK for interacting optimizing Solana Transactions using Solana Web3.js-2.0 and QuickNode.

## Features

- Automatic compute unit estimation
- Dynamic priority fee calculation using QuickNode's [Priority Fee API](https://marketplace.quicknode.com/add-on/solana-priority-fee)
- Smart transaction preparation and sending
- Type-safe API
- [Solan Web3.js - 2.0](https://github.com/solana-labs/solana-web3.js)

## Installation

Clone this repo and then navigate to `web3.js-2.0` folder and run `npm install` to install dependencies.

## Quick Start

- Create or import your keypair (you can generate using `solana-keygen`).
- Make sure file is saved as secret.json (or update imports accordingly).
- Update `endpoint` in `example.ts` to point to your QuickNode endpoint.
- Run `example.ts` to see the example. Ensure you have the [Priority Fee API](https://marketplace.quicknode.com/add-on/solana-priority-fee) enabled in your QuickNode account.
41 changes: 41 additions & 0 deletions solana/web3.js-2.0/optimized-tx/example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { QuickNodeSolana } from './src';
import { getAddMemoInstruction } from "@solana-program/memo";
import { createKeyPairSignerFromBytes, lamports } from "@solana/web3.js";
import { getTransferSolInstruction } from "@solana-program/system";
import secret from "./secret.json";

// Initialize QuickNode client
const quickNode = new QuickNodeSolana({
endpoint: 'https://example.solana-mainnet.quiknode.pro/123/',
});

// Create and send a transaction
async function sendTransaction() {
// Create or import your keypair (you can generate using `solana-keygen`). Make sure file i saved as secret.json (or update imports accordingly)
const payerSigner = await createKeyPairSignerFromBytes(new Uint8Array(secret));

// Create instructions
const instructions = [
getAddMemoInstruction({
memo: "Hello Solana!",
}),
getTransferSolInstruction({
amount: lamports(BigInt(1)),
source: payerSigner,
destination: payerSigner.address, // use payerSigner.address for demo purposes
})
];

// Send transaction with automatic compute units and priority fees
const signature = await quickNode.sendSmartTransaction({
instructions,
signer: payerSigner,
});

console.log(`Transaction sent! Signature: ${signature}`);
}

sendTransaction().catch((error) => {
console.error(`❌ Error: ${error}`);
process.exit(1);
});
170 changes: 170 additions & 0 deletions solana/web3.js-2.0/optimized-tx/src/QuickNodeSolana.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import {
Rpc,
createRpc,
createRpcApi,
pipe,
createTransactionMessage,
setTransactionMessageLifetimeUsingBlockhash,
createSolanaRpc,
prependTransactionMessageInstruction,
getComputeUnitEstimateForTransactionMessageFactory,
Blockhash,
U64UnsafeBeyond2Pow53Minus1,
CompilableTransactionMessage,
BaseTransactionMessage,
setTransactionMessageFeePayer,
Address,
signTransaction,
appendTransactionMessageInstructions,
compileTransaction,
assertTransactionIsFullySigned,
sendAndConfirmTransactionFactory,
createSolanaRpcSubscriptions,
getSignatureFromTransaction,
assertIsTransactionMessageWithBlockhashLifetime,
SolanaRpcApi,
RpcSubscriptions,
SolanaRpcSubscriptionsApi
} from "@solana/web3.js";
import {
getSetComputeUnitLimitInstruction,
getSetComputeUnitPriceInstruction,
} from "@solana-program/compute-budget";
import { DEFAULTS } from './constants/defaults';
import { QuickNodeSolanaConfig } from './types/config';
import { PrepareSmartTransactionMessageArgs, SendSmartTransactionArgs } from './types/transaction';
import { createQuickNodeTransport } from './utils/transport'
import { PriorityFeeApi, PriorityFeeQuery } from "./types";

export class QuickNodeSolana {
private readonly solanaCore: Rpc<SolanaRpcApi>;
private readonly priorityFeeApi: Rpc<PriorityFeeApi>;
private readonly solanaSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>;
private readonly computeMargin: number;

constructor({ endpoint, wssEndpoint, computeMargin = DEFAULTS.DEFAULT_COMPUTE_MARGIN }: QuickNodeSolanaConfig) {
if (!wssEndpoint) {
const httpProviderUrl = new URL(endpoint);
httpProviderUrl.protocol = "wss:";
wssEndpoint = httpProviderUrl.toString();
}

this.solanaCore = createSolanaRpc(endpoint);
this.priorityFeeApi = this.createPriorityFeeApi(endpoint);
this.solanaSubscriptions = createSolanaRpcSubscriptions(wssEndpoint);
this.computeMargin = computeMargin;
}

private createPriorityFeeApi(endpoint: string): Rpc<PriorityFeeApi> {
const api = createRpcApi<PriorityFeeApi>({
parametersTransformer: (params: any[]) => params[0],
responseTransformer: (response: any) => response.result,
});
const transport = createQuickNodeTransport(endpoint);
return createRpc({ api, transport });
}

private createTransactionMessageWithInstructions(
feePayerAddress: Address<string>,
instructions: ReadonlyArray<BaseTransactionMessage['instructions'][number]>,
blockHash: Readonly<{
blockhash: Blockhash;
lastValidBlockHeight: U64UnsafeBeyond2Pow53Minus1;
}> = DEFAULTS.PLACEHOLDER_BLOCKHASH,
computeUnits: number = DEFAULTS.PLACEHOLDER_COMPUTE_UNIT,
priorityFeeMicroLamports: number = DEFAULTS.PLACEHOLDER_PRIORITY_FEE,
): CompilableTransactionMessage {
return pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayer(feePayerAddress, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(blockHash, tx),
(tx) => prependTransactionMessageInstruction(
getSetComputeUnitLimitInstruction({ units: computeUnits }),
tx
),
(tx) => prependTransactionMessageInstruction(
getSetComputeUnitPriceInstruction({ microLamports: priorityFeeMicroLamports }),
tx
),
(tx) => appendTransactionMessageInstructions(instructions, tx)
);
}

private async estimateComputeUnits(sampleMessage: CompilableTransactionMessage): Promise<number> {
const computeUnitsEstimator = getComputeUnitEstimateForTransactionMessageFactory({
rpc: this.solanaCore
});
const estimatedComputeUnits = await computeUnitsEstimator(sampleMessage);
return Math.ceil(estimatedComputeUnits * this.computeMargin);
}

private async getPriorityFeeMicroLamports(priorityFeeQuery: PriorityFeeQuery): Promise<number> {
const priorityFees = await this.priorityFeeApi.qn_estimatePriorityFees({
account: priorityFeeQuery.account,
last_n_blocks: priorityFeeQuery.last_n_blocks
}).send();
return priorityFees.per_compute_unit[priorityFeeQuery.level];
}

async prepareSmartTransactionMessage({
instructions,
feePayerAddress,
priorityFeeQuery = { level: "extreme" },
blockHashCommitment = "confirmed"
}: PrepareSmartTransactionMessageArgs): Promise<CompilableTransactionMessage> {
const sampleMessage = this.createTransactionMessageWithInstructions(
feePayerAddress,
instructions,
);
const estimatedComputeUnits = await this.estimateComputeUnits(sampleMessage);
const priorityFeeMicroLamports = await this.getPriorityFeeMicroLamports(priorityFeeQuery);

const { value: latestBlockhash } = await this.solanaCore
.getLatestBlockhash({ commitment: blockHashCommitment })
.send();

return this.createTransactionMessageWithInstructions(
feePayerAddress,
instructions,
latestBlockhash,
estimatedComputeUnits,
priorityFeeMicroLamports
);
}

async sendSmartTransaction({
instructions,
signer,
priorityFeeQuery = { level: "extreme" },
blockHashCommitment = "confirmed",
confirmCommitment = "confirmed"
}: SendSmartTransactionArgs): Promise<string> {
const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
rpc: this.solanaCore,
rpcSubscriptions: this.solanaSubscriptions
});

const smartMessage = await this.prepareSmartTransactionMessage({
instructions,
feePayerAddress: signer.address,
priorityFeeQuery,
blockHashCommitment
});

assertIsTransactionMessageWithBlockhashLifetime(smartMessage);
const compiledTransaction = compileTransaction(smartMessage);
const signedTransaction = await signTransaction([signer.keyPair], compiledTransaction);
assertTransactionIsFullySigned(signedTransaction);
const signature = getSignatureFromTransaction(signedTransaction);

try {
await sendAndConfirmTransaction(
signedTransaction,
{ commitment: confirmCommitment }
);
return signature;
} catch (error) {
throw error;
}
}
}
11 changes: 11 additions & 0 deletions solana/web3.js-2.0/optimized-tx/src/constants/defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Blockhash } from "@solana/web3.js";

export const DEFAULTS = {
PLACEHOLDER_BLOCKHASH: {
blockhash: '11111111111111111111111111111111' as Blockhash,
lastValidBlockHeight: 0n,
} as const,
PLACEHOLDER_COMPUTE_UNIT: 1_400_000,
PLACEHOLDER_PRIORITY_FEE: 1,
DEFAULT_COMPUTE_MARGIN: 1.05
} as const;
1 change: 1 addition & 0 deletions solana/web3.js-2.0/optimized-tx/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './defaults';
3 changes: 3 additions & 0 deletions solana/web3.js-2.0/optimized-tx/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { QuickNodeSolana } from './QuickNodeSolana';
export * from './types';
export * from './constants';
5 changes: 5 additions & 0 deletions solana/web3.js-2.0/optimized-tx/src/types/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface QuickNodeSolanaConfig {
endpoint: string;
wssEndpoint?: string;
computeMargin?: number;
}
3 changes: 3 additions & 0 deletions solana/web3.js-2.0/optimized-tx/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './config';
export * from './priority-fees';
export * from './transaction';
34 changes: 34 additions & 0 deletions solana/web3.js-2.0/optimized-tx/src/types/priority-fees.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
type PriorityFeeLevel = { level: "low" | "medium" | "high" | "extreme" };

interface FeeEstimates {
extreme: number;
high: number;
low: number;
medium: number;
percentiles: {
[key: string]: number;
};
}

interface EstimatePriorityFeesResponse {
context: {
slot: number;
};
per_compute_unit: FeeEstimates;
per_transaction: FeeEstimates;
};

interface EstimatePriorityFeesParams {
// (Optional) The number of blocks to consider for the fee estimate
last_n_blocks?: number;
// (Optional) The program account to use for fetching the local estimate (e.g., Jupiter: JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4)
account?: string;
}

type PriorityFeeQuery = PriorityFeeLevel & EstimatePriorityFeesParams;

type PriorityFeeApi = {
qn_estimatePriorityFees(params: EstimatePriorityFeesParams): EstimatePriorityFeesResponse;
}

export type { FeeEstimates, EstimatePriorityFeesResponse, EstimatePriorityFeesParams, PriorityFeeLevel, PriorityFeeQuery, PriorityFeeApi };
17 changes: 17 additions & 0 deletions solana/web3.js-2.0/optimized-tx/src/types/transaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { BaseTransactionMessage, Commitment, Address, KeyPairSigner } from "@solana/web3.js";
import { PriorityFeeQuery } from "./priority-fees";

export interface SmartTransactionBaseArgs {
instructions: ReadonlyArray<BaseTransactionMessage['instructions'][number]>;
priorityFeeQuery?: PriorityFeeQuery;
blockHashCommitment?: Commitment;
}

export interface PrepareSmartTransactionMessageArgs extends SmartTransactionBaseArgs {
feePayerAddress: Address<string>;
}

export interface SendSmartTransactionArgs extends SmartTransactionBaseArgs {
signer: KeyPairSigner<string>;
confirmCommitment?: Commitment;
}
1 change: 1 addition & 0 deletions solana/web3.js-2.0/optimized-tx/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './transport';
8 changes: 8 additions & 0 deletions solana/web3.js-2.0/optimized-tx/src/utils/transport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { RpcTransport, createDefaultRpcTransport } from "@solana/web3.js";

export function createQuickNodeTransport(endpoint: string): RpcTransport {
const jsonRpcTransport = createDefaultRpcTransport({ url: endpoint });
return async <TResponse>(...args: Parameters<RpcTransport>): Promise<TResponse> => {
return await jsonRpcTransport(...args);
};
}
2 changes: 1 addition & 1 deletion solana/web3.js-2.0/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es2016",
"target": "es2020",
"module": "commonjs",
"resolveJsonModule": true,
"esModuleInterop": true,
Expand Down

0 comments on commit fc80b5b

Please sign in to comment.