Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/spicy-gifts-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"gill": minor
---

add support for durable nonce transactions
26 changes: 24 additions & 2 deletions packages/gill/src/__tests__/create-transaction.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from "node:assert";

import { type Address, blockhash, generateKeyPairSigner, isKeyPairSigner, type KeyPairSigner } from "@solana/kit";
import { type Address, type Nonce, blockhash, generateKeyPairSigner, isKeyPairSigner, type KeyPairSigner } from "@solana/kit";

import { createTransaction } from "../core";
import { hasSetComputeLimitInstruction, hasSetComputeUnitPriceInstruction } from "../programs";
Expand Down Expand Up @@ -108,7 +108,7 @@ describe("createTransaction", () => {
assert.equal(hasSetComputeUnitPriceInstruction(tx), true);
});

it("create a version 0 transaction with an `Address` as the feePayer", () => {
it("create a version 0 transaction with an `Address` as the feePayer with a blockhash lifetime", () => {
const tx = createTransaction({
feePayer: signer.address,
instructions: [],
Expand All @@ -129,6 +129,28 @@ describe("createTransaction", () => {
assert.equal(hasSetComputeLimitInstruction(tx), false);
});

it("create a version 0 transaction with an `Address` as the feePayer with a durable nonce lifetime", () => {
const tx = createTransaction({
feePayer: signer.address,
instructions: [],
durableNonce: {
nonce: '123' as Nonce,
nonceAccountAddress: '123' as Address,
nonceAuthorityAddress: '123' as Address,
},
version: 0,
});

assert.equal(tx.version, 0);
assert.equal(tx.feePayer.address, signer.address);
assert.equal(isKeyPairSigner(tx.feePayer), false);
assert.equal(tx.instructions.length, 1); // Nonce advance instruction
assert.equal(Object.hasOwn(tx, "lifetimeConstraint"), true);
assert.equal(tx.lifetimeConstraint.nonce, '123' as Nonce);
assert.equal(hasSetComputeUnitPriceInstruction(tx), false);
assert.equal(hasSetComputeLimitInstruction(tx), false);
});

it("create a version 0 transaction with a compute unit limit instruction", () => {
const tx = createTransaction({
computeUnitLimit: 0,
Expand Down
37 changes: 36 additions & 1 deletion packages/gill/src/__typetests__/create-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
Instruction,
KeyPairSigner,
TransactionMessageWithBlockhashLifetime,
TransactionMessageWithDurableNonceLifetime,
TransactionMessageWithFeePayer,
TransactionMessageWithFeePayerSigner,
} from "@solana/kit";
Expand All @@ -17,6 +18,7 @@ import { createTransaction } from "../core";
const feePayer = null as unknown as Address;
const signer = null as unknown as KeyPairSigner;
const latestBlockhash = null as unknown as TransactionMessageWithBlockhashLifetime["lifetimeConstraint"];
const durableNonce = null as unknown as TransactionMessageWithDurableNonceLifetime["lifetimeConstraint"];

const ix = null as unknown as Instruction;

Expand Down Expand Up @@ -143,5 +145,38 @@ import { createTransaction } from "../core";

// Should be a signable transaction
signTransactionMessageWithSigners(txSignable);

// Should be version 0 with a Durable Nonce Lifetime and Signer
createTransaction({
version: 0,
feePayer: signer,
instructions: [ix],
durableNonce,
}) satisfies BaseTransactionMessage<0> &
TransactionMessageWithDurableNonceLifetime &
TransactionMessageWithFeePayerSigner;

// Should be version 0 with a Durable Nonce Lifetime and address (aka non Signer)
const txDurableNonceSignable = createTransaction({
version: 0,
feePayer: feePayer,
instructions: [ix],
durableNonce,
}) satisfies BaseTransactionMessage<0> &
TransactionMessageWithDurableNonceLifetime &
TransactionMessageWithFeePayer;

createTransaction({
version: 0,
feePayer: feePayer,
instructions: [ix],
durableNonce,
// @ts-expect-error Should not be a "fee payer signer"
}) satisfies TransactionMessageWithFeePayerSigner;

// Should be a signable transaction
signTransactionMessageWithSigners(txDurableNonceSignable);


}
}
}
48 changes: 47 additions & 1 deletion packages/gill/src/core/create-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction }
import type {
Address,
TransactionMessageWithBlockhashLifetime,
TransactionMessageWithDurableNonceLifetime,
TransactionMessageWithFeePayer,
TransactionMessageWithFeePayerSigner,
TransactionSigner,
Expand All @@ -16,6 +17,7 @@ import {
setTransactionMessageFeePayer,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
setTransactionMessageLifetimeUsingDurableNonce,
} from "@solana/kit";
import type { Simplify } from "../types";
import type { CreateTransactionInput, FullTransaction } from "../types/transactions";
Expand All @@ -42,6 +44,19 @@ export function createTransaction<
TransactionMessageWithBlockhashLifetime
>
>;
export function createTransaction<
TVersion extends TransactionVersion | "auto",
TFeePayer extends TransactionSigner,
TLifetimeConstraint extends TransactionMessageWithDurableNonceLifetime["lifetimeConstraint"],
>(
props: CreateTransactionInput<TVersion, TFeePayer, TLifetimeConstraint>,
): Simplify<
FullTransaction<
TVersion extends "auto" ? TransactionVersion : TVersion,
TransactionMessageWithFeePayerSigner,
TransactionMessageWithDurableNonceLifetime
>
>;
export function createTransaction<
TVersion extends TransactionVersion | "auto",
TFeePayer extends Address,
Expand All @@ -55,6 +70,19 @@ export function createTransaction<
TransactionMessageWithBlockhashLifetime
>
>;
export function createTransaction<
TVersion extends TransactionVersion | "auto",
TFeePayer extends Address,
TLifetimeConstraint extends TransactionMessageWithDurableNonceLifetime["lifetimeConstraint"],
>(
props: CreateTransactionInput<TVersion, TFeePayer, TLifetimeConstraint>,
): Simplify<
FullTransaction<
TVersion extends "auto" ? TransactionVersion : TVersion,
TransactionMessageWithFeePayer,
TransactionMessageWithDurableNonceLifetime
>
>;
export function createTransaction<
TVersion extends TransactionVersion | "auto",
TFeePayer extends Address | TransactionSigner,
Expand All @@ -68,6 +96,19 @@ export function createTransaction<
TransactionMessageWithBlockhashLifetime
>
>;
export function createTransaction<
TVersion extends TransactionVersion | "auto",
TFeePayer extends Address | TransactionSigner,
TLifetimeConstraint extends TransactionMessageWithDurableNonceLifetime["lifetimeConstraint"],
>(
props: CreateTransactionInput<TVersion, TFeePayer, TLifetimeConstraint>,
): Simplify<
FullTransaction<
TVersion extends "auto" ? TransactionVersion : TVersion,
TransactionMessageWithFeePayer,
TransactionMessageWithDurableNonceLifetime
>
>;
export function createTransaction<
TVersion extends TransactionVersion | "auto",
TFeePayer extends Address | TransactionSigner,
Expand All @@ -76,6 +117,7 @@ export function createTransaction<
feePayer,
instructions,
latestBlockhash,
durableNonce,
computeUnitLimit,
computeUnitPrice,
}: CreateTransactionInput<TVersion, TFeePayer>): FullTransaction<
Expand Down Expand Up @@ -103,7 +145,11 @@ export function createTransaction<
return createTransactionMessage({ version: selectedVersion });
})(),
(tx) => {
const withLifetime = latestBlockhash ? setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx) : tx;
const withLifetime = latestBlockhash ?
setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx) :
durableNonce ?
setTransactionMessageLifetimeUsingDurableNonce(durableNonce, tx) :
tx;
if (typeof feePayer !== "string" && "address" in feePayer && isTransactionSigner(feePayer)) {
return setTransactionMessageFeePayerSigner(feePayer, withLifetime);
} else return setTransactionMessageFeePayer(feePayer, withLifetime);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
Commitment,
getBase64EncodedWireTransaction,
getSignatureFromTransaction,
sendAndConfirmDurableNonceTransactionFactory,
sendAndConfirmTransactionFactory,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners,
Expand Down Expand Up @@ -94,6 +95,9 @@ export function sendAndConfirmTransactionWithSignersFactory<
}: SendAndConfirmTransactionWithSignersFactoryConfig<TCluster>): SendAndConfirmTransactionWithSignersFunction {
// @ts-ignore - TODO(FIXME)
const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });
// @ts-ignore - TODO(FIXME)
const sendAndConfirmDurableNonceTransaction = sendAndConfirmDurableNonceTransactionFactory({ rpc, rpcSubscriptions });

