Skip to content

Commit 5915e87

Browse files
committed
Add functions to narrow a TransactionWithLifetime to a specific lifetime
1 parent 8a6cc93 commit 5915e87

File tree

3 files changed

+350
-10
lines changed

3 files changed

+350
-10
lines changed

packages/transactions/src/__tests__/lifetime-test.ts

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Address } from '@solana/addresses';
2+
import { ReadonlyUint8Array } from '@solana/codecs-core';
23
import { SOLANA_ERROR__TRANSACTION__NONCE_ACCOUNT_CANNOT_BE_IN_LOOKUP_TABLE, SolanaError } from '@solana/errors';
34
import { Blockhash } from '@solana/rpc-types';
45
import {
@@ -7,7 +8,12 @@ import {
78
Nonce,
89
} from '@solana/transaction-messages';
910

10-
import { getTransactionLifetimeConstraintFromCompiledTransactionMessage } from '../lifetime';
11+
import {
12+
assertIsTransactionWithBlockhashLifetime,
13+
assertIsTransactionWithDurableNonceLifetime,
14+
getTransactionLifetimeConstraintFromCompiledTransactionMessage,
15+
} from '../lifetime';
16+
import { Transaction, TransactionMessageBytes } from '../transaction';
1117

1218
const SYSTEM_PROGRAM_ADDRESS = '11111111111111111111111111111111' as Address;
1319
const U64_MAX = 2n ** 64n - 1n;
@@ -149,3 +155,129 @@ describe('getTransactionLifetimeConstraintFromCompiledTransactionMessage', () =>
149155
);
150156
});
151157
});
158+
159+
describe('assertIsTransactionWithBlockhashLifetime', () => {
160+
it('throws for a transaction with no lifetime constraint', () => {
161+
const transaction: Transaction = {
162+
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
163+
signatures: {},
164+
};
165+
expect(() => assertIsTransactionWithBlockhashLifetime(transaction)).toThrow();
166+
});
167+
it('throws for a transaction with a durable nonce constraint', () => {
168+
const transaction = {
169+
lifetimeConstraint: {
170+
nonce: 'abcd' as Nonce,
171+
nonceAccountAddress: 'nonceAccountAddress' as Address,
172+
},
173+
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
174+
signatures: {},
175+
} as Transaction;
176+
expect(() => assertIsTransactionWithBlockhashLifetime(transaction)).toThrow();
177+
});
178+
it('throws for a transaction with a blockhash but no lastValidBlockHeight in lifetimeConstraint', () => {
179+
const transaction = {
180+
lifetimeConstraint: {
181+
blockhash: '11111111111111111111111111111111',
182+
},
183+
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
184+
signatures: {},
185+
} as Transaction;
186+
expect(() => assertIsTransactionWithBlockhashLifetime(transaction)).toThrow();
187+
});
188+
it('throws for a transaction with a lastValidBlockHeight but no blockhash in lifetimeConstraint', () => {
189+
const transaction = {
190+
lifetimeConstraint: {
191+
lastValidBlockHeight: 1234n,
192+
},
193+
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
194+
signatures: {},
195+
} as Transaction;
196+
expect(() => assertIsTransactionWithBlockhashLifetime(transaction)).toThrow();
197+
});
198+
it('throws for a transaction with a blockhash lifetime but an invalid blockhash value', () => {
199+
const transaction = {
200+
lifetimeConstraint: {
201+
blockhash: 'not a valid blockhash value',
202+
},
203+
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
204+
signatures: {},
205+
} as Transaction;
206+
expect(() => assertIsTransactionWithBlockhashLifetime(transaction)).toThrow();
207+
});
208+
it('does not throw for a transaction with a valid blockhash lifetime constraint', () => {
209+
const transaction = {
210+
lifetimeConstraint: {
211+
blockhash: '11111111111111111111111111111111',
212+
lastValidBlockHeight: 1234n,
213+
},
214+
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
215+
signatures: {},
216+
} as Transaction;
217+
expect(() => assertIsTransactionWithBlockhashLifetime(transaction)).not.toThrow();
218+
});
219+
});
220+
221+
describe('assertIsTransactionWithDurableNonceLifetime()', () => {
222+
const validAddress = '2B7hCrBozp5hPV31mw1qUh5XhXYs9f6p1GsRdHNjF4xS' as Address;
223+
it('throws for a transaction with no lifetime constraint', () => {
224+
const transaction: Transaction = {
225+
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
226+
signatures: {},
227+
};
228+
expect(() => assertIsTransactionWithDurableNonceLifetime(transaction)).toThrow();
229+
});
230+
it('throws for a transaction with a blockhash constraint', () => {
231+
const transaction = {
232+
lifetimeConstraint: {
233+
blockhash: '11111111111111111111111111111111',
234+
lastValidBlockHeight: 1234n,
235+
},
236+
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
237+
signatures: {},
238+
} as Transaction;
239+
expect(() => assertIsTransactionWithDurableNonceLifetime(transaction)).toThrow();
240+
});
241+
it('throws for a transaction with a nonce but no nonceAccountAddress in lifetimeConstraint', () => {
242+
const transaction = {
243+
lifetimeConstraint: {
244+
nonce: 'abcd' as Nonce,
245+
},
246+
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
247+
signatures: {},
248+
} as Transaction;
249+
expect(() => assertIsTransactionWithDurableNonceLifetime(transaction)).toThrow();
250+
});
251+
it('throws for a transaction with a nonceAccountAddress but no nonce in lifetimeConstraint', () => {
252+
const transaction = {
253+
lifetimeConstraint: {
254+
nonceAccountAddress: validAddress,
255+
},
256+
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
257+
signatures: {},
258+
} as Transaction;
259+
expect(() => assertIsTransactionWithDurableNonceLifetime(transaction)).toThrow();
260+
});
261+
it('throws for a transaction with a durable nonce lifetime but an invalid nonceAccountAddress value', () => {
262+
const transaction = {
263+
lifetimeConstraint: {
264+
nonce: 'abcd' as Nonce,
265+
nonceAccountAddress: 'not a valid address' as Address,
266+
},
267+
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
268+
signatures: {},
269+
} as Transaction;
270+
expect(() => assertIsTransactionWithDurableNonceLifetime(transaction)).toThrow();
271+
});
272+
it('does not throw for a transaction with a valid durable nonce lifetime constraint', () => {
273+
const transaction = {
274+
lifetimeConstraint: {
275+
nonce: 'abcd' as Nonce,
276+
nonceAccountAddress: validAddress,
277+
},
278+
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
279+
signatures: {},
280+
} as Transaction;
281+
expect(() => assertIsTransactionWithDurableNonceLifetime(transaction)).not.toThrow();
282+
});
283+
});

