Skip to content

Commit 04da52a

Browse files
committed
Improve type safety when adding instructions
1 parent 5e72f5a commit 04da52a

File tree

8 files changed

+336
-50
lines changed

8 files changed

+336
-50
lines changed

.changeset/cold-beans-relax.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@solana/transaction-messages': patch
3+
---
4+
5+
Keep type safety when appending or prepending instructions to transaction messages

packages/transaction-messages/src/__typetests__/durable-nonce-typetests.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import type { Address } from '@solana/addresses';
2+
import { pipe } from '@solana/functional';
3+
import { IInstruction } from '@solana/instructions';
24

5+
import { CompilableTransactionMessage } from '../compilable-transaction-message';
6+
import { createTransactionMessage } from '../create-transaction-message';
37
import {
48
assertIsTransactionMessageWithDurableNonceLifetime,
59
isTransactionMessageWithDurableNonceLifetime,
@@ -8,6 +12,8 @@ import {
812
TransactionMessageWithDurableNonceLifetime,
913
} from '../durable-nonce';
1014
import { AdvanceNonceAccountInstruction } from '../durable-nonce-instruction';
15+
import { setTransactionMessageFeePayer } from '../fee-payer';
16+
import { appendTransactionMessageInstruction } from '../instructions';
1117
import { BaseTransactionMessage, TransactionMessage } from '../transaction-message';
1218

1319
const mockNonceConfig = {
@@ -16,6 +22,13 @@ const mockNonceConfig = {
1622
nonceAuthorityAddress: null as unknown as Address<'nonceAuthority'>,
1723
};
1824

25+
const newMockNonceConfig = {
26+
nonce: null as unknown as Nonce<'newNonce'>,
27+
nonceAccountAddress: null as unknown as Address<'newNonce'>,
28+
nonceAuthorityAddress: null as unknown as Address<'newNonceAuthority'>,
29+
};
30+
31+
type InstructionA = IInstruction & { identifier: 'A' };
1932
type LegacyTransactionMessage = Extract<TransactionMessage, { version: 'legacy' }>;
2033
type V0TransactionMessage = Extract<TransactionMessage, { version: 0 }>;
2134

@@ -72,4 +85,43 @@ type V0TransactionMessage = Extract<TransactionMessage, { version: 0 }>;
7285
// @ts-expect-error Should not be a v0 message.
7386
newMessage satisfies TransactionMessageWithDurableNonceLifetime & V0TransactionMessage & { some: 1 };
7487
}
88+
89+
// It prepends the nonce instruction to the transaction message.
90+
{
91+
const feePayer = null as unknown as Address;
92+
const message = pipe(
93+
createTransactionMessage({ version: 0 }),
94+
m => setTransactionMessageFeePayer(feePayer, m),
95+
m => appendTransactionMessageInstruction(null as unknown as InstructionA, m),
96+
m => setTransactionMessageLifetimeUsingDurableNonce(mockNonceConfig, m),
97+
);
98+
99+
message satisfies CompilableTransactionMessage;
100+
message satisfies BaseTransactionMessage &
101+
TransactionMessageWithDurableNonceLifetime<'nonce', 'nonceAuthority', 'nonce'>;
102+
message.instructions satisfies readonly [
103+
AdvanceNonceAccountInstruction<'nonce', 'nonceAuthority'>,
104+
InstructionA,
105+
];
106+
}
107+
108+
// It replaces the existing nonce instruction with the new one.
109+
{
110+
const feePayer = null as unknown as Address;
111+
const message = pipe(
112+
createTransactionMessage({ version: 0 }),
113+
m => setTransactionMessageFeePayer(feePayer, m),
114+
m => appendTransactionMessageInstruction(null as unknown as InstructionA, m),
115+
m => setTransactionMessageLifetimeUsingDurableNonce(mockNonceConfig, m),
116+
m => setTransactionMessageLifetimeUsingDurableNonce(newMockNonceConfig, m),
117+
);
118+
119+
message satisfies CompilableTransactionMessage;
120+
message satisfies BaseTransactionMessage &
121+
TransactionMessageWithDurableNonceLifetime<'newNonce', 'newNonceAuthority', 'newNonce'>;
122+
message.instructions satisfies readonly [
123+
AdvanceNonceAccountInstruction<'newNonce', 'newNonceAuthority'>,
124+
InstructionA,
125+
];
126+
}
75127
}

packages/transaction-messages/src/__typetests__/instructions-typetests.ts

