Skip to content

Commit c522714

Browse files
committed
Add TransactionPlanResult type and helpers
1 parent f193c09 commit c522714

6 files changed

Lines changed: 746 additions & 0 deletions

File tree

.changeset/afraid-tables-occur.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@solana/instruction-plans': patch
3+
---
4+
5+
Add new `TransactionPlanResult` type with helpers. This type describes the execution results of transaction plans with the same structural hierarchy — capturing the execution status of each transaction message whether executed in parallel, sequentially, or as a single transaction.

packages/instruction-plans/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"maintained node versions"
7373
],
7474
"dependencies": {
75+
"@solana/errors": "workspace:*",
7576
"@solana/instructions": "workspace:*",
7677
"@solana/transaction-messages": "workspace:*",
7778
"@solana/transactions": "workspace:*"
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import '@solana/test-matchers/toBeFrozenObject';
2+
3+
import { Address } from '@solana/addresses';
4+
import { SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE, SolanaError } from '@solana/errors';
5+
import { pipe } from '@solana/functional';
6+
import {
7+
CompilableTransactionMessage,
8+
setTransactionMessageLifetimeUsingBlockhash,
9+
} from '@solana/transaction-messages';
10+
import { createTransactionMessage, setTransactionMessageFeePayer } from '@solana/transaction-messages';
11+
import { Transaction } from '@solana/transactions';
12+
13+
import {
14+
canceledSingleTransactionPlanResult,
15+
failedSingleTransactionPlanResult,
16+
nonDivisibleSequentialTransactionPlanResult,
17+
parallelTransactionPlanResult,
18+
sequentialTransactionPlanResult,
19+
successfulSingleTransactionPlanResult,
20+
} from '../transaction-plan-result';
21+
22+
function createMessage<TId extends string>(id: TId): CompilableTransactionMessage & { id: TId } {
23+
return pipe(
24+
createTransactionMessage({ version: 0 }),
25+
m => setTransactionMessageFeePayer('E9Nykp3rSdza2moQutaJ3K3RSC8E5iFERX2SqLTsQfjJ' as Address, m),
26+
// TODO(loris): Either remove lifetime constraint or use the new
27+
// `fillMissingTransactionMessageLifetimeUsingProvisoryBlockhash`
28+
// function from https://github.com/anza-xyz/kit/pull/519.
29+
m =>
30+
setTransactionMessageLifetimeUsingBlockhash(
31+
{} as Parameters<typeof setTransactionMessageLifetimeUsingBlockhash>[0],
32+
m,
33+
),
34+
m => Object.freeze({ ...m, id }),
35+
);
36+
}
37+
38+
function createTransaction<TId extends string>(id: TId): Transaction & { id: TId } {
39+
return Object.freeze({ id }) as unknown as Transaction & { id: TId };
40+
}
41+
42+
describe('successfulSingleTransactionPlanResult', () => {
43+
it('creates SingleTransactionPlanResult objects with successful status', () => {
44+
const messageA = createMessage('A');
45+
const transactionA = createTransaction('A');
46+
const result = successfulSingleTransactionPlanResult(messageA, transactionA);
47+
expect(result).toEqual({
48+
kind: 'single',
49+
message: messageA,
50+
status: { context: {}, kind: 'successful', transaction: transactionA },
51+
});
52+
});
53+
it('accepts an optional context object', () => {
54+
const messageA = createMessage('A');
55+
const transactionA = createTransaction('A');
56+
const context = { foo: 'bar' };
57+
const result = successfulSingleTransactionPlanResult(messageA, transactionA, context);
58+
expect(result).toEqual({
59+
kind: 'single',
60+
message: messageA,
61+
status: { context, kind: 'successful', transaction: transactionA },
62+
});
63+
});
64+
it('freezes created SingleTransactionPlanResult objects', () => {
65+
const messageA = createMessage('A');
66+
const transactionA = createTransaction('A');
67+
const result = successfulSingleTransactionPlanResult(messageA, transactionA);
68+
expect(result).toBeFrozenObject();
69+
});
70+
it('freezes the status object of created SingleTransactionPlanResult objects', () => {
71+
const messageA = createMessage('A');
72+
const transactionA = createTransaction('A');
73+
const result = successfulSingleTransactionPlanResult(messageA, transactionA);
74+
expect(result.status).toBeFrozenObject();
75+
});
76+
});
77+
78+
describe('failedSingleTransactionPlanResult', () => {
79+
it('creates SingleTransactionPlanResult objects with failed status', () => {
80+
const messageA = createMessage('A');
81+
const error = new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE);
82+
const result = failedSingleTransactionPlanResult(messageA, error);
83+
expect(result).toEqual({
84+
kind: 'single',
85+
message: messageA,
86+
status: { error, kind: 'failed' },
87+
});
88+
});
89+
it('freezes created SingleTransactionPlanResult objects', () => {
90+
const messageA = createMessage('A');
91+
const error = new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE);
92+
const result = failedSingleTransactionPlanResult(messageA, error);
93+
expect(result).toBeFrozenObject();
94+
});
95+
it('freezes the status object of created SingleTransactionPlanResult objects', () => {
96+
const messageA = createMessage('A');
97+
const error = new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE);
98+
const result = failedSingleTransactionPlanResult(messageA, error);
99+
expect(result.status).toBeFrozenObject();
100+
});
101+
});
102+
103+
describe('canceledSingleTransactionPlanResult', () => {
104+
it('creates SingleTransactionPlanResult objects with canceled status', () => {
105+
const messageA = createMessage('A');
106+
const result = canceledSingleTransactionPlanResult(messageA);
107+
expect(result).toEqual({
108+
kind: 'single',
109+
message: messageA,
110+
status: { kind: 'canceled' },
111+
});
112+
});
113+
it('freezes created SingleTransactionPlanResult objects', () => {
114+
const messageA = createMessage('A');
115+
const result = canceledSingleTransactionPlanResult(messageA);
116+
expect(result).toBeFrozenObject();
117+
});
118+
it('freezes the status object of created SingleTransactionPlanResult objects', () => {
119+
const messageA = createMessage('A');
120+
const result = canceledSingleTransactionPlanResult(messageA);
121+
expect(result.status).toBeFrozenObject();
122+
});
123+
});
124+
125+
describe('parallelTransactionPlanResult', () => {
126+
it('creates ParallelTransactionPlanResult objects from other results', () => {
127+
const planA = canceledSingleTransactionPlanResult(createMessage('A'));
128+
const planB = canceledSingleTransactionPlanResult(createMessage('B'));
129+
const result = parallelTransactionPlanResult([planA, planB]);
130+
expect(result).toEqual({
131+
kind: 'parallel',
132+
plans: [planA, planB],
133+
});
134+
});
135+
it('can nest other result types', () => {
136+
const planA = canceledSingleTransactionPlanResult(createMessage('A'));
137+
const planB = canceledSingleTransactionPlanResult(createMessage('B'));
138+
const planC = canceledSingleTransactionPlanResult(createMessage('C'));
139+
const result = parallelTransactionPlanResult([planA, parallelTransactionPlanResult([planB, planC])]);
140+
expect(result).toEqual({
141+
kind: 'parallel',
142+
plans: [planA, { kind: 'parallel', plans: [planB, planC] }],
143+
});
144+
});
145+
it('freezes created ParallelTransactionPlanResult objects', () => {
146+
const planA = canceledSingleTransactionPlanResult(createMessage('A'));
147+
const planB = canceledSingleTransactionPlanResult(createMessage('B'));
148+
const result = parallelTransactionPlanResult([planA, planB]);
149+
expect(result).toBeFrozenObject();
150+
});
151+
});
152+
153+
describe('sequentialTransactionPlanResult', () => {
154+
it('creates divisible SequentialTransactionPlanResult objects from other results', () => {
155+
const planA = canceledSingleTransactionPlanResult(createMessage('A'));
156+
const planB = canceledSingleTransactionPlanResult(createMessage('B'));
157+
const result = sequentialTransactionPlanResult([planA, planB]);
158+
expect(result).toEqual({
159+
divisible: true,
160+
kind: 'sequential',
161+
plans: [planA, planB],
162+
});
163+
});
164+
it('can nest other result types', () => {
165+
const planA = canceledSingleTransactionPlanResult(createMessage('A'));
166+
const planB = canceledSingleTransactionPlanResult(createMessage('B'));
167+
const planC = canceledSingleTransactionPlanResult(createMessage('C'));
168+
const result = sequentialTransactionPlanResult([planA, sequentialTransactionPlanResult([planB, planC])]);
169+
expect(result).toEqual({
170+
divisible: true,
171+
kind: 'sequential',
172+
plans: [planA, { divisible: true, kind: 'sequential', plans: [planB, planC] }],
173+
});
174+
});
175+
it('freezes created SequentialTransactionPlanResult objects', () => {
176+
const planA = canceledSingleTransactionPlanResult(createMessage('A'));
177+
const planB = canceledSingleTransactionPlanResult(createMessage('B'));
178+
const result = sequentialTransactionPlanResult([planA, planB]);
179+
expect(result).toBeFrozenObject();
180+
});
181+
});
182+
183+
describe('nonDivisibleSequentialTransactionPlanResult', () => {
184+
it('creates non-divisible SequentialTransactionPlanResult objects from other results', () => {
185+
const planA = canceledSingleTransactionPlanResult(createMessage('A'));
186+
const planB = canceledSingleTransactionPlanResult(createMessage('B'));
187+
const result = nonDivisibleSequentialTransactionPlanResult([planA, planB]);
188+
expect(result).toEqual({
189+
divisible: false,
190+
kind: 'sequential',
191+
plans: [planA, planB],
192+
});
193+
});
194+
it('can nest other result types', () => {
195+
const planA = canceledSingleTransactionPlanResult(createMessage('A'));
196+
const planB = canceledSingleTransactionPlanResult(createMessage('B'));
197+
const planC = canceledSingleTransactionPlanResult(createMessage('C'));
198+
const result = nonDivisibleSequentialTransactionPlanResult([
199+
planA,
200+
nonDivisibleSequentialTransactionPlanResult([planB, planC]),
201+
]);
202+
expect(result).toEqual({
203+
divisible: false,
204+
kind: 'sequential',
205+
plans: [planA, { divisible: false, kind: 'sequential', plans: [planB, planC] }],
206+
});
207+
});
208+
it('freezes created SequentialTransactionPlanResult objects', () => {
209+
const planA = canceledSingleTransactionPlanResult(createMessage('A'));
210+
const planB = canceledSingleTransactionPlanResult(createMessage('B'));
211+
const result = nonDivisibleSequentialTransactionPlanResult([planA, planB]);
212+
expect(result).toBeFrozenObject();
213+
});
214+
});
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import type { SolanaError } from '@solana/errors';
2+
import type { CompilableTransactionMessage } from '@solana/transaction-messages';
3+
import type { Transaction } from '@solana/transactions';
4+
5+
import {
6+
canceledSingleTransactionPlanResult,
7+
failedSingleTransactionPlanResult,
8+
nonDivisibleSequentialTransactionPlanResult,
9+
ParallelTransactionPlanResult,
10+
parallelTransactionPlanResult,
11+
SequentialTransactionPlanResult,
12+
sequentialTransactionPlanResult,
13+
SingleTransactionPlanResult,
14+
successfulSingleTransactionPlanResult,
15+
TransactionPlanResult,
16+
} from '../transaction-plan-result';
17+
18+
const messageA = null as unknown as CompilableTransactionMessage & { id: 'A' };
19+
const messageB = null as unknown as CompilableTransactionMessage & { id: 'B' };
20+
const messageC = null as unknown as CompilableTransactionMessage & { id: 'C' };
21+
const transactionA = null as unknown as Transaction;
22+
const transactionB = null as unknown as Transaction;
23+
const error = null as unknown as SolanaError;
24+
25+
type CustomContext = { customData: string };
26+
27+
// [DESCRIBE] parallelTransactionPlanResult
28+
{
29+
// It satisfies ParallelTransactionPlanResult.
30+
{
31+
const result = parallelTransactionPlanResult([
32+
successfulSingleTransactionPlanResult(messageA, transactionA),
33+
successfulSingleTransactionPlanResult(messageB, transactionB),
34+
]);
35+
result satisfies ParallelTransactionPlanResult;
36+
result satisfies TransactionPlanResult;
37+
}
38+
39+
// It can work with custom context.
40+
{
41+
const result = parallelTransactionPlanResult([
42+
successfulSingleTransactionPlanResult(messageA, transactionA, { customData: 'A' }),
43+
successfulSingleTransactionPlanResult(messageB, transactionB, { customData: 'B' }),
44+
]);
45+
result satisfies ParallelTransactionPlanResult<CustomContext>;
46+
result satisfies TransactionPlanResult;
47+
}
48+
49+
// It can nest other result plans.
50+
{
51+
const result = parallelTransactionPlanResult([
52+
successfulSingleTransactionPlanResult(messageA, transactionA),
53+
parallelTransactionPlanResult([
54+
successfulSingleTransactionPlanResult(messageB, transactionB),
55+
canceledSingleTransactionPlanResult(messageC),
56+
]),
57+
]);
58+
result satisfies ParallelTransactionPlanResult;
59+
result satisfies TransactionPlanResult;
60+
}
61+
}
62+
63+
// [DESCRIBE] sequentialTransactionPlanResult
64+
{
65+
// It satisfies a divisible SequentialTransactionPlanResult.
66+
{
67+
const result = sequentialTransactionPlanResult([
68+
successfulSingleTransactionPlanResult(messageA, transactionA),
69+
successfulSingleTransactionPlanResult(messageB, transactionB),
70+
]);
71+
result satisfies SequentialTransactionPlanResult & { divisible: true };
72+
result satisfies TransactionPlanResult;
73+
}
74+
75+
// It can work with custom context.
76+
{
77+
const result = sequentialTransactionPlanResult([
78+
successfulSingleTransactionPlanResult(messageA, transactionA, { customData: 'A' }),
79+
successfulSingleTransactionPlanResult(messageB, transactionB, { customData: 'B' }),
80+
]);
81+
result satisfies SequentialTransactionPlanResult<CustomContext> & { divisible: true };
82+
result satisfies TransactionPlanResult;
83+
}
84+
85+
// It can nest other result plans.
86+
{
87+
const result = sequentialTransactionPlanResult([
88+
successfulSingleTransactionPlanResult(messageA, transactionA),
89+
sequentialTransactionPlanResult([
90+
successfulSingleTransactionPlanResult(messageB, transactionB),
91+
canceledSingleTransactionPlanResult(messageC),
92+
]),
93+
]);
94+
result satisfies SequentialTransactionPlanResult & { divisible: true };
95+
result satisfies TransactionPlanResult;
96+
}
97+
}
98+
99+
// [DESCRIBE] nonDivisibleSequentialTransactionPlanResult
100+
{
101+
// It satisfies a non-divisible SequentialTransactionPlanResult.
102+
{
103+
const result = nonDivisibleSequentialTransactionPlanResult([
104+
successfulSingleTransactionPlanResult(messageA, transactionA),
105+
successfulSingleTransactionPlanResult(messageB, transactionB),
106+
]);
107+
result satisfies SequentialTransactionPlanResult & { divisible: false };
108+
result satisfies TransactionPlanResult;
109+
}
110+
111+
// It can work with custom context.
112+
{
113+
const result = nonDivisibleSequentialTransactionPlanResult([
114+
successfulSingleTransactionPlanResult(messageA, transactionA, { customData: 'A' }),
115+
successfulSingleTransactionPlanResult(messageB, transactionB, { customData: 'B' }),
116+
]);
117+
result satisfies SequentialTransactionPlanResult<CustomContext> & { divisible: false };
118+
result satisfies TransactionPlanResult;
119+
}
120+
121+
// It can nest other result plans.
122+
{
123+
const result = nonDivisibleSequentialTransactionPlanResult([
124+
successfulSingleTransactionPlanResult(messageA, transactionA),
125+
nonDivisibleSequentialTransactionPlanResult([
126+
successfulSingleTransactionPlanResult(messageB, transactionB),
127+
canceledSingleTransactionPlanResult(messageC),
128+
]),
129+
]);
130+
result satisfies SequentialTransactionPlanResult & { divisible: false };
131+
result satisfies TransactionPlanResult;
132+
}
133+
}
134+
135+
// [DESCRIBE] successfulSingleTransactionPlanResult
136+
{
137+
// It satisfies SingleTransactionPlanResult with a successful status.
138+
{
139+
const result = successfulSingleTransactionPlanResult(messageA, transactionA);
140+
result satisfies SingleTransactionPlanResult<object, typeof messageA>;
141+
result satisfies TransactionPlanResult;
142+
}
143+
144+
// It can include a custom context.
145+
{
146+
const result = successfulSingleTransactionPlanResult(messageA, transactionA, { customData: 'test' });
147+
result satisfies SingleTransactionPlanResult<CustomContext, typeof messageA>;
148+
result satisfies TransactionPlanResult;
149+
}
150+
}
151+
152+
// [DESCRIBE] failedSingleTransactionPlanResult
153+
{
154+
// It satisfies SingleTransactionPlanResult with a failed status.
155+
{
156+
const result = failedSingleTransactionPlanResult(messageA, error);
157+
result satisfies SingleTransactionPlanResult<object, typeof messageA>;
158+
result satisfies TransactionPlanResult;
159+
}
160+
}
161+
162+
// [DESCRIBE] canceledSingleTransactionPlanResult
163+
{
164+
// It satisfies SingleTransactionPlanResult with a canceled status.
165+
{
166+
const result = canceledSingleTransactionPlanResult(messageA);
167+
result satisfies SingleTransactionPlanResult<object, typeof messageA>;
168+
result satisfies TransactionPlanResult;
169+
}
170+
}

0 commit comments

Comments
 (0)