packages/transactions/src/__typetests__/lifetime-typetest.ts

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
1+
import { Address } from '@solana/addresses';
2+
import { Blockhash } from '@solana/rpc-types';
13
import type {
4+
Nonce,
25
TransactionMessage,
36
TransactionMessageWithBlockhashLifetime,
47
TransactionMessageWithDurableNonceLifetime,
58
TransactionMessageWithLifetime,
69
} from '@solana/transaction-messages';
710

8-
import type {
9-
SetTransactionLifetimeFromTransactionMessage,
10-
TransactionWithBlockhashLifetime,
11-
TransactionWithDurableNonceLifetime,
12-
TransactionWithLifetime,
11+
import {
12+
assertIsTransactionWithBlockhashLifetime,
13+
assertIsTransactionWithDurableNonceLifetime,
14+
isTransactionWithBlockhashLifetime,
15+
isTransactionWithDurableNonceLifetime,
16+
type SetTransactionLifetimeFromTransactionMessage,
17+
type TransactionWithBlockhashLifetime,
18+
type TransactionWithDurableNonceLifetime,
19+
type TransactionWithLifetime,
1320
} from '../lifetime';
1421
import { Transaction } from '../transaction';
1522

@@ -71,3 +78,71 @@ import { Transaction } from '../transaction';
7178
null as unknown as Result satisfies Readonly<Transaction & TransactionWithBlockhashLifetime>;
7279
}
7380
}
81+
82+
// [DESCRIBE] isTransactionWithBlockhashLifetime
83+
{
84+
// It narrows the transaction type to one with a blockhash-based lifetime.
85+
{
86+
const transaction = null as unknown as Transaction & { some: 1 };
87+
if (isTransactionWithBlockhashLifetime(transaction)) {
88+
transaction satisfies Transaction & TransactionWithBlockhashLifetime & { some: 1 };
89+
} else {
90+
transaction satisfies Transaction & { some: 1 };
91+
// @ts-expect-error It does not have a blockhash-based lifetime.
92+
transaction satisfies TransactionWithBlockhashLifetime;
93+
}
94+
}
95+
}
96+
97+
// [DESCRIBE] assertIsTransactionWithBlockhashLifetime
98+
{
99+
// It narrows the transaction type to one with a blockhash-based lifetime.
100+
{
101+
const transaction = null as unknown as Transaction & { some: 1 };
102+
// @ts-expect-error Should not be blockhash lifetime
103+
transaction satisfies TransactionWithBlockhashLifetime;
104+
// @ts-expect-error Should not satisfy has blockhash
105+
transaction satisfies { lifetimeConstraint: { blockhash: Blockhash } };
106+
// @ts-expect-error Should not satisfy has lastValidBlockHeight
107+
transaction satisfies { lifetimeConstraint: { lastValidBlockHeight: bigint } };
108+
assertIsTransactionWithBlockhashLifetime(transaction);
109+
transaction satisfies Transaction & TransactionWithBlockhashLifetime & { some: 1 };
110+
transaction satisfies TransactionWithBlockhashLifetime;
111+
transaction satisfies { lifetimeConstraint: { blockhash: Blockhash } };
112+
transaction satisfies { lifetimeConstraint: { lastValidBlockHeight: bigint } };
113+
}
114+
}
115+
116+
// [DESCRIBE] isTransactionWithDurableNonceLifetime
117+
{
118+
// It narrows the transaction type to one with a nonce-based lifetime.
119+
{
120+
const transaction = null as unknown as Transaction & { some: 1 };
121+
if (isTransactionWithDurableNonceLifetime(transaction)) {
122+
transaction satisfies Transaction & TransactionWithDurableNonceLifetime & { some: 1 };
123+
} else {
124+
transaction satisfies Transaction & { some: 1 };
125+
// @ts-expect-error It does not have a nonce-based lifetime.
126+
transaction satisfies TransactionWithDurableNonceLifetime;
127+
}
128+
}
129+
}
130+
131+
// [DESCRIBE] assertIsTransactionWithDurableNonceLifetime
132+
{
133+
// It narrows the transaction type to one with a nonce-based lifetime.
134+
{
135+
const transaction = null as unknown as Transaction & { some: 1 };
136+
// @ts-expect-error Should not be durable nonce lifetime
137+
transaction satisfies TransactionWithDurableNonceLifetime;
138+
// @ts-expect-error Should not have a nonce-based lifetime
139+
transaction satisfies { lifetimeConstraint: { nonce: Nonce } };
140+
// @ts-expect-error Should not have a nonce account address
141+
transaction satisfies { lifetimeConstraint: { nonceAccountAddress: Address } };
142+
assertIsTransactionWithDurableNonceLifetime(transaction);
143+
transaction satisfies Transaction & TransactionWithDurableNonceLifetime & { some: 1 };
144+
transaction satisfies TransactionWithDurableNonceLifetime;
145+
transaction satisfies { lifetimeConstraint: { nonce: Nonce } };
146+
transaction satisfies { lifetimeConstraint: { nonceAccountAddress: Address } };
147+
}
148+
}

0 commit comments

Comments
 (0)