Lines changed: 165 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1-
import { TransactionMessageWithBlockhashLifetime } from '../blockhash';
2-
import { TransactionMessageWithDurableNonceLifetime } from '../durable-nonce';
1+
import { Address } from '@solana/addresses';
2+
import { pipe } from '@solana/functional';
3+
4+
import { setTransactionMessageLifetimeUsingBlockhash, TransactionMessageWithBlockhashLifetime } from '../blockhash';
5+
import { CompilableTransactionMessage } from '../compilable-transaction-message';
6+
import { createTransactionMessage } from '../create-transaction-message';
7+
import {
8+
setTransactionMessageLifetimeUsingDurableNonce,
9+
TransactionMessageWithDurableNonceLifetime,
10+
} from '../durable-nonce';
11+
import { AdvanceNonceAccountInstruction } from '../durable-nonce-instruction';
12+
import { setTransactionMessageFeePayer } from '../fee-payer';
313
import {
414
appendTransactionMessageInstruction,
515
appendTransactionMessageInstructions,
@@ -9,6 +19,9 @@ import {
919
import { BaseTransactionMessage } from '../transaction-message';
1020

1121
type IInstruction = BaseTransactionMessage['instructions'][number];
22+
type InstructionA = IInstruction & { identifier: 'A' };
23+
type InstructionB = IInstruction & { identifier: 'B' };
24+
type InstructionC = IInstruction & { identifier: 'C' };
1225

1326
// [DESCRIBE] appendTransactionMessageInstruction
1427
{
@@ -18,6 +31,56 @@ type IInstruction = BaseTransactionMessage['instructions'][number];
1831
const newMessage = appendTransactionMessageInstruction(null as unknown as IInstruction, message);
1932
newMessage satisfies BaseTransactionMessage & { some: 1 };
2033
}
34+
35+
// It concatenates the instruction types
36+
{
37+
const message = null as unknown as { instructions: [InstructionA]; version: 0 };
38+
const newMessage = appendTransactionMessageInstruction(null as unknown as InstructionB, message);
39+
newMessage.instructions satisfies readonly [InstructionA, InstructionB];
40+
// @ts-expect-error Wrong order.
41+
newMessage.instructions satisfies readonly [InstructionB, InstructionA];
42+
// @ts-expect-error Not readonly.
43+
newMessage.instructions satisfies [InstructionA, InstructionB];
44+
}
45+
46+
// It adds instruction types to base transaction messages
47+
{
48+
const message = null as unknown as BaseTransactionMessage;
49+
const newMessage = appendTransactionMessageInstruction(null as unknown as InstructionA, message);
50+
newMessage.instructions satisfies readonly [...IInstruction[], InstructionA];
51+
}
52+
53+
// It keeps the blockhash lifetime type safety.
54+
{
55+
const feePayer = null as unknown as Address;
56+
const blockhash = null as unknown as Parameters<typeof setTransactionMessageLifetimeUsingBlockhash>[0];
57+
const message = pipe(
58+
createTransactionMessage({ version: 0 }),
59+
m => setTransactionMessageFeePayer(feePayer, m),
60+
m => setTransactionMessageLifetimeUsingBlockhash(blockhash, m),
61+
m => appendTransactionMessageInstruction(null as unknown as InstructionA, m),
62+
);
63+
64+
message satisfies CompilableTransactionMessage;
65+
message satisfies BaseTransactionMessage & TransactionMessageWithBlockhashLifetime;
66+
message.instructions satisfies readonly [InstructionA];
67+
}
68+
69+
// It keeps the durable nonce lifetime type safety.
70+
{
71+
const feePayer = null as unknown as Address;
72+
const nonceConfig = null as unknown as Parameters<typeof setTransactionMessageLifetimeUsingDurableNonce>[0];
73+
const message = pipe(
74+
createTransactionMessage({ version: 0 }),
75+
m => setTransactionMessageFeePayer(feePayer, m),
76+
m => setTransactionMessageLifetimeUsingDurableNonce(nonceConfig, m),
77+
m => appendTransactionMessageInstruction(null as unknown as InstructionA, m),
78+
);
79+
80+
message satisfies CompilableTransactionMessage;
81+
message satisfies BaseTransactionMessage & TransactionMessageWithDurableNonceLifetime;
82+
message.instructions satisfies readonly [AdvanceNonceAccountInstruction, InstructionA];
83+
}
2184
}
2285

2386
// [DESCRIBE] appendTransactionMessageInstructions
@@ -28,6 +91,30 @@ type IInstruction = BaseTransactionMessage['instructions'][number];
2891
const newMessage = appendTransactionMessageInstructions(null as unknown as IInstruction[], message);
2992
newMessage satisfies BaseTransactionMessage & { some: 1 };
3093
}
94+
95+
// It concatenates the instruction types
96+
{
97+
const message = null as unknown as { instructions: [InstructionA]; version: 0 };
98+
const newMessage = appendTransactionMessageInstructions(
99+
[null as unknown as InstructionB, null as unknown as InstructionC],
100+
message,
101+
);
102+
newMessage.instructions satisfies readonly [InstructionA, InstructionB, InstructionC];
103+
// @ts-expect-error Wrong order.
104+
newMessage.instructions satisfies readonly [InstructionC, InstructionB, InstructionA];
105+
// @ts-expect-error Not readonly.
106+
newMessage.instructions satisfies [InstructionA, InstructionB, InstructionC];
107+
}
108+
109+
// It adds instruction types to base transaction messages
110+
{
111+
const message = null as unknown as BaseTransactionMessage;
112+
const newMessage = appendTransactionMessageInstructions(
113+
[null as unknown as InstructionA, null as unknown as InstructionB],
114+
message,
115+
);
116+
newMessage.instructions satisfies readonly [...IInstruction[], InstructionA, InstructionB];
117+
}
31118
}
32119

