Skip to content

Commit

Permalink
feat: a nonce-based transaction confirmation strategy for web3.js (#2…
Browse files Browse the repository at this point in the history
…5839)

* feat: you can now construct a `Transaction` with durable nonce information

* chore: refactor confirmation logic so that each strategy gets its own method

* feat: `geNonce` now accepts a `minContextSlot` param

* feat: a nonce-based transaction confirmation strategy

* feat: add nonce confirmation strategy to send-and-confirm helpers

* fix: nits from July 8 review

* Use Typescript narrowing to determine which strategy to use

* Double check the signature confirmation against the slot in which the nonce was discovered to have advanced
  • Loading branch information
steveluscher committed Nov 29, 2022
1 parent 0d0a491 commit 7646521
Show file tree
Hide file tree
Showing 9 changed files with 1,076 additions and 209 deletions.
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
@@ -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
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
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
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
@@ -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

0 comments on commit 7646521

Please sign in to comment.