-
Notifications
You must be signed in to change notification settings - Fork 68
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
13 changed files
with
317 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './defaults'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export { QuickNodeSolana } from './QuickNodeSolana'; | ||
export * from './types'; | ||
export * from './constants'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export interface QuickNodeSolanaConfig { | ||
endpoint: string; | ||
wssEndpoint?: string; | ||
computeMargin?: number; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
34
solana/web3.js-2.0/optimized-tx/src/types/priority-fees.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './transport'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters