From fc80b5baa05826f9c9982dcb42e9a2c368b7a1ca Mon Sep 17 00:00:00 2001 From: "amilz.sol" <85324096+amilz@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:09:22 -0700 Subject: [PATCH] init: Smart Transactions for SW3js.2 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 --- solana/web3.js-2.0/optimized-tx/README.md | 22 +++ solana/web3.js-2.0/optimized-tx/example.ts | 41 +++++ .../optimized-tx/src/QuickNodeSolana.ts | 170 ++++++++++++++++++ .../optimized-tx/src/constants/defaults.ts | 11 ++ .../optimized-tx/src/constants/index.ts | 1 + solana/web3.js-2.0/optimized-tx/src/index.ts | 3 + .../optimized-tx/src/types/config.ts | 5 + .../optimized-tx/src/types/index.ts | 3 + .../optimized-tx/src/types/priority-fees.ts | 34 ++++ .../optimized-tx/src/types/transaction.ts | 17 ++ .../optimized-tx/src/utils/index.ts | 1 + .../optimized-tx/src/utils/transport.ts | 8 + solana/web3.js-2.0/tsconfig.json | 2 +- 13 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 solana/web3.js-2.0/optimized-tx/README.md create mode 100644 solana/web3.js-2.0/optimized-tx/example.ts create mode 100644 solana/web3.js-2.0/optimized-tx/src/QuickNodeSolana.ts create mode 100644 solana/web3.js-2.0/optimized-tx/src/constants/defaults.ts create mode 100644 solana/web3.js-2.0/optimized-tx/src/constants/index.ts create mode 100644 solana/web3.js-2.0/optimized-tx/src/index.ts create mode 100644 solana/web3.js-2.0/optimized-tx/src/types/config.ts create mode 100644 solana/web3.js-2.0/optimized-tx/src/types/index.ts create mode 100644 solana/web3.js-2.0/optimized-tx/src/types/priority-fees.ts create mode 100644 solana/web3.js-2.0/optimized-tx/src/types/transaction.ts create mode 100644 solana/web3.js-2.0/optimized-tx/src/utils/index.ts create mode 100644 solana/web3.js-2.0/optimized-tx/src/utils/transport.ts diff --git a/solana/web3.js-2.0/optimized-tx/README.md b/solana/web3.js-2.0/optimized-tx/README.md new file mode 100644 index 0000000..13f406c --- /dev/null +++ b/solana/web3.js-2.0/optimized-tx/README.md @@ -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. diff --git a/solana/web3.js-2.0/optimized-tx/example.ts b/solana/web3.js-2.0/optimized-tx/example.ts new file mode 100644 index 0000000..ef1df17 --- /dev/null +++ b/solana/web3.js-2.0/optimized-tx/example.ts @@ -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); +}); \ No newline at end of file diff --git a/solana/web3.js-2.0/optimized-tx/src/QuickNodeSolana.ts b/solana/web3.js-2.0/optimized-tx/src/QuickNodeSolana.ts new file mode 100644 index 0000000..e4c8faa --- /dev/null +++ b/solana/web3.js-2.0/optimized-tx/src/QuickNodeSolana.ts @@ -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; + private readonly priorityFeeApi: Rpc; + private readonly solanaSubscriptions: RpcSubscriptions; + 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 { + const api = createRpcApi({ + parametersTransformer: (params: any[]) => params[0], + responseTransformer: (response: any) => response.result, + }); + const transport = createQuickNodeTransport(endpoint); + return createRpc({ api, transport }); + } + + private createTransactionMessageWithInstructions( + feePayerAddress: Address, + instructions: ReadonlyArray, + 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 { + const computeUnitsEstimator = getComputeUnitEstimateForTransactionMessageFactory({ + rpc: this.solanaCore + }); + const estimatedComputeUnits = await computeUnitsEstimator(sampleMessage); + return Math.ceil(estimatedComputeUnits * this.computeMargin); + } + + private async getPriorityFeeMicroLamports(priorityFeeQuery: PriorityFeeQuery): Promise { + 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 { + 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 { + 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; + } + } +} \ No newline at end of file diff --git a/solana/web3.js-2.0/optimized-tx/src/constants/defaults.ts b/solana/web3.js-2.0/optimized-tx/src/constants/defaults.ts new file mode 100644 index 0000000..a08b55f --- /dev/null +++ b/solana/web3.js-2.0/optimized-tx/src/constants/defaults.ts @@ -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; diff --git a/solana/web3.js-2.0/optimized-tx/src/constants/index.ts b/solana/web3.js-2.0/optimized-tx/src/constants/index.ts new file mode 100644 index 0000000..34df3f0 --- /dev/null +++ b/solana/web3.js-2.0/optimized-tx/src/constants/index.ts @@ -0,0 +1 @@ +export * from './defaults'; \ No newline at end of file diff --git a/solana/web3.js-2.0/optimized-tx/src/index.ts b/solana/web3.js-2.0/optimized-tx/src/index.ts new file mode 100644 index 0000000..7b7199a --- /dev/null +++ b/solana/web3.js-2.0/optimized-tx/src/index.ts @@ -0,0 +1,3 @@ +export { QuickNodeSolana } from './QuickNodeSolana'; +export * from './types'; +export * from './constants'; diff --git a/solana/web3.js-2.0/optimized-tx/src/types/config.ts b/solana/web3.js-2.0/optimized-tx/src/types/config.ts new file mode 100644 index 0000000..49f3617 --- /dev/null +++ b/solana/web3.js-2.0/optimized-tx/src/types/config.ts @@ -0,0 +1,5 @@ +export interface QuickNodeSolanaConfig { + endpoint: string; + wssEndpoint?: string; + computeMargin?: number; +} \ No newline at end of file diff --git a/solana/web3.js-2.0/optimized-tx/src/types/index.ts b/solana/web3.js-2.0/optimized-tx/src/types/index.ts new file mode 100644 index 0000000..a1ede4e --- /dev/null +++ b/solana/web3.js-2.0/optimized-tx/src/types/index.ts @@ -0,0 +1,3 @@ +export * from './config'; +export * from './priority-fees'; +export * from './transaction'; \ No newline at end of file diff --git a/solana/web3.js-2.0/optimized-tx/src/types/priority-fees.ts b/solana/web3.js-2.0/optimized-tx/src/types/priority-fees.ts new file mode 100644 index 0000000..360defa --- /dev/null +++ b/solana/web3.js-2.0/optimized-tx/src/types/priority-fees.ts @@ -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 }; diff --git a/solana/web3.js-2.0/optimized-tx/src/types/transaction.ts b/solana/web3.js-2.0/optimized-tx/src/types/transaction.ts new file mode 100644 index 0000000..07a9fd5 --- /dev/null +++ b/solana/web3.js-2.0/optimized-tx/src/types/transaction.ts @@ -0,0 +1,17 @@ +import { BaseTransactionMessage, Commitment, Address, KeyPairSigner } from "@solana/web3.js"; +import { PriorityFeeQuery } from "./priority-fees"; + +export interface SmartTransactionBaseArgs { + instructions: ReadonlyArray; + priorityFeeQuery?: PriorityFeeQuery; + blockHashCommitment?: Commitment; +} + +export interface PrepareSmartTransactionMessageArgs extends SmartTransactionBaseArgs { + feePayerAddress: Address; +} + +export interface SendSmartTransactionArgs extends SmartTransactionBaseArgs { + signer: KeyPairSigner; + confirmCommitment?: Commitment; +} \ No newline at end of file diff --git a/solana/web3.js-2.0/optimized-tx/src/utils/index.ts b/solana/web3.js-2.0/optimized-tx/src/utils/index.ts new file mode 100644 index 0000000..7f7e584 --- /dev/null +++ b/solana/web3.js-2.0/optimized-tx/src/utils/index.ts @@ -0,0 +1 @@ +export * from './transport'; \ No newline at end of file diff --git a/solana/web3.js-2.0/optimized-tx/src/utils/transport.ts b/solana/web3.js-2.0/optimized-tx/src/utils/transport.ts new file mode 100644 index 0000000..0593094 --- /dev/null +++ b/solana/web3.js-2.0/optimized-tx/src/utils/transport.ts @@ -0,0 +1,8 @@ +import { RpcTransport, createDefaultRpcTransport } from "@solana/web3.js"; + +export function createQuickNodeTransport(endpoint: string): RpcTransport { + const jsonRpcTransport = createDefaultRpcTransport({ url: endpoint }); + return async (...args: Parameters): Promise => { + return await jsonRpcTransport(...args); + }; +} diff --git a/solana/web3.js-2.0/tsconfig.json b/solana/web3.js-2.0/tsconfig.json index 52bcaf1..a0a92da 100644 --- a/solana/web3.js-2.0/tsconfig.json +++ b/solana/web3.js-2.0/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es2016", + "target": "es2020", "module": "commonjs", "resolveJsonModule": true, "esModuleInterop": true,