return async function sendAndConfirmTransactionWithSigners(transaction, config = { commitment: "confirmed" }) {
let signedTransaction: (Transaction & FullySignedTransaction) | undefined;

Expand Down Expand Up @@ -123,15 +127,28 @@ export function sendAndConfirmTransactionWithSignersFactory<
signedTransaction = transaction as unknown as Transaction & FullySignedTransaction;
}

assertIsTransactionWithBlockhashLifetime(signedTransaction);
assertIsTransactionWithinSizeLimit(signedTransaction);
assertIsFullySignedTransaction(signedTransaction);
assertIsSendableTransaction(signedTransaction);

debug(`Sending transaction: ${getExplorerLink({ transaction: getSignatureFromTransaction(signedTransaction) })}`);
debug(`Transaction as base64: ${getBase64EncodedWireTransaction(signedTransaction)}`, "debug");

await sendAndConfirmTransaction(signedTransaction, config);
// Check if the transaction has a durable nonce lifetime or blockhash lifetime
if (
"lifetimeConstraint" in signedTransaction &&
signedTransaction.lifetimeConstraint &&
typeof signedTransaction.lifetimeConstraint === "object" &&
"nonce" in signedTransaction.lifetimeConstraint
) {
// Durable nonce transaction
await sendAndConfirmDurableNonceTransaction(signedTransaction as any, config);
} else {
// Blockhash-based transaction
assertIsTransactionWithBlockhashLifetime(signedTransaction);
await sendAndConfirmTransaction(signedTransaction, config);
}

