Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: a nonce-based transaction confirmation strategy for web3.js #25839

Merged
merged 8 commits into from
Nov 29, 2022
396 changes: 320 additions & 76 deletions web3.js/src/connection.ts

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions web3.js/src/nonce-account.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as BufferLayout from '@solana/buffer-layout';
import {Buffer} from 'buffer';

import type {Blockhash} from './blockhash';
import * as Layout from './layout';
import {PublicKey} from './publickey';
import type {FeeCalculator} from './fee-calculator';
Expand Down Expand Up @@ -36,9 +35,14 @@ const NonceAccountLayout = BufferLayout.struct<

export const NONCE_ACCOUNT_LENGTH = NonceAccountLayout.span;

/**
* A durable nonce is a 32 byte value encoded as a base58 string.
*/
export type DurableNonce = string;

type NonceAccountArgs = {
authorizedPubkey: PublicKey;
nonce: Blockhash;
nonce: DurableNonce;
feeCalculator: FeeCalculator;
};

Expand All @@ -47,7 +51,7 @@ type NonceAccountArgs = {
*/
export class NonceAccount {
authorizedPubkey: PublicKey;
nonce: Blockhash;
nonce: DurableNonce;
feeCalculator: FeeCalculator;

/**
Expand Down
13 changes: 13 additions & 0 deletions web3.js/src/transaction/expiry-custom-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,16 @@ export class TransactionExpiredTimeoutError extends Error {
Object.defineProperty(TransactionExpiredTimeoutError.prototype, 'name', {
value: 'TransactionExpiredTimeoutError',
});

export class TransactionExpiredNonceInvalidError extends Error {
signature: string;

constructor(signature: string) {
super(`Signature ${signature} has expired: the nonce is no longer valid.`);
this.signature = signature;
}
}

Object.defineProperty(TransactionExpiredNonceInvalidError.prototype, 'name', {
value: 'TransactionExpiredNonceInvalidError',
});
42 changes: 39 additions & 3 deletions web3.js/src/transaction/legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const enum TransactionStatus {
BLOCKHEIGHT_EXCEEDED,
PROCESSED,
TIMED_OUT,
NONCE_INVALID,
}

/**
Expand Down Expand Up @@ -145,7 +146,9 @@ export type TransactionCtorFields_DEPRECATED = {
export type TransactionCtorFields = TransactionCtorFields_DEPRECATED;

/**
* List of Transaction object fields that may be initialized at construction
* Blockhash-based transactions have a lifetime that are defined by
* the blockhash they include. Any transaction whose blockhash is
* too old will be rejected.
*/
export type TransactionBlockhashCtor = {
/** The transaction fee payer */
Expand All @@ -158,6 +161,18 @@ export type TransactionBlockhashCtor = {
lastValidBlockHeight: number;
};

/**
* Use these options to construct a durable nonce transaction.
*/
export type TransactionNonceCtor = {
/** The transaction fee payer */
feePayer?: PublicKey | null;
minContextSlot: number;
nonceInfo: NonceInformation;
/** One or more signatures */
signatures?: Array<SignaturePubkeyPair>;
};

/**
* Nonce information to be used to build an offline Transaction.
*/
Expand Down Expand Up @@ -228,6 +243,15 @@ export class Transaction {
*/
nonceInfo?: NonceInformation;

/**
* If this is a nonce transaction this represents the minimum slot from which
* to evaluate if the nonce has advanced when attempting to confirm the
* transaction. This protects against a case where the transaction confirmation
* logic loads the nonce account from an old slot and assumes the mismatch in
* nonce value implies that the nonce has been advanced.
*/
minNonceContextSlot?: number;

/**
* @internal
*/
Expand All @@ -241,6 +265,9 @@ export class Transaction {
// Construct a transaction with a blockhash and lastValidBlockHeight
constructor(opts?: TransactionBlockhashCtor);

// Construct a transaction using a durable nonce
constructor(opts?: TransactionNonceCtor);

/**
* @deprecated `TransactionCtorFields` has been deprecated and will be removed in a future version.
* Please supply a `TransactionBlockhashCtor` instead.
Expand All @@ -251,7 +278,10 @@ export class Transaction {
* Construct an empty Transaction
*/
constructor(
opts?: TransactionBlockhashCtor | TransactionCtorFields_DEPRECATED,
opts?:
| TransactionBlockhashCtor
| TransactionNonceCtor
| TransactionCtorFields_DEPRECATED,
) {
if (!opts) {
return;
Expand All @@ -262,7 +292,13 @@ export class Transaction {
if (opts.signatures) {
this.signatures = opts.signatures;
}
if (Object.prototype.hasOwnProperty.call(opts, 'lastValidBlockHeight')) {
if (Object.prototype.hasOwnProperty.call(opts, 'nonceInfo')) {
const {minContextSlot, nonceInfo} = opts as TransactionNonceCtor;
this.minNonceContextSlot = minContextSlot;
this.nonceInfo = nonceInfo;
} else if (
Object.prototype.hasOwnProperty.call(opts, 'lastValidBlockHeight')
) {
const {blockhash, lastValidBlockHeight} =
opts as TransactionBlockhashCtor;
this.recentBlockhash = blockhash;
Expand Down
13 changes: 13 additions & 0 deletions web3.js/src/utils/send-and-confirm-raw-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {Buffer} from 'buffer';
import {
BlockheightBasedTransactionConfirmationStrategy,
Connection,
DurableNonceTransactionConfirmationStrategy,
} from '../connection';
import type {TransactionSignature} from '../transaction';
import type {ConfirmOptions} from '../connection';
Expand Down Expand Up @@ -42,12 +43,14 @@ export async function sendAndConfirmRawTransaction(
rawTransaction: Buffer,
confirmationStrategyOrConfirmOptions:
| BlockheightBasedTransactionConfirmationStrategy
| DurableNonceTransactionConfirmationStrategy
| ConfirmOptions
| undefined,
maybeConfirmOptions?: ConfirmOptions,
): Promise<TransactionSignature> {
let confirmationStrategy:
| BlockheightBasedTransactionConfirmationStrategy
| DurableNonceTransactionConfirmationStrategy
| undefined;
let options: ConfirmOptions | undefined;
if (
Expand All @@ -60,6 +63,16 @@ export async function sendAndConfirmRawTransaction(
confirmationStrategy =
confirmationStrategyOrConfirmOptions as BlockheightBasedTransactionConfirmationStrategy;
options = maybeConfirmOptions;
} else if (
confirmationStrategyOrConfirmOptions &&
Object.prototype.hasOwnProperty.call(
confirmationStrategyOrConfirmOptions,
'nonceValue',
)
) {
confirmationStrategy =
confirmationStrategyOrConfirmOptions as DurableNonceTransactionConfirmationStrategy;
options = maybeConfirmOptions;
} else {
options = confirmationStrategyOrConfirmOptions as
| ConfirmOptions
Expand Down
57 changes: 39 additions & 18 deletions web3.js/src/utils/send-and-confirm-transaction.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Connection} from '../connection';
import {Connection, SignatureResult} from '../connection';
import {Transaction} from '../transaction';
import type {ConfirmOptions} from '../connection';
import type {Signer} from '../keypair';
Expand Down Expand Up @@ -34,25 +34,46 @@ export async function sendAndConfirmTransaction(
sendOptions,
);

const status =
let status: SignatureResult;
if (
transaction.recentBlockhash != null &&
transaction.lastValidBlockHeight != null
? (
await connection.confirmTransaction(
{
signature: signature,
blockhash: transaction.recentBlockhash,
lastValidBlockHeight: transaction.lastValidBlockHeight,
},
options && options.commitment,
)
).value
: (
await connection.confirmTransaction(
signature,
options && options.commitment,
)
).value;
) {
status = (
await connection.confirmTransaction(
{
signature: signature,
blockhash: transaction.recentBlockhash,
lastValidBlockHeight: transaction.lastValidBlockHeight,
},
options && options.commitment,
)
).value;
} else if (
transaction.minNonceContextSlot != null &&
transaction.nonceInfo != null
) {
const {nonceInstruction} = transaction.nonceInfo;
const nonceAccountPubkey = nonceInstruction.keys[0].pubkey;
status = (
await connection.confirmTransaction(
{
minContextSlot: transaction.minNonceContextSlot,
nonceAccountPubkey,
nonceValue: transaction.nonceInfo.nonce,
signature,
},
options && options.commitment,
)
).value;
} else {
status = (
await connection.confirmTransaction(
signature,
options && options.commitment,
)
).value;
}

if (status.err) {
throw new Error(
Expand Down
Loading