Skip to content
Merged
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/plenty-signs-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solana/transactions': patch
---

Add functions to narrow a TransactionWithLifetime to a specific lifetime
134 changes: 133 additions & 1 deletion packages/transactions/src/__tests__/lifetime-test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Address } from '@solana/addresses';
import { ReadonlyUint8Array } from '@solana/codecs-core';
import { SOLANA_ERROR__TRANSACTION__NONCE_ACCOUNT_CANNOT_BE_IN_LOOKUP_TABLE, SolanaError } from '@solana/errors';
import { Blockhash } from '@solana/rpc-types';
import {
Expand All @@ -7,7 +8,12 @@ import {
Nonce,
} from '@solana/transaction-messages';

import { getTransactionLifetimeConstraintFromCompiledTransactionMessage } from '../lifetime';
import {
assertIsTransactionWithBlockhashLifetime,
assertIsTransactionWithDurableNonceLifetime,
getTransactionLifetimeConstraintFromCompiledTransactionMessage,
} from '../lifetime';
import { Transaction, TransactionMessageBytes } from '../transaction';

const SYSTEM_PROGRAM_ADDRESS = '11111111111111111111111111111111' as Address;
const U64_MAX = 2n ** 64n - 1n;
Expand Down Expand Up @@ -149,3 +155,129 @@ describe('getTransactionLifetimeConstraintFromCompiledTransactionMessage', () =>
);
});
});

describe('assertIsTransactionWithBlockhashLifetime', () => {
it('throws for a transaction with no lifetime constraint', () => {
const transaction: Transaction = {
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
signatures: {},
};
expect(() => assertIsTransactionWithBlockhashLifetime(transaction)).toThrow();
});
it('throws for a transaction with a durable nonce constraint', () => {
const transaction = {
lifetimeConstraint: {
nonce: 'abcd' as Nonce,
nonceAccountAddress: 'nonceAccountAddress' as Address,
},
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
signatures: {},
} as Transaction;
expect(() => assertIsTransactionWithBlockhashLifetime(transaction)).toThrow();
});
it('throws for a transaction with a blockhash but no lastValidBlockHeight in lifetimeConstraint', () => {
const transaction = {
lifetimeConstraint: {
blockhash: '11111111111111111111111111111111',
},
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
signatures: {},
} as Transaction;
expect(() => assertIsTransactionWithBlockhashLifetime(transaction)).toThrow();
});
it('throws for a transaction with a lastValidBlockHeight but no blockhash in lifetimeConstraint', () => {
const transaction = {
lifetimeConstraint: {
lastValidBlockHeight: 1234n,
},
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
signatures: {},
} as Transaction;
expect(() => assertIsTransactionWithBlockhashLifetime(transaction)).toThrow();
});
it('throws for a transaction with a blockhash lifetime but an invalid blockhash value', () => {
const transaction = {
lifetimeConstraint: {
blockhash: 'not a valid blockhash value',
},
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
signatures: {},
} as Transaction;
expect(() => assertIsTransactionWithBlockhashLifetime(transaction)).toThrow();
});
it('does not throw for a transaction with a valid blockhash lifetime constraint', () => {
const transaction = {
lifetimeConstraint: {
blockhash: '11111111111111111111111111111111',
lastValidBlockHeight: 1234n,
},
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
signatures: {},
} as Transaction;
expect(() => assertIsTransactionWithBlockhashLifetime(transaction)).not.toThrow();
});
});

describe('assertIsTransactionWithDurableNonceLifetime()', () => {
const validAddress = '2B7hCrBozp5hPV31mw1qUh5XhXYs9f6p1GsRdHNjF4xS' as Address;
it('throws for a transaction with no lifetime constraint', () => {
const transaction: Transaction = {
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
signatures: {},
};
expect(() => assertIsTransactionWithDurableNonceLifetime(transaction)).toThrow();
});
it('throws for a transaction with a blockhash constraint', () => {
const transaction = {
lifetimeConstraint: {
blockhash: '11111111111111111111111111111111',
lastValidBlockHeight: 1234n,
},
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
signatures: {},
} as Transaction;
expect(() => assertIsTransactionWithDurableNonceLifetime(transaction)).toThrow();
});
it('throws for a transaction with a nonce but no nonceAccountAddress in lifetimeConstraint', () => {
const transaction = {
lifetimeConstraint: {
nonce: 'abcd' as Nonce,
},
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
signatures: {},
} as Transaction;
expect(() => assertIsTransactionWithDurableNonceLifetime(transaction)).toThrow();
});
it('throws for a transaction with a nonceAccountAddress but no nonce in lifetimeConstraint', () => {
const transaction = {
lifetimeConstraint: {
nonceAccountAddress: validAddress,
},
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
signatures: {},
} as Transaction;
expect(() => assertIsTransactionWithDurableNonceLifetime(transaction)).toThrow();
});
it('throws for a transaction with a durable nonce lifetime but an invalid nonceAccountAddress value', () => {
const transaction = {
lifetimeConstraint: {
nonce: 'abcd' as Nonce,
nonceAccountAddress: 'not a valid address' as Address,
},
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
signatures: {},
} as Transaction;
expect(() => assertIsTransactionWithDurableNonceLifetime(transaction)).toThrow();
});
it('does not throw for a transaction with a valid durable nonce lifetime constraint', () => {
const transaction = {
lifetimeConstraint: {
nonce: 'abcd' as Nonce,
nonceAccountAddress: validAddress,
},
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
signatures: {},
} as Transaction;
expect(() => assertIsTransactionWithDurableNonceLifetime(transaction)).not.toThrow();
});
});
85 changes: 80 additions & 5 deletions packages/transactions/src/__typetests__/lifetime-typetest.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { Address } from '@solana/addresses';
import { Blockhash } from '@solana/rpc-types';
import type {
Nonce,
TransactionMessage,
TransactionMessageWithBlockhashLifetime,
TransactionMessageWithDurableNonceLifetime,
TransactionMessageWithLifetime,
} from '@solana/transaction-messages';

