Skip to content

Commit ae94ca3

Browse files
authored
refactor(experimental): add a function to assert a transaction has a blockhash lifetime (#1908)
1 parent d484358 commit ae94ca3

File tree

3 files changed

+137
-8
lines changed

3 files changed

+137
-8
lines changed

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

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import 'test-matchers/toBeFrozenObject';
33
import { Encoder } from '@solana/codecs-core';
44
import { getBase58Encoder } from '@solana/codecs-strings';
55

6-
import { Blockhash, ITransactionWithBlockhashLifetime, setTransactionLifetimeUsingBlockhash } from '../blockhash';
6+
import {
7+
assertIsTransactionWithBlockhashLifetime,
8+
Blockhash,
9+
ITransactionWithBlockhashLifetime,
10+
setTransactionLifetimeUsingBlockhash,
11+
} from '../blockhash';
712
import { ITransactionWithSignatures } from '../signatures';
813
import { BaseTransaction } from '../types';
914

@@ -109,6 +114,71 @@ describe('assertIsBlockhash()', () => {
109114
});
110115
});
111116

117+
describe('assertIsBlockhashLifetimeTransaction', () => {
118+
beforeEach(() => {
119+
// use real implementation
120+
jest.mocked(getBase58Encoder).mockReturnValue(originalGetBase58Encoder);
121+
});
122+
it('throws for a transaction with no lifetime constraint', () => {
123+
const transaction: BaseTransaction = {
124+
instructions: [],
125+
version: 0,
126+
};
127+
expect(() => assertIsTransactionWithBlockhashLifetime(transaction)).toThrow();
128+
});
129+
it('throws for a transaction with a durable nonce constraint', () => {
130+
const transaction = {
131+
instructions: [],
132+
lifetimeConstraint: {
133+
nonce: 'abcd',
134+
},
135+
version: 0,
136+
} as unknown as BaseTransaction;
137+
expect(() => assertIsTransactionWithBlockhashLifetime(transaction)).toThrow();
138+
});
139+
it('throws for a transaction with a blockhash but no lastValidBlockHeight in lifetimeConstraint', () => {
140+
const transaction = {
141+
instructions: [],
142+
lifetimeConstraint: {
143+
blockhash: '11111111111111111111111111111111',
144+
},
145+
version: 0,
146+
} as unknown as BaseTransaction;
147+
expect(() => assertIsTransactionWithBlockhashLifetime(transaction)).toThrow();
148+
});
149+
it('throws for a transaction with a lastValidBlockHeight but no blockhash in lifetimeConstraint', () => {
150+
const transaction = {
151+
instructions: [],
152+
lifetimeConstraint: {
153+
lastValidBlockHeight: 1234n,
154+
},
155+
version: 0,
156+
} as unknown as BaseTransaction;
157+
expect(() => assertIsTransactionWithBlockhashLifetime(transaction)).toThrow();
158+
});
159+
it('throws for a transaction with a blockhash lifetime but an invalid blockhash value', () => {
160+
const transaction = {
161+
instructions: [],
162+
lifetimeConstraint: {
163+
blockhash: 'not a valid blockhash value',
164+
},
165+
version: 0,
166+
} as unknown as BaseTransaction;
167+
expect(() => assertIsTransactionWithBlockhashLifetime(transaction)).toThrow();
168+
});
169+
it('does not throw for a transaction with a valid blockhash lifetime constraint', () => {
170+
const transaction = {
171+
instructions: [],
172+
lifetimeConstraint: {
173+
blockhash: '11111111111111111111111111111111',
174+
lastValidBlockHeight: 1234n,
175+
},
176+
version: 0,
177+
} as unknown as BaseTransaction;
178+
expect(() => assertIsTransactionWithBlockhashLifetime(transaction)).not.toThrow();
179+
});
180+
});
181+
112182
describe('setTransactionLifetimeUsingBlockhash', () => {
113183
let baseTx: BaseTransaction;
114184
const BLOCKHASH_CONSTRAINT_A = {

packages/transactions/src/__typetests__/transaction-typetests.ts

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Address } from '@solana/addresses';
22

33
import {
44
appendTransactionInstruction,
5+
assertIsTransactionWithBlockhashLifetime,
56
assertTransactionIsFullySigned,
67
Blockhash,
78
IDurableNonceTransaction,
@@ -18,7 +19,7 @@ import {
1819
import { createTransaction } from '../create-transaction';
1920
import { ITransactionWithFeePayer, setTransactionFeePayer } from '../fee-payer';
2021
import { CompiledMessage, compileMessage } from '../message';
21-
import { Transaction } from '../types';
22+
import { BaseTransaction, Transaction } from '../types';
2223
import { getUnsignedTransaction } from '../unsigned-transaction';
2324

2425
const mockFeePayer = null as unknown as Address<'feePayer'>;
@@ -538,10 +539,43 @@ async () => {
538539
} & ITransactionWithFeePayer<'feePayer'> &
539540
ITransactionWithSignatures;
540541

541-
// assertTransactionIsFullySigned
542-
const transaction = {} as Parameters<typeof assertTransactionIsFullySigned>[0];
543-
// @ts-expect-error Should not be fully signed
544-
transaction satisfies IFullySignedTransaction;
545-
assertTransactionIsFullySigned(transaction);
546-
transaction satisfies IFullySignedTransaction;
542+
{
543+
// assertTransactionIsFullySigned
544+
const transaction = {} as Parameters<typeof assertTransactionIsFullySigned>[0];
545+
// @ts-expect-error Should not be fully signed
546+
transaction satisfies IFullySignedTransaction;
547+
assertTransactionIsFullySigned(transaction);
548+
transaction satisfies IFullySignedTransaction;
549+
}
550+
551+
{
552+
// assertIsBlockhashLifetimeTransaction
553+
const transaction = null as unknown as BaseTransaction;
554+
// @ts-expect-error Should not be blockhash lifetime
555+
transaction satisfies ITransactionWithBlockhashLifetime;
556+
// @ts-expect-error Should not satisfy has blockhash
557+
transaction satisfies {
558+
lifetimeConstraint: {
559+
blockhash: Blockhash;
560+
};
561+
};
562+
// @ts-expect-error Should not satisfy has lastValidBlockHeight
563+
transaction satisfies {
564+
lifetimeConstraint: {
565+
lastValidBlockHeight: bigint;
566+
};
567+
};
568+
assertIsTransactionWithBlockhashLifetime(transaction);
569+
transaction satisfies ITransactionWithBlockhashLifetime;
570+
transaction satisfies {
571+
lifetimeConstraint: {
572+
blockhash: Blockhash;
573+
};
574+
};
575+
transaction satisfies {
576+
lifetimeConstraint: {
577+
lastValidBlockHeight: bigint;
578+
};
579+
};
580+
}
547581
};

packages/transactions/src/blockhash.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,31 @@ export function assertIsBlockhash(putativeBlockhash: string): asserts putativeBl
4444
}
4545
}
4646

47+
function isTransactionWithBlockhashLifetime(
48+
transaction: BaseTransaction | (BaseTransaction & ITransactionWithBlockhashLifetime)
49+
): transaction is BaseTransaction & ITransactionWithBlockhashLifetime {
50+
const lifetimeConstraintShapeMatches =
51+
'lifetimeConstraint' in transaction &&
52+
typeof transaction.lifetimeConstraint.blockhash === 'string' &&
53+
typeof transaction.lifetimeConstraint.lastValidBlockHeight === 'bigint';
54+
if (!lifetimeConstraintShapeMatches) return false;
55+
try {
56+
assertIsBlockhash(transaction.lifetimeConstraint.blockhash);
57+
return true;
58+
} catch {
59+
return false;
60+
}
61+
}
62+
63+
export function assertIsTransactionWithBlockhashLifetime(
64+
transaction: BaseTransaction | (BaseTransaction & ITransactionWithBlockhashLifetime)
65+
): asserts transaction is BaseTransaction & ITransactionWithBlockhashLifetime {
66+
if (!isTransactionWithBlockhashLifetime(transaction)) {
67+
// TODO: Coded error.
68+
throw new Error('Transaction does not have a blockhash lifetime');
69+
}
70+
}
71+
4772
export function setTransactionLifetimeUsingBlockhash<TTransaction extends BaseTransaction & IDurableNonceTransaction>(
4873
blockhashLifetimeConstraint: BlockhashLifetimeConstraint,
4974
transaction: TTransaction | (TTransaction & ITransactionWithSignatures),

0 commit comments

Comments
 (0)