return getSignatureFromTransaction(signedTransaction);
};
}
22 changes: 18 additions & 4 deletions packages/gill/src/types/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
BaseTransactionMessage,
Instruction,
TransactionMessageWithBlockhashLifetime,
TransactionMessageWithDurableNonceLifetime,
TransactionMessageWithFeePayer,
TransactionMessageWithFeePayerSigner,
TransactionSigner,
Expand All @@ -14,7 +15,10 @@ import type { Simplify } from ".";
export type CreateTransactionInput<
TVersion extends TransactionVersion | "auto",
TFeePayer extends Address | TransactionSigner = TransactionSigner,
TLifetimeConstraint extends TransactionMessageWithBlockhashLifetime["lifetimeConstraint"] | undefined = undefined,
TLifetimeConstraint extends
| TransactionMessageWithBlockhashLifetime["lifetimeConstraint"]
| TransactionMessageWithDurableNonceLifetime["lifetimeConstraint"]
| undefined = undefined,
> = {
/** Compute unit limit value to set on this transaction */
computeUnitLimit?: bigint | number;
Expand All @@ -29,6 +33,11 @@ export type CreateTransactionInput<
* accepted for execution on the Solana network
* */
latestBlockhash?: TLifetimeConstraint;
/**
* Durable nonce lifetime for this transaction to
* accepted for execution on the Solana network
* */
durableNonce?: TLifetimeConstraint;
/**
* Transaction version
* - `auto` automatically selects based on instruction content (default)
Expand All @@ -43,11 +52,16 @@ export type CreateTransactionInput<
export type FullTransaction<
TVersion extends TransactionVersion,
TFeePayer extends TransactionMessageWithFeePayer | TransactionMessageWithFeePayerSigner,
TBlockhashLifetime extends TransactionMessageWithBlockhashLifetime | undefined = undefined,
TLifetime extends
| TransactionMessageWithBlockhashLifetime
| TransactionMessageWithDurableNonceLifetime
| undefined = undefined,
> = Simplify<
BaseTransactionMessage<TVersion> &
TFeePayer &
(TBlockhashLifetime extends TransactionMessageWithBlockhashLifetime
(TLifetime extends TransactionMessageWithBlockhashLifetime
? TransactionMessageWithBlockhashLifetime
: object)
: TLifetime extends TransactionMessageWithDurableNonceLifetime
? TransactionMessageWithDurableNonceLifetime
: object)
>;