import type {
SetTransactionLifetimeFromTransactionMessage,
TransactionWithBlockhashLifetime,
TransactionWithDurableNonceLifetime,
TransactionWithLifetime,
import {
assertIsTransactionWithBlockhashLifetime,
assertIsTransactionWithDurableNonceLifetime,
isTransactionWithBlockhashLifetime,
isTransactionWithDurableNonceLifetime,
type SetTransactionLifetimeFromTransactionMessage,
type TransactionWithBlockhashLifetime,
type TransactionWithDurableNonceLifetime,
type TransactionWithLifetime,
} from '../lifetime';
import { Transaction } from '../transaction';

Expand Down Expand Up @@ -71,3 +78,71 @@ import { Transaction } from '../transaction';
null as unknown as Result satisfies Readonly<Transaction & TransactionWithBlockhashLifetime>;
}
}

// [DESCRIBE] isTransactionWithBlockhashLifetime
{
// It narrows the transaction type to one with a blockhash-based lifetime.
{
const transaction = null as unknown as Transaction & { some: 1 };
if (isTransactionWithBlockhashLifetime(transaction)) {
transaction satisfies Transaction & TransactionWithBlockhashLifetime & { some: 1 };
} else {
transaction satisfies Transaction & { some: 1 };
// @ts-expect-error It does not have a blockhash-based lifetime.
transaction satisfies TransactionWithBlockhashLifetime;
}
}
}

// [DESCRIBE] assertIsTransactionWithBlockhashLifetime
{
// It narrows the transaction type to one with a blockhash-based lifetime.
{
const transaction = null as unknown as Transaction & { some: 1 };
// @ts-expect-error Should not be blockhash lifetime
transaction satisfies TransactionWithBlockhashLifetime;
// @ts-expect-error Should not satisfy has blockhash
transaction satisfies { lifetimeConstraint: { blockhash: Blockhash } };
// @ts-expect-error Should not satisfy has lastValidBlockHeight
transaction satisfies { lifetimeConstraint: { lastValidBlockHeight: bigint } };
assertIsTransactionWithBlockhashLifetime(transaction);
transaction satisfies Transaction & TransactionWithBlockhashLifetime & { some: 1 };
transaction satisfies TransactionWithBlockhashLifetime;
transaction satisfies { lifetimeConstraint: { blockhash: Blockhash } };
transaction satisfies { lifetimeConstraint: { lastValidBlockHeight: bigint } };
}
}

// [DESCRIBE] isTransactionWithDurableNonceLifetime
{
// It narrows the transaction type to one with a nonce-based lifetime.
{
const transaction = null as unknown as Transaction & { some: 1 };
if (isTransactionWithDurableNonceLifetime(transaction)) {
transaction satisfies Transaction & TransactionWithDurableNonceLifetime & { some: 1 };
} else {
transaction satisfies Transaction & { some: 1 };
// @ts-expect-error It does not have a nonce-based lifetime.
transaction satisfies TransactionWithDurableNonceLifetime;
}
}
}

// [DESCRIBE] assertIsTransactionWithDurableNonceLifetime
{
// It narrows the transaction type to one with a nonce-based lifetime.
{
const transaction = null as unknown as Transaction & { some: 1 };
// @ts-expect-error Should not be durable nonce lifetime
transaction satisfies TransactionWithDurableNonceLifetime;
// @ts-expect-error Should not have a nonce-based lifetime
transaction satisfies { lifetimeConstraint: { nonce: Nonce } };
// @ts-expect-error Should not have a nonce account address
transaction satisfies { lifetimeConstraint: { nonceAccountAddress: Address } };
assertIsTransactionWithDurableNonceLifetime(transaction);
transaction satisfies Transaction & TransactionWithDurableNonceLifetime & { some: 1 };
transaction satisfies TransactionWithDurableNonceLifetime;
transaction satisfies { lifetimeConstraint: { nonce: Nonce } };
transaction satisfies { lifetimeConstraint: { nonceAccountAddress: Address } };
}
}
Loading