Skip to content

Commit

Permalink
Optimise tx confirmation further (#157)
Browse files Browse the repository at this point in the history
* optimise tx confirmation further, trigger one last status check manually

* bump
  • Loading branch information
Yolley authored Apr 2, 2024
1 parent 53a78cb commit d8f882d
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 23 deletions.
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"packages": [
"packages/*"
],
"version": "6.0.2",
"version": "6.0.3",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
2 changes: 1 addition & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@streamflow/common",
"version": "6.0.2",
"version": "6.0.3",
"description": "Common utilities and types used by streamflow packages.",
"homepage": "https://github.com/streamflow-finance/js-sdk/",
"main": "dist/index.js",
Expand Down
97 changes: 88 additions & 9 deletions packages/common/solana/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ import {
sendAndConfirmRawTransaction,
Transaction,
TransactionInstruction,
TransactionExpiredBlockheightExceededError,
SignatureStatus,
} from "@solana/web3.js";
import bs58 from "bs58";

import { Account, AtaParams, ITransactionSolanaExt } from "./types";
import { sleep } from "../utils";

/**
* Wrapper function for Solana web3 getProgramAccounts with slightly better call interface
Expand Down Expand Up @@ -119,10 +122,6 @@ export async function signTransaction(invoker: Keypair | SignerWalletAdapter, tx

/**
* Signs, sends and confirms Transaction
* Confirmation strategy is not 100% reliable here as in times of congestion there can be a case that tx is executed,
* but is not in `commitment` state and so it's not considered executed by the `sendAndConfirmRawTransaction` method,
* and it raises an expiry error even though transaction may be executed soon.
* So we add additional 50 blocks for checks to account for such issues.
* @param connection - Solana client connection
* @param invoker - Keypair used as signer
* @param tx - Transaction instance
Expand All @@ -136,17 +135,97 @@ export async function signAndExecuteTransaction(
hash: BlockhashWithExpiryBlockHeight,
): Promise<string> {
const signedTx = await signTransaction(invoker, tx);
const rawTx = signedTx.serialize();

if (!hash.lastValidBlockHeight || !signedTx.signature || !hash.blockhash)
throw Error("Error with transaction parameters.");
return executeTransaction(connection, signedTx, hash);
}

/**
* Sends and confirms Transaction
* Confirmation strategy is not 100% reliable here as in times of congestion there can be a case that tx is executed,
* but is not in `commitment` state and so it's not considered executed by the `sendAndConfirmRawTransaction` method,
* and it raises an expiry error even though transaction may be executed soon.
* - so we add additional 50 blocks for checks to account for such issues;
* - also, we check for SignatureStatus one last time as it could be that websocket was slow to respond.
* @param connection - Solana client connection
* @param tx - Transaction instance
* @param hash - blockhash information, the same hash should be used in the Transaction
* @returns Transaction signature
*/
export async function executeTransaction(
connection: Connection,
tx: Transaction,
hash: BlockhashWithExpiryBlockHeight,
): Promise<string> {
const rawTx = tx.serialize();

if (!hash.lastValidBlockHeight || !tx.signature || !hash.blockhash) throw Error("Error with transaction parameters.");

const signature = bs58.encode(tx.signature);
const confirmationStrategy: BlockheightBasedTransactionConfirmationStrategy = {
lastValidBlockHeight: hash.lastValidBlockHeight + 50,
signature: bs58.encode(signedTx.signature),
signature,
blockhash: hash.blockhash,
};
return sendAndConfirmRawTransaction(connection, rawTx, confirmationStrategy);
try {
return await sendAndConfirmRawTransaction(connection, rawTx, confirmationStrategy);
} catch (e) {
// If BlockHeight expired, we will check tx status one last time to make sure
if (e instanceof TransactionExpiredBlockheightExceededError) {
await sleep(1000);
const value = await confirmAndEnsureTransaction(connection, signature);
if (!value) {
throw e;
}
return signature;
}
throw e;
}
}

/**
* Confirms and validates transaction success once
* @param connection - Solana client connection
* @param signature - Transaction signature
* @returns Transaction Status
*/
export async function confirmAndEnsureTransaction(
connection: Connection,
signature: string,
): Promise<SignatureStatus | null> {
const response = await connection.getSignatureStatus(signature);
if (!response) {
return null;
}
const { value } = response;
if (!value) {
return null;
}
if (value.err) {
// That's how solana-web3js does it, `err` here is an object that won't really be handled
throw new Error(`Raw transaction ${signature} failed (${JSON.stringify({ err: value.err })})`);
}
switch (connection.commitment) {
case "confirmed":
case "single":
case "singleGossip": {
if (value.confirmationStatus === "processed") {
return null;
}
break;
}
case "finalized":
case "max":
case "root": {
if (value.confirmationStatus === "processed" || value.confirmationStatus === "confirmed") {
return null;
}
break;
}
// exhaust enums to ensure full coverage
case "processed":
case "recent":
}
return value;
}

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,11 @@ export async function handleContractError<T>(
throw err;
}
}

/**
* Pause async function execution for given amount of milliseconds
* @param ms millisecond to sleep for
*/
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
2 changes: 1 addition & 1 deletion packages/distributor/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@streamflow/distributor",
"version": "6.0.2",
"version": "6.0.3",
"description": "JavaScript SDK to interact with Streamflow Airdrop protocol.",
"homepage": "https://github.com/streamflow-finance/js-sdk/",
"main": "dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/stream/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@streamflow/stream",
"version": "6.0.2",
"version": "6.0.3",
"description": "JavaScript SDK to interact with Streamflow protocol.",
"homepage": "https://github.com/streamflow-finance/js-sdk/",
"main": "dist/index.js",
Expand Down
13 changes: 3 additions & 10 deletions packages/stream/solana/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { SignerWalletAdapter } from "@solana/wallet-adapter-base";
import {
BlockheightBasedTransactionConfirmationStrategy,
Connection,
Keypair,
PublicKey,
sendAndConfirmRawTransaction,
} from "@solana/web3.js";
import { isSignerKeypair, isSignerWallet } from "@streamflow/common/solana";
import { BlockheightBasedTransactionConfirmationStrategy, Connection, Keypair, PublicKey } from "@solana/web3.js";
import { executeTransaction, isSignerKeypair, isSignerWallet } from "@streamflow/common/solana";
import BN from "bn.js";
import bs58 from "bs58";

Expand Down Expand Up @@ -107,7 +101,6 @@ export async function sendAndConfirmStreamRawTransaction(
batchItem: BatchItem,
): Promise<BatchItemResult> {
try {
const rawTx = batchItem.tx.serialize();
const { lastValidBlockHeight, signature, recentBlockhash } = batchItem.tx;
if (!lastValidBlockHeight || !signature || !recentBlockhash)
throw { recipient: batchItem.recipient, error: "no recent blockhash" };
Expand All @@ -117,7 +110,7 @@ export async function sendAndConfirmStreamRawTransaction(
signature: bs58.encode(signature),
blockhash: recentBlockhash,
};
const completedTxSignature = await sendAndConfirmRawTransaction(connection, rawTx, confirmationStrategy);
const completedTxSignature = await executeTransaction(connection, batchItem.tx, confirmationStrategy);
return { ...batchItem, signature: completedTxSignature };
} catch (error: any) {
throw {
Expand Down

0 comments on commit d8f882d

Please sign in to comment.