33120
// [DESCRIBE] prependTransactionMessageInstruction
@@ -56,6 +143,58 @@ type IInstruction = BaseTransactionMessage['instructions'][number];
56143
const newMessage = prependTransactionMessageInstruction(null as unknown as IInstruction, message);
57144
newMessage satisfies BaseTransactionMessage & TransactionMessageWithBlockhashLifetime & { some: 1 };
58145
}
146+
147+
// It concatenates the instruction types
148+
{
149+
const message = null as unknown as { instructions: [InstructionA]; version: 0 };
150+
const newMessage = prependTransactionMessageInstruction(null as unknown as InstructionB, message);
151+
newMessage.instructions satisfies readonly [InstructionB, InstructionA];
152+
// @ts-expect-error Wrong order.
153+
newMessage.instructions satisfies readonly [InstructionA, InstructionB];
154+
// @ts-expect-error Not readonly.
155+
newMessage.instructions satisfies [InstructionB, InstructionA];
156+
}
157+
158+
// It adds instruction types to base transaction messages
159+
{
160+
const message = null as unknown as BaseTransactionMessage;
161+
const newMessage = prependTransactionMessageInstruction(null as unknown as InstructionA, message);
162+
newMessage.instructions satisfies readonly [InstructionA, ...IInstruction[]];
163+
}
164+
165+
// It keeps the blockhash lifetime type safety.
166+
{
167+
const feePayer = null as unknown as Address;
168+
const blockhash = null as unknown as Parameters<typeof setTransactionMessageLifetimeUsingBlockhash>[0];
169+
const message = pipe(
170+
createTransactionMessage({ version: 0 }),
171+
m => setTransactionMessageFeePayer(feePayer, m),
172+
m => setTransactionMessageLifetimeUsingBlockhash(blockhash, m),
173+
m => prependTransactionMessageInstruction(null as unknown as InstructionA, m),
174+
);
175+
176+
message satisfies CompilableTransactionMessage;
177+
message satisfies BaseTransactionMessage & TransactionMessageWithBlockhashLifetime;
178+
message.instructions satisfies readonly [InstructionA];
179+
}
180+
181+
// It removes the durable nonce lifetime type safety but keep the nonce instruction.
182+
{
183+
const feePayer = null as unknown as Address;
184+
const nonceConfig = null as unknown as Parameters<typeof setTransactionMessageLifetimeUsingDurableNonce>[0];
185+
const message = pipe(
186+
createTransactionMessage({ version: 0 }),
187+
m => setTransactionMessageFeePayer(feePayer, m),
188+
m => setTransactionMessageLifetimeUsingDurableNonce(nonceConfig, m),
189+
m => prependTransactionMessageInstruction(null as unknown as InstructionA, m),
190+
);
191+
192+
message.instructions satisfies readonly [InstructionA, AdvanceNonceAccountInstruction];
193+
// @ts-expect-error No longer a durable nonce lifetime.
194+
message satisfies CompilableTransactionMessage;
195+
// @ts-expect-error No longer a durable nonce lifetime.
196+
message satisfies BaseTransactionMessage & TransactionMessageWithDurableNonceLifetime;
197+
}
59198
}
60199

61200
// [DESCRIBE] prependTransactionMessageInstructions
@@ -76,4 +215,28 @@ type IInstruction = BaseTransactionMessage['instructions'][number];
76215
// @ts-expect-error The durable nonce transaction message type should be stripped.
77216
newMessage satisfies TransactionMessageWithDurableNonceLifetime;
78217
}
218+
219+
// It concatenates the instruction types
220+
{
221+
const message = null as unknown as { instructions: [InstructionA]; version: 0 };
222+
const newMessage = prependTransactionMessageInstructions(
223+
[null as unknown as InstructionB, null as unknown as InstructionC],
224+
message,
225+
);
226+
newMessage.instructions satisfies readonly [InstructionB, InstructionC, InstructionA];
227+
// @ts-expect-error Wrong order.
228+
newMessage.instructions satisfies readonly [InstructionA, InstructionC, InstructionB];
229+
// @ts-expect-error Not readonly.
230+
newMessage.instructions satisfies [InstructionB, InstructionC, InstructionA];
231+
}
232+
233+
// It adds instruction types to base transaction messages
234+
{
235+
const message = null as unknown as BaseTransactionMessage;
236+
const newMessage = prependTransactionMessageInstructions(
237+
[null as unknown as InstructionA, null as unknown as InstructionB],
238+
message,
239+
);
240+
newMessage.instructions satisfies readonly [InstructionA, InstructionB, ...IInstruction[]];
241+
}
79242
}

packages/transaction-messages/src/blockhash.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,20 +120,20 @@ export function setTransactionMessageLifetimeUsingBlockhash<
120120
blockhashLifetimeConstraint: BlockhashLifetimeConstraint,
121121
transactionMessage: TTransactionMessage,
122122
): ExcludeTransactionMessageLifetime<TTransactionMessage> & TransactionMessageWithBlockhashLifetime {
123+
type ReturnType = ExcludeTransactionMessageLifetime<TTransactionMessage> & TransactionMessageWithBlockhashLifetime;
124+
123125
if (
124126
'lifetimeConstraint' in transactionMessage &&
125127
transactionMessage.lifetimeConstraint &&
126128
'blockhash' in transactionMessage.lifetimeConstraint &&
127129
transactionMessage.lifetimeConstraint.blockhash === blockhashLifetimeConstraint.blockhash &&
128130
transactionMessage.lifetimeConstraint.lastValidBlockHeight === blockhashLifetimeConstraint.lastValidBlockHeight
129131
) {
130-
return transactionMessage as ExcludeTransactionMessageLifetime<TTransactionMessage> &
131-
TransactionMessageWithBlockhashLifetime;
132+
return transactionMessage as ReturnType;
132133
}
133-
const out = {
134+
135+
return Object.freeze({
134136
...transactionMessage,
135137
lifetimeConstraint: Object.freeze(blockhashLifetimeConstraint),
136-
};
137-
Object.freeze(out);
138-
return out as ExcludeTransactionMessageLifetime<TTransactionMessage> & TransactionMessageWithBlockhashLifetime;
138+
}) as ReturnType;
139139
}

packages/transaction-messages/src/create-transaction-message.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ type TransactionConfig<TVersion extends TransactionVersion> = Readonly<{
1717
*/
1818
export function createTransactionMessage<TVersion extends TransactionVersion>(
1919
config: TransactionConfig<TVersion>,
20-
): Extract<TransactionMessage, { version: TVersion }>;
20+
): Omit<Extract<TransactionMessage, { version: TVersion }>, 'instructions'> & { instructions: readonly [] };
2121
export function createTransactionMessage<TVersion extends TransactionVersion>({
2222
version,
2323
}: TransactionConfig<TVersion>): TransactionMessage {

packages/transaction-messages/src/decompile-message.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ import type { getCompiledAddressTableLookups } from './compile/address-table-loo
1818
import { createTransactionMessage } from './create-transaction-message';
1919
import { Nonce, setTransactionMessageLifetimeUsingDurableNonce } from './durable-nonce';
2020
import { isAdvanceNonceAccountInstruction } from './durable-nonce-instruction';
21-
import { setTransactionMessageFeePayer } from './fee-payer';
21+
import { setTransactionMessageFeePayer, TransactionMessageWithFeePayer } from './fee-payer';
2222
import { appendTransactionMessageInstruction } from './instructions';
23-
import { TransactionVersion } from './transaction-message';
23+
import { BaseTransactionMessage, TransactionVersion } from './transaction-message';
2424

2525
function getAccountMetas(message: CompiledTransactionMessage): IAccountMeta[] {
2626
const { header } = message;
@@ -241,9 +241,10 @@ export function decompileTransactionMessage(
241241
createTransactionMessage({ version: compiledTransactionMessage.version as TransactionVersion }),
242242
m => setTransactionMessageFeePayer(feePayer, m),
243243
m =>
244-
instructions.reduce((acc, instruction) => {
245-
return appendTransactionMessageInstruction(instruction, acc);
246-
}, m),
244+
instructions.reduce(
245+
(acc, instruction) => appendTransactionMessageInstruction(instruction, acc),
246+
m as BaseTransactionMessage & TransactionMessageWithFeePayer,
247+
),
247248
m =>
248249
'blockhash' in lifetimeConstraint
249250
? setTransactionMessageLifetimeUsingBlockhash(lifetimeConstraint, m)

0 commit comments

Comments
 (0)