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
59 changes: 59 additions & 0 deletions .changeset/thin-maps-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
'@solana/instruction-plans': major
---

Reshape `SingleTransactionPlanResult` from a single object type with a `status` discriminated union into three distinct types: `SuccessfulSingleTransactionPlanResult`, `FailedSingleTransactionPlanResult`, and `CanceledSingleTransactionPlanResult`. This flattens the result structure so that `status` is now a string literal (`'successful'`, `'failed'`, or `'canceled'`) and properties like `context`, `error`, and `signature` live at the top level of each variant.

Other changes include:

- Rename the `message` property to `plannedMessage` on all single transaction plan result types. This makes it clearer that this original planned message from the `TransactionPlan`, not the final message that was sent to the network.
- Move the `context` object from inside the `status` field to the top level of each result variant. All variants now carry a `context` — not just successful ones.
- Expand `TransactionPlanResultContext` to optionally include `message`, `signature`, and `transaction` properties.
- Remove the now-unused `TransactionPlanResultStatus` type.
- `failedSingleTransactionPlanResult` and `canceledSingleTransactionPlanResult` now accept an optional `context` parameter.

**BREAKING CHANGES**

**Accessing the status kind.** Replace `result.status.kind` with `result.status`.

```diff
- if (result.status.kind === 'successful') { /* ... */ }
+ if (result.status === 'successful') { /* ... */ }
```

**Accessing the signature.** The signature has moved from `result.status.signature` to `result.context.signature`.

```diff
- const sig = result.status.signature;
+ const sig = result.context.signature;
```

**Accessing the transaction.** The transaction has moved from `result.status.transaction` to `result.context.transaction`.

```diff
- const tx = result.status.transaction;
+ const tx = result.context.transaction;
```

**Accessing the error.** The error has moved from `result.status.error` to `result.error`.

```diff
- const err = result.status.error;
+ const err = result.error;
```

**Accessing the context.** The context has moved from `result.status.context` to `result.context`.

```diff
- const ctx = result.status.context;
+ const ctx = result.context;
```

**Accessing the message.** The `message` property has been renamed to `plannedMessage`.

```diff
- const msg = result.message;
+ const msg = result.plannedMessage;
```

**`TransactionPlanResultStatus` removed.** Code that references this type must be updated to use the individual result variant types (`SuccessfulSingleTransactionPlanResult`, `FailedSingleTransactionPlanResult`, `CanceledSingleTransactionPlanResult`) or the `SingleTransactionPlanResult` union directly.
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@ describe('successfulSingleTransactionPlanResult', () => {
const transactionA = createTransaction('A');
const result = successfulSingleTransactionPlanResult(messageA, transactionA);
expect(result).toEqual({
context: { signature: 'A', transaction: transactionA },
kind: 'single',
message: messageA,
status: { context: {}, kind: 'successful', signature: 'A', transaction: transactionA },
plannedMessage: messageA,
status: 'successful',
});
});
it('accepts an optional context object', () => {
Expand All @@ -58,9 +59,10 @@ describe('successfulSingleTransactionPlanResult', () => {
const context = { foo: 'bar' };
const result = successfulSingleTransactionPlanResult(messageA, transactionA, context);
expect(result).toEqual({
context: { ...context, signature: 'A', transaction: transactionA },
kind: 'single',
message: messageA,
status: { context, kind: 'successful', signature: 'A', transaction: transactionA },
plannedMessage: messageA,
status: 'successful',
});
});
it('freezes created SingleTransactionPlanResult objects', () => {
Expand All @@ -83,9 +85,10 @@ describe('successfulSingleTransactionPlanResultFromSignature', () => {
const signature = 'A' as Signature;
const result = successfulSingleTransactionPlanResultFromSignature(messageA, signature);
expect(result).toEqual({
context: { signature: 'A' },
kind: 'single',
message: messageA,
status: { context: {}, kind: 'successful', signature: 'A' },
plannedMessage: messageA,
status: 'successful',
});
});
it('accepts an optional context object', () => {
Expand All @@ -94,9 +97,10 @@ describe('successfulSingleTransactionPlanResultFromSignature', () => {
const context = { foo: 'bar' };
const result = successfulSingleTransactionPlanResultFromSignature(messageA, signature, context);
expect(result).toEqual({
context: { ...context, signature: 'A' },
kind: 'single',
message: messageA,
status: { context, kind: 'successful', signature: 'A' },
plannedMessage: messageA,
status: 'successful',
});
});
it('freezes created SingleTransactionPlanResult objects', () => {
Expand All @@ -119,9 +123,11 @@ describe('failedSingleTransactionPlanResult', () => {
const error = new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE);
const result = failedSingleTransactionPlanResult(messageA, error);
expect(result).toEqual({
context: {},
error,
kind: 'single',
message: messageA,
status: { error, kind: 'failed' },
plannedMessage: messageA,
status: 'failed',
});
});
it('freezes created SingleTransactionPlanResult objects', () => {
Expand All @@ -143,9 +149,10 @@ describe('canceledSingleTransactionPlanResult', () => {
const messageA = createMessage('A');
const result = canceledSingleTransactionPlanResult(messageA);
expect(result).toEqual({
context: {},
kind: 'single',
message: messageA,
status: { kind: 'canceled' },
plannedMessage: messageA,
status: 'canceled',
});
});
it('freezes created SingleTransactionPlanResult objects', () => {
Expand Down Expand Up @@ -917,7 +924,7 @@ describe('findTransactionPlanResult', () => {
const found = findTransactionPlanResult(
result,
// eslint-disable-next-line jest/no-conditional-in-test
r => r.kind === 'single' && r.status.kind === 'failed',
r => r.kind === 'single' && r.status === 'failed',
);
expect(found).toBe(failedResult);
});
Expand All @@ -932,7 +939,7 @@ describe('findTransactionPlanResult', () => {
const found = findTransactionPlanResult(
result,
// eslint-disable-next-line jest/no-conditional-in-test
r => r.kind === 'single' && r.status.kind === 'successful',
r => r.kind === 'single' && r.status === 'successful',
);
expect(found).toBe(successfulResult);
});
Expand All @@ -958,7 +965,7 @@ describe('everyTransactionPlanResult', () => {
it('matches successful single transaction plans', () => {
const plan = successfulSingleTransactionPlanResult(createMessage('A'), createTransaction('A'));
// eslint-disable-next-line jest/no-conditional-in-test
const result = everyTransactionPlanResult(plan, p => p.kind === 'single' && p.status.kind === 'successful');
const result = everyTransactionPlanResult(plan, p => p.kind === 'single' && p.status === 'successful');
expect(result).toBe(true);
});
it('matches failed single transaction plans', () => {
Expand All @@ -967,13 +974,13 @@ describe('everyTransactionPlanResult', () => {
new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE),
);
// eslint-disable-next-line jest/no-conditional-in-test
const result = everyTransactionPlanResult(plan, p => p.kind === 'single' && p.status.kind === 'failed');
const result = everyTransactionPlanResult(plan, p => p.kind === 'single' && p.status === 'failed');
expect(result).toBe(true);
});
it('matches canceled single transaction plans', () => {
const plan = canceledSingleTransactionPlanResult(createMessage('A'));
// eslint-disable-next-line jest/no-conditional-in-test
const result = everyTransactionPlanResult(plan, p => p.kind === 'single' && p.status.kind === 'canceled');
const result = everyTransactionPlanResult(plan, p => p.kind === 'single' && p.status === 'canceled');
expect(result).toBe(true);
});
it('matches sequential transaction plans', () => {
Expand Down Expand Up @@ -1006,7 +1013,7 @@ describe('everyTransactionPlanResult', () => {
const result = everyTransactionPlanResult(plan, p => {
// eslint-disable-next-line jest/no-conditional-in-test
if (p.kind !== 'single') return true;
const message = p.message as ReturnType<typeof createMessage>;
const message = p.plannedMessage as ReturnType<typeof createMessage>;
return ['A', 'B', 'C'].includes(message.id);
});
expect(result).toBe(true);
Expand All @@ -1023,7 +1030,7 @@ describe('everyTransactionPlanResult', () => {
nonDivisibleSequentialTransactionPlanResult([resultA, resultC]),
]);
// eslint-disable-next-line jest/no-conditional-in-test
const result = everyTransactionPlanResult(plan, p => p.kind !== 'single' || p.status.kind === 'successful');
const result = everyTransactionPlanResult(plan, p => p.kind !== 'single' || p.status === 'successful');
expect(result).toBe(false);
});
it('fails fast before evaluating children', () => {
Expand Down Expand Up @@ -1057,7 +1064,7 @@ describe('transformTransactionPlanResult', () => {
const plan = successfulSingleTransactionPlanResult(createMessage('A'), createTransaction('A'));
const transformedPlan = transformTransactionPlanResult(plan, p =>
// eslint-disable-next-line jest/no-conditional-in-test
p.kind === 'single' ? { ...p, message: { ...p.message, id: 'New A' } } : p,
p.kind === 'single' ? { ...p, plannedMessage: { ...p.plannedMessage, id: 'New A' } } : p,
);
expect(transformedPlan).toStrictEqual(
successfulSingleTransactionPlanResult(createMessage('New A'), createTransaction('A')),
Expand All @@ -1068,15 +1075,15 @@ describe('transformTransactionPlanResult', () => {
const plan = failedSingleTransactionPlanResult(createMessage('A'), error);
const transformedPlan = transformTransactionPlanResult(plan, p =>
// eslint-disable-next-line jest/no-conditional-in-test
p.kind === 'single' ? { ...p, message: { ...p.message, id: 'New A' } } : p,
p.kind === 'single' ? { ...p, plannedMessage: { ...p.plannedMessage, id: 'New A' } } : p,
);
expect(transformedPlan).toStrictEqual(failedSingleTransactionPlanResult(createMessage('New A'), error));
});
it('transforms canceled single transaction plan results', () => {
const plan = canceledSingleTransactionPlanResult(createMessage('A'));
const transformedPlan = transformTransactionPlanResult(plan, p =>
// eslint-disable-next-line jest/no-conditional-in-test
p.kind === 'single' ? { ...p, message: { ...p.message, id: 'New A' } } : p,
p.kind === 'single' ? { ...p, plannedMessage: { ...p.plannedMessage, id: 'New A' } } : p,
);
expect(transformedPlan).toStrictEqual(canceledSingleTransactionPlanResult(createMessage('New A')));
});
Expand Down Expand Up @@ -1146,15 +1153,15 @@ describe('transformTransactionPlanResult', () => {
transformTransactionPlanResult(plan, p => {
// eslint-disable-next-line jest/no-conditional-in-test
if (p.kind === 'single') {
const message = p.message as ReturnType<typeof createMessage>;
return { ...p, message: { ...message, id: `New ${message.id}` } };
const plannedMessage = p.plannedMessage as ReturnType<typeof createMessage>;
return { ...p, plannedMessage: { ...plannedMessage, id: `New ${plannedMessage.id}` } };
}
// eslint-disable-next-line jest/no-conditional-in-test
if (p.kind === 'sequential') {
const seenMessages = p.plans
const seenPlannedMessages = p.plans
.filter(subPlan => subPlan.kind === 'single')
.map(subPlan => subPlan.message as ReturnType<typeof createMessage>);
seenTransactionMessageIds.push(...seenMessages.map(message => message.id));
.map(subPlan => subPlan.plannedMessage as ReturnType<typeof createMessage>);
seenTransactionMessageIds.push(...seenPlannedMessages.map(m => m.id));
}
return p;
});
Expand Down
4 changes: 2 additions & 2 deletions packages/instruction-plans/src/transaction-plan-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
* This function traverses the transaction plan tree, executing each transaction
* message and collecting results that mirror the structure of the original plan.
*
* @typeParam TContext - The type of the context object that may be passed along with successful results.
* @typeParam TContext - The type of the context object that may be passed along with results.
* @param transactionPlan - The transaction plan to execute.
* @param config - Optional configuration object that can include an `AbortSignal` to cancel execution.
* @return A promise that resolves to the execution results.
Expand Down Expand Up @@ -219,7 +219,7 @@ async function traverseSingle(

function findErrorFromTransactionPlanResult(result: TransactionPlanResult): Error | undefined {
if (result.kind === 'single') {
return result.status.kind === 'failed' ? result.status.error : undefined;
return result.status === 'failed' ? result.error : undefined;
}
for (const plan of result.plans) {
const error = findErrorFromTransactionPlanResult(plan);
Expand Down
Loading