From 6711d7a75269344a8ada5452482a92dcbad53b1f Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 3 Feb 2026 12:02:26 +0000 Subject: [PATCH] Add `planType` to distinguish between `InstructionPlan`, `TransactionPlan` and `TransactionPlanResult` --- .changeset/little-paws-trade.md | 29 +++++++++ .../src/__tests__/__setup__.ts | 2 + .../src/__tests__/instruction-plan-test.ts | 54 ++++++++++++++- .../__tests__/transaction-plan-result-test.ts | 65 ++++++++++++++++++- .../src/__tests__/transaction-plan-test.ts | 51 ++++++++++++++- .../instruction-plan-typetest.ts | 12 ++++ .../transaction-plan-result-typetest.ts | 12 ++++ .../transaction-plan-typetest.ts | 12 ++++ .../instruction-plans/src/instruction-plan.ts | 46 ++++++++++++- .../src/transaction-plan-result.ts | 50 +++++++++++++- .../instruction-plans/src/transaction-plan.ts | 56 ++++++++++++++-- .../src/transaction-planner.ts | 10 +-- 12 files changed, 380 insertions(+), 19 deletions(-) create mode 100644 .changeset/little-paws-trade.md diff --git a/.changeset/little-paws-trade.md b/.changeset/little-paws-trade.md new file mode 100644 index 000000000..787d7511c --- /dev/null +++ b/.changeset/little-paws-trade.md @@ -0,0 +1,29 @@ +--- +'@solana/instruction-plans': major +--- + +Add a new `planType` property to all `InstructionPlan`, `TransactionPlan`, and `TransactionPlanResult` types to distinguish them from each other at runtime. This property is a string literal with the value `'instructionPlan'`, `'transactionPlan'`, or `'transactionPlanResult'` respectively. It also adds new type guard functions that make use of that new property: `isInstructionPlan`, `isTransactionPlan`, and `isTransactionPlanResult`. + +**BREAKING CHANGES** + +**`InstructionPlan`, `TransactionPlan`, and `TransactionPlanResult` type guards updated.** All factories have been updated to add the new `planType` property but any custom instantiation of these types must be updated to include it as well. + +```diff + const myInstructionPlan: InstructionPlan = { + kind: 'parallel', + plans: [/* ... */], ++ planType: 'instructionPlan', + }; + + const myTransactionPlan: TransactionPlan = { + kind: 'parallel', + plans: [/* ... */], ++ planType: 'transactionPlan', + }; + + const myTransactionPlanResult: TransactionPlanResult = { + kind: 'parallel', + plans: [/* ... */], ++ planType: 'transactionPlanResult', + }; +``` diff --git a/packages/instruction-plans/src/__tests__/__setup__.ts b/packages/instruction-plans/src/__tests__/__setup__.ts index 9fd445990..b63c06a91 100644 --- a/packages/instruction-plans/src/__tests__/__setup__.ts +++ b/packages/instruction-plans/src/__tests__/__setup__.ts @@ -42,6 +42,7 @@ export function createSingleInstructionAtATimeMessagePackerInstructionPlan( }; }, kind: 'messagePacker', + planType: 'instructionPlan', }); } @@ -139,5 +140,6 @@ export function createMessagePackerInstructionPlan( }; }, kind: 'messagePacker', + planType: 'instructionPlan', }); } diff --git a/packages/instruction-plans/src/__tests__/instruction-plan-test.ts b/packages/instruction-plans/src/__tests__/instruction-plan-test.ts index 77865a6af..dd943338c 100644 --- a/packages/instruction-plans/src/__tests__/instruction-plan-test.ts +++ b/packages/instruction-plans/src/__tests__/instruction-plan-test.ts @@ -29,6 +29,7 @@ import { getLinearMessagePackerInstructionPlan, getMessagePackerInstructionPlanFromInstructions, getReallocMessagePackerInstructionPlan, + isInstructionPlan, isMessagePackerInstructionPlan, isNonDivisibleSequentialInstructionPlan, isParallelInstructionPlan, @@ -55,7 +56,7 @@ describe('singleInstructionPlan', () => { it('creates SingleInstructionPlan objects', () => { const instruction = createInstruction('A'); const plan = singleInstructionPlan(instruction); - expect(plan).toStrictEqual({ instruction, kind: 'single' }); + expect(plan).toStrictEqual({ instruction, kind: 'single', planType: 'instructionPlan' }); }); it('freezes created SingleInstructionPlan objects', () => { const instruction = createInstruction('A'); @@ -74,6 +75,7 @@ describe('parallelInstructionPlan', () => { ]); expect(plan).toStrictEqual({ kind: 'parallel', + planType: 'instructionPlan', plans: [singleInstructionPlan(instructionA), singleInstructionPlan(instructionB)], }); }); @@ -83,6 +85,7 @@ describe('parallelInstructionPlan', () => { const plan = parallelInstructionPlan([instructionA, instructionB]); expect(plan).toStrictEqual({ kind: 'parallel', + planType: 'instructionPlan', plans: [singleInstructionPlan(instructionA), singleInstructionPlan(instructionB)], }); }); @@ -93,9 +96,14 @@ describe('parallelInstructionPlan', () => { const plan = parallelInstructionPlan([instructionA, parallelInstructionPlan([instructionB, instructionC])]); expect(plan).toStrictEqual({ kind: 'parallel', + planType: 'instructionPlan', plans: [ singleInstructionPlan(instructionA), - { kind: 'parallel', plans: [singleInstructionPlan(instructionB), singleInstructionPlan(instructionC)] }, + { + kind: 'parallel', + planType: 'instructionPlan', + plans: [singleInstructionPlan(instructionB), singleInstructionPlan(instructionC)], + }, ], }); }); @@ -118,6 +126,7 @@ describe('sequentialInstructionPlan', () => { expect(plan).toStrictEqual({ divisible: true, kind: 'sequential', + planType: 'instructionPlan', plans: [singleInstructionPlan(instructionA), singleInstructionPlan(instructionB)], }); }); @@ -128,6 +137,7 @@ describe('sequentialInstructionPlan', () => { expect(plan).toStrictEqual({ divisible: true, kind: 'sequential', + planType: 'instructionPlan', plans: [singleInstructionPlan(instructionA), singleInstructionPlan(instructionB)], }); }); @@ -139,11 +149,13 @@ describe('sequentialInstructionPlan', () => { expect(plan).toStrictEqual({ divisible: true, kind: 'sequential', + planType: 'instructionPlan', plans: [ singleInstructionPlan(instructionA), { divisible: true, kind: 'sequential', + planType: 'instructionPlan', plans: [singleInstructionPlan(instructionB), singleInstructionPlan(instructionC)], }, ], @@ -168,6 +180,7 @@ describe('nonDivisibleSequentialInstructionPlan', () => { expect(plan).toStrictEqual({ divisible: false, kind: 'sequential', + planType: 'instructionPlan', plans: [singleInstructionPlan(instructionA), singleInstructionPlan(instructionB)], }); }); @@ -178,6 +191,7 @@ describe('nonDivisibleSequentialInstructionPlan', () => { expect(plan).toStrictEqual({ divisible: false, kind: 'sequential', + planType: 'instructionPlan', plans: [singleInstructionPlan(instructionA), singleInstructionPlan(instructionB)], }); }); @@ -192,11 +206,13 @@ describe('nonDivisibleSequentialInstructionPlan', () => { expect(plan).toStrictEqual({ divisible: false, kind: 'sequential', + planType: 'instructionPlan', plans: [ singleInstructionPlan(instructionA), { divisible: false, kind: 'sequential', + planType: 'instructionPlan', plans: [singleInstructionPlan(instructionB), singleInstructionPlan(instructionC)], }, ], @@ -925,3 +941,37 @@ describe('flattenInstructionPlan', () => { ]); }); }); + +describe('isInstructionPlan', () => { + it('returns true for SingleInstructionPlan', () => { + expect(isInstructionPlan(singleInstructionPlan(createInstruction('A')))).toBe(true); + }); + it('returns true for ParallelInstructionPlan', () => { + expect(isInstructionPlan(parallelInstructionPlan([]))).toBe(true); + }); + it('returns true for SequentialInstructionPlan', () => { + expect(isInstructionPlan(sequentialInstructionPlan([]))).toBe(true); + }); + it('returns true for non-divisible SequentialInstructionPlan', () => { + expect(isInstructionPlan(nonDivisibleSequentialInstructionPlan([]))).toBe(true); + }); + it('returns true for MessagePackerInstructionPlan', () => { + expect(isInstructionPlan(getMessagePackerInstructionPlanFromInstructions([]))).toBe(true); + }); + it('returns false for non-objects', () => { + expect(isInstructionPlan(null)).toBe(false); + expect(isInstructionPlan(undefined)).toBe(false); + expect(isInstructionPlan('string')).toBe(false); + expect(isInstructionPlan(123)).toBe(false); + expect(isInstructionPlan(true)).toBe(false); + }); + it('returns false for objects without planType', () => { + expect(isInstructionPlan({ kind: 'single' })).toBe(false); + }); + it('returns false for objects with wrong planType', () => { + expect(isInstructionPlan({ planType: 123 })).toBe(false); + expect(isInstructionPlan({ planType: null })).toBe(false); + expect(isInstructionPlan({ planType: 'transactionPlan' })).toBe(false); + expect(isInstructionPlan({ planType: 'transactionPlanResult' })).toBe(false); + }); +}); diff --git a/packages/instruction-plans/src/__tests__/transaction-plan-result-test.ts b/packages/instruction-plans/src/__tests__/transaction-plan-result-test.ts index e3232a3c6..431b13f2a 100644 --- a/packages/instruction-plans/src/__tests__/transaction-plan-result-test.ts +++ b/packages/instruction-plans/src/__tests__/transaction-plan-result-test.ts @@ -31,6 +31,7 @@ import { isSingleTransactionPlanResult, isSuccessfulSingleTransactionPlanResult, isSuccessfulTransactionPlanResult, + isTransactionPlanResult, nonDivisibleSequentialTransactionPlanResult, parallelTransactionPlanResult, sequentialTransactionPlanResult, @@ -49,6 +50,7 @@ describe('successfulSingleTransactionPlanResultFromTransaction', () => { expect(result).toEqual({ context: { signature: 'A', transaction: transactionA }, kind: 'single', + planType: 'transactionPlanResult', plannedMessage: messageA, status: 'successful', }); @@ -61,6 +63,7 @@ describe('successfulSingleTransactionPlanResultFromTransaction', () => { expect(result).toEqual({ context: { ...context, signature: 'A', transaction: transactionA }, kind: 'single', + planType: 'transactionPlanResult', plannedMessage: messageA, status: 'successful', }); @@ -87,6 +90,7 @@ describe('successfulSingleTransactionPlanResult', () => { expect(result).toEqual({ context: { signature: 'A' }, kind: 'single', + planType: 'transactionPlanResult', plannedMessage: messageA, status: 'successful', }); @@ -99,6 +103,7 @@ describe('successfulSingleTransactionPlanResult', () => { expect(result).toEqual({ context: { ...context, signature: 'A' }, kind: 'single', + planType: 'transactionPlanResult', plannedMessage: messageA, status: 'successful', }); @@ -126,6 +131,7 @@ describe('failedSingleTransactionPlanResult', () => { context: {}, error, kind: 'single', + planType: 'transactionPlanResult', plannedMessage: messageA, status: 'failed', }); @@ -151,6 +157,7 @@ describe('canceledSingleTransactionPlanResult', () => { expect(result).toEqual({ context: {}, kind: 'single', + planType: 'transactionPlanResult', plannedMessage: messageA, status: 'canceled', }); @@ -174,6 +181,7 @@ describe('parallelTransactionPlanResult', () => { const result = parallelTransactionPlanResult([planA, planB]); expect(result).toEqual({ kind: 'parallel', + planType: 'transactionPlanResult', plans: [planA, planB], }); }); @@ -184,7 +192,8 @@ describe('parallelTransactionPlanResult', () => { const result = parallelTransactionPlanResult([planA, parallelTransactionPlanResult([planB, planC])]); expect(result).toEqual({ kind: 'parallel', - plans: [planA, { kind: 'parallel', plans: [planB, planC] }], + planType: 'transactionPlanResult', + plans: [planA, { kind: 'parallel', planType: 'transactionPlanResult', plans: [planB, planC] }], }); }); it('freezes created ParallelTransactionPlanResult objects', () => { @@ -203,6 +212,7 @@ describe('sequentialTransactionPlanResult', () => { expect(result).toEqual({ divisible: true, kind: 'sequential', + planType: 'transactionPlanResult', plans: [planA, planB], }); }); @@ -214,7 +224,11 @@ describe('sequentialTransactionPlanResult', () => { expect(result).toEqual({ divisible: true, kind: 'sequential', - plans: [planA, { divisible: true, kind: 'sequential', plans: [planB, planC] }], + planType: 'transactionPlanResult', + plans: [ + planA, + { divisible: true, kind: 'sequential', planType: 'transactionPlanResult', plans: [planB, planC] }, + ], }); }); it('freezes created SequentialTransactionPlanResult objects', () => { @@ -233,6 +247,7 @@ describe('nonDivisibleSequentialTransactionPlanResult', () => { expect(result).toEqual({ divisible: false, kind: 'sequential', + planType: 'transactionPlanResult', plans: [planA, planB], }); }); @@ -247,7 +262,11 @@ describe('nonDivisibleSequentialTransactionPlanResult', () => { expect(result).toEqual({ divisible: false, kind: 'sequential', - plans: [planA, { divisible: false, kind: 'sequential', plans: [planB, planC] }], + planType: 'transactionPlanResult', + plans: [ + planA, + { divisible: false, kind: 'sequential', planType: 'transactionPlanResult', plans: [planB, planC] }, + ], }); }); it('freezes created SequentialTransactionPlanResult objects', () => { @@ -1588,3 +1607,43 @@ describe('getFirstFailedSingleTransactionPlanResult', () => { expect(Object.prototype.propertyIsEnumerable.call(caughtError!.context, 'transactionPlanResult')).toBe(false); }); }); + +describe('isTransactionPlanResult', () => { + it('returns true for SuccessfulSingleTransactionPlanResult', () => { + const signature = 'A' as Signature; + expect(isTransactionPlanResult(successfulSingleTransactionPlanResult(createMessage('A'), { signature }))).toBe( + true, + ); + }); + it('returns true for FailedSingleTransactionPlanResult', () => { + expect(isTransactionPlanResult(failedSingleTransactionPlanResult(createMessage('A'), new Error()))).toBe(true); + }); + it('returns true for CanceledSingleTransactionPlanResult', () => { + expect(isTransactionPlanResult(canceledSingleTransactionPlanResult(createMessage('A')))).toBe(true); + }); + it('returns true for ParallelTransactionPlanResult', () => { + expect(isTransactionPlanResult(parallelTransactionPlanResult([]))).toBe(true); + }); + it('returns true for SequentialTransactionPlanResult', () => { + expect(isTransactionPlanResult(sequentialTransactionPlanResult([]))).toBe(true); + }); + it('returns true for non-divisible SequentialTransactionPlanResult', () => { + expect(isTransactionPlanResult(nonDivisibleSequentialTransactionPlanResult([]))).toBe(true); + }); + it('returns false for non-objects', () => { + expect(isTransactionPlanResult(null)).toBe(false); + expect(isTransactionPlanResult(undefined)).toBe(false); + expect(isTransactionPlanResult('string')).toBe(false); + expect(isTransactionPlanResult(123)).toBe(false); + expect(isTransactionPlanResult(true)).toBe(false); + }); + it('returns false for objects without planType', () => { + expect(isTransactionPlanResult({ kind: 'single', status: 'successful' })).toBe(false); + }); + it('returns false for objects with wrong planType', () => { + expect(isTransactionPlanResult({ planType: 123 })).toBe(false); + expect(isTransactionPlanResult({ planType: null })).toBe(false); + expect(isTransactionPlanResult({ planType: 'instructionPlan' })).toBe(false); + expect(isTransactionPlanResult({ planType: 'transactionPlan' })).toBe(false); + }); +}); diff --git a/packages/instruction-plans/src/__tests__/transaction-plan-test.ts b/packages/instruction-plans/src/__tests__/transaction-plan-test.ts index 4781a0b34..fdd3b1176 100644 --- a/packages/instruction-plans/src/__tests__/transaction-plan-test.ts +++ b/packages/instruction-plans/src/__tests__/transaction-plan-test.ts @@ -12,6 +12,7 @@ import { isParallelTransactionPlan, isSequentialTransactionPlan, isSingleTransactionPlan, + isTransactionPlan, nonDivisibleSequentialTransactionPlan, parallelTransactionPlan, sequentialTransactionPlan, @@ -24,7 +25,7 @@ describe('singleTransactionPlan', () => { it('creates SingleTransactionPlan objects', () => { const messageA = createMessage('A'); const plan = singleTransactionPlan(messageA); - expect(plan).toEqual({ kind: 'single', message: messageA }); + expect(plan).toEqual({ kind: 'single', message: messageA, planType: 'transactionPlan' }); }); it('freezes created SingleTransactionPlan objects', () => { const messageA = createMessage('A'); @@ -40,6 +41,7 @@ describe('parallelTransactionPlan', () => { const plan = parallelTransactionPlan([singleTransactionPlan(messageA), singleTransactionPlan(messageB)]); expect(plan).toEqual({ kind: 'parallel', + planType: 'transactionPlan', plans: [singleTransactionPlan(messageA), singleTransactionPlan(messageB)], }); }); @@ -49,6 +51,7 @@ describe('parallelTransactionPlan', () => { const plan = parallelTransactionPlan([messageA, messageB]); expect(plan).toEqual({ kind: 'parallel', + planType: 'transactionPlan', plans: [singleTransactionPlan(messageA), singleTransactionPlan(messageB)], }); }); @@ -59,9 +62,14 @@ describe('parallelTransactionPlan', () => { const plan = parallelTransactionPlan([messageA, parallelTransactionPlan([messageB, messageC])]); expect(plan).toEqual({ kind: 'parallel', + planType: 'transactionPlan', plans: [ singleTransactionPlan(messageA), - { kind: 'parallel', plans: [singleTransactionPlan(messageB), singleTransactionPlan(messageC)] }, + { + kind: 'parallel', + planType: 'transactionPlan', + plans: [singleTransactionPlan(messageB), singleTransactionPlan(messageC)], + }, ], }); }); @@ -81,6 +89,7 @@ describe('sequentialTransactionPlan', () => { expect(plan).toEqual({ divisible: true, kind: 'sequential', + planType: 'transactionPlan', plans: [singleTransactionPlan(messageA), singleTransactionPlan(messageB)], }); }); @@ -91,6 +100,7 @@ describe('sequentialTransactionPlan', () => { expect(plan).toEqual({ divisible: true, kind: 'sequential', + planType: 'transactionPlan', plans: [singleTransactionPlan(messageA), singleTransactionPlan(messageB)], }); }); @@ -102,11 +112,13 @@ describe('sequentialTransactionPlan', () => { expect(plan).toEqual({ divisible: true, kind: 'sequential', + planType: 'transactionPlan', plans: [ singleTransactionPlan(messageA), { divisible: true, kind: 'sequential', + planType: 'transactionPlan', plans: [singleTransactionPlan(messageB), singleTransactionPlan(messageC)], }, ], @@ -131,6 +143,7 @@ describe('nonDivisibleSequentialTransactionPlan', () => { expect(plan).toEqual({ divisible: false, kind: 'sequential', + planType: 'transactionPlan', plans: [singleTransactionPlan(messageA), singleTransactionPlan(messageB)], }); }); @@ -141,6 +154,7 @@ describe('nonDivisibleSequentialTransactionPlan', () => { expect(plan).toEqual({ divisible: false, kind: 'sequential', + planType: 'transactionPlan', plans: [singleTransactionPlan(messageA), singleTransactionPlan(messageB)], }); }); @@ -155,11 +169,13 @@ describe('nonDivisibleSequentialTransactionPlan', () => { expect(plan).toEqual({ divisible: false, kind: 'sequential', + planType: 'transactionPlan', plans: [ singleTransactionPlan(messageA), { divisible: false, kind: 'sequential', + planType: 'transactionPlan', plans: [singleTransactionPlan(messageB), singleTransactionPlan(messageC)], }, ], @@ -619,3 +635,34 @@ describe('transformTransactionPlan', () => { expect(transformedPlan).toBeFrozenObject(); }); }); + +describe('isTransactionPlan', () => { + it('returns true for SingleTransactionPlan', () => { + expect(isTransactionPlan(singleTransactionPlan(createMessage('A')))).toBe(true); + }); + it('returns true for ParallelTransactionPlan', () => { + expect(isTransactionPlan(parallelTransactionPlan([]))).toBe(true); + }); + it('returns true for SequentialTransactionPlan', () => { + expect(isTransactionPlan(sequentialTransactionPlan([]))).toBe(true); + }); + it('returns true for non-divisible SequentialTransactionPlan', () => { + expect(isTransactionPlan(nonDivisibleSequentialTransactionPlan([]))).toBe(true); + }); + it('returns false for non-objects', () => { + expect(isTransactionPlan(null)).toBe(false); + expect(isTransactionPlan(undefined)).toBe(false); + expect(isTransactionPlan('string')).toBe(false); + expect(isTransactionPlan(123)).toBe(false); + expect(isTransactionPlan(true)).toBe(false); + }); + it('returns false for objects without planType', () => { + expect(isTransactionPlan({ kind: 'single' })).toBe(false); + }); + it('returns false for objects with wrong planType', () => { + expect(isTransactionPlan({ planType: 123 })).toBe(false); + expect(isTransactionPlan({ planType: null })).toBe(false); + expect(isTransactionPlan({ planType: 'instructionPlan' })).toBe(false); + expect(isTransactionPlan({ planType: 'transactionPlanResult' })).toBe(false); + }); +}); diff --git a/packages/instruction-plans/src/__typetests__/instruction-plan-typetest.ts b/packages/instruction-plans/src/__typetests__/instruction-plan-typetest.ts index fd97a6865..7a31ebe97 100644 --- a/packages/instruction-plans/src/__typetests__/instruction-plan-typetest.ts +++ b/packages/instruction-plans/src/__typetests__/instruction-plan-typetest.ts @@ -11,6 +11,7 @@ import { getMessagePackerInstructionPlanFromInstructions, getReallocMessagePackerInstructionPlan, InstructionPlan, + isInstructionPlan, isMessagePackerInstructionPlan, isNonDivisibleSequentialInstructionPlan, isParallelInstructionPlan, @@ -246,3 +247,14 @@ const instructionC = null as unknown as Instruction & { id: 'C' }; plan satisfies ParallelInstructionPlan; } } + +// [DESCRIBE] isInstructionPlan +{ + // It narrows to any InstructionPlan. + { + const plan = null as unknown; + if (isInstructionPlan(plan)) { + plan satisfies InstructionPlan; + } + } +} diff --git a/packages/instruction-plans/src/__typetests__/transaction-plan-result-typetest.ts b/packages/instruction-plans/src/__typetests__/transaction-plan-result-typetest.ts index 9832cd957..9760d2eb6 100644 --- a/packages/instruction-plans/src/__typetests__/transaction-plan-result-typetest.ts +++ b/packages/instruction-plans/src/__typetests__/transaction-plan-result-typetest.ts @@ -24,6 +24,7 @@ import { isSingleTransactionPlanResult, isSuccessfulSingleTransactionPlanResult, isSuccessfulTransactionPlanResult, + isTransactionPlanResult, nonDivisibleSequentialTransactionPlanResult, ParallelTransactionPlanResult, parallelTransactionPlanResult, @@ -524,3 +525,14 @@ type CustomContext = { customData: string }; plan satisfies SuccessfulTransactionPlanResult; } } + +// [DESCRIBE] isTransactionPlanResult +{ + // It narrows to any TransactionPlanResult. + { + const plan = null as unknown; + if (isTransactionPlanResult(plan)) { + plan satisfies TransactionPlanResult; + } + } +} diff --git a/packages/instruction-plans/src/__typetests__/transaction-plan-typetest.ts b/packages/instruction-plans/src/__typetests__/transaction-plan-typetest.ts index ece7922f6..41a3f9082 100644 --- a/packages/instruction-plans/src/__typetests__/transaction-plan-typetest.ts +++ b/packages/instruction-plans/src/__typetests__/transaction-plan-typetest.ts @@ -10,6 +10,7 @@ import { isParallelTransactionPlan, isSequentialTransactionPlan, isSingleTransactionPlan, + isTransactionPlan, nonDivisibleSequentialTransactionPlan, ParallelTransactionPlan, parallelTransactionPlan, @@ -184,3 +185,14 @@ const messageC = null as unknown as TransactionMessage & TransactionMessageWithF plan satisfies ParallelTransactionPlan; } } + +// [DESCRIBE] isTransactionPlan +{ + // It narrows to any TransactionPlan. + { + const plan = null as unknown; + if (isTransactionPlan(plan)) { + plan satisfies TransactionPlan; + } + } +} diff --git a/packages/instruction-plans/src/instruction-plan.ts b/packages/instruction-plans/src/instruction-plan.ts index 7111bb255..fe55ba406 100644 --- a/packages/instruction-plans/src/instruction-plan.ts +++ b/packages/instruction-plans/src/instruction-plan.ts @@ -92,6 +92,7 @@ export type InstructionPlan = export type SequentialInstructionPlan = Readonly<{ divisible: boolean; kind: 'sequential'; + planType: 'instructionPlan'; plans: InstructionPlan[]; }>; @@ -126,6 +127,7 @@ export type SequentialInstructionPlan = Readonly<{ */ export type ParallelInstructionPlan = Readonly<{ kind: 'parallel'; + planType: 'instructionPlan'; plans: InstructionPlan[]; }>; @@ -147,6 +149,7 @@ export type ParallelInstructionPlan = Readonly<{ export type SingleInstructionPlan = Readonly<{ instruction: TInstruction; kind: 'single'; + planType: 'instructionPlan'; }>; /** @@ -206,6 +209,7 @@ export type SingleInstructionPlan MessagePacker; kind: 'messagePacker'; + planType: 'instructionPlan'; }>; /** @@ -276,6 +280,7 @@ export type MessagePacker = Readonly<{ export function parallelInstructionPlan(plans: (Instruction | InstructionPlan)[]): ParallelInstructionPlan { return Object.freeze({ kind: 'parallel', + planType: 'instructionPlan', plans: parseSingleInstructionPlans(plans), }); } @@ -307,6 +312,7 @@ export function sequentialInstructionPlan( return Object.freeze({ divisible: true, kind: 'sequential', + planType: 'instructionPlan', plans: parseSingleInstructionPlans(plans), }); } @@ -338,6 +344,7 @@ export function nonDivisibleSequentialInstructionPlan( return Object.freeze({ divisible: false, kind: 'sequential', + planType: 'instructionPlan', plans: parseSingleInstructionPlans(plans), }); } @@ -353,13 +360,48 @@ export function nonDivisibleSequentialInstructionPlan( * @see {@link SingleInstructionPlan} */ export function singleInstructionPlan(instruction: Instruction): SingleInstructionPlan { - return Object.freeze({ instruction, kind: 'single' }); + return Object.freeze({ instruction, kind: 'single', planType: 'instructionPlan' }); } function parseSingleInstructionPlans(plans: (Instruction | InstructionPlan)[]): InstructionPlan[] { return plans.map(plan => ('kind' in plan ? plan : singleInstructionPlan(plan))); } +/** + * Checks if the given value is an {@link InstructionPlan}. + * + * This type guard checks the `planType` property to determine if the value + * is an instruction plan. This is useful when you have a value that could be + * an {@link InstructionPlan}, {@link TransactionPlan}, or {@link TransactionPlanResult} + * and need to narrow the type. + * + * @param value - The value to check. + * @return `true` if the value is an instruction plan, `false` otherwise. + * + * @example + * ```ts + * function processItem(item: InstructionPlan | TransactionPlan | TransactionPlanResult) { + * if (isInstructionPlan(item)) { + * // item is narrowed to InstructionPlan + * console.log(item.kind); + * } + * } + * ``` + * + * @see {@link InstructionPlan} + * @see {@link isTransactionPlan} + * @see {@link isTransactionPlanResult} + */ +export function isInstructionPlan(value: unknown): value is InstructionPlan { + return ( + typeof value === 'object' && + value !== null && + 'planType' in value && + typeof value.planType === 'string' && + value.planType === 'instructionPlan' + ); +} + /** * Checks if the given instruction plan is a {@link SingleInstructionPlan}. * @@ -915,6 +957,7 @@ export function getLinearMessagePackerInstructionPlan({ }); }, kind: 'messagePacker', + planType: 'instructionPlan', }); } @@ -984,6 +1027,7 @@ export function getMessagePackerInstructionPlanFromInstructions = Readonly<{ divisible: boolean; kind: 'sequential'; + planType: 'transactionPlanResult'; plans: TransactionPlanResult[]; }>; @@ -206,6 +207,7 @@ export type ParallelTransactionPlanResult< >, > = Readonly<{ kind: 'parallel'; + planType: 'transactionPlanResult'; plans: TransactionPlanResult[]; }>; @@ -301,6 +303,7 @@ export type SuccessfulSingleTransactionPlanResult< > = { context: Readonly; kind: 'single'; + planType: 'transactionPlanResult'; plannedMessage: TTransactionMessage; status: 'successful'; }; @@ -345,6 +348,7 @@ export type FailedSingleTransactionPlanResult< context: Readonly; error: Error; kind: 'single'; + planType: 'transactionPlanResult'; plannedMessage: TTransactionMessage; status: 'failed'; }; @@ -384,6 +388,7 @@ export type CanceledSingleTransactionPlanResult< > = { context: Readonly; kind: 'single'; + planType: 'transactionPlanResult'; plannedMessage: TTransactionMessage; status: 'canceled'; }; @@ -412,7 +417,7 @@ export type CanceledSingleTransactionPlanResult< export function sequentialTransactionPlanResult< TContext extends TransactionPlanResultContext = TransactionPlanResultContext, >(plans: TransactionPlanResult[]): SequentialTransactionPlanResult & { divisible: true } { - return Object.freeze({ divisible: true, kind: 'sequential', plans }); + return Object.freeze({ divisible: true, kind: 'sequential', planType: 'transactionPlanResult', plans }); } /** @@ -439,7 +444,7 @@ export function sequentialTransactionPlanResult< export function nonDivisibleSequentialTransactionPlanResult< TContext extends TransactionPlanResultContext = TransactionPlanResultContext, >(plans: TransactionPlanResult[]): SequentialTransactionPlanResult & { divisible: false } { - return Object.freeze({ divisible: false, kind: 'sequential', plans }); + return Object.freeze({ divisible: false, kind: 'sequential', planType: 'transactionPlanResult', plans }); } /** @@ -465,7 +470,7 @@ export function nonDivisibleSequentialTransactionPlanResult< export function parallelTransactionPlanResult< TContext extends TransactionPlanResultContext = TransactionPlanResultContext, >(plans: TransactionPlanResult[]): ParallelTransactionPlanResult { - return Object.freeze({ kind: 'parallel', plans }); + return Object.freeze({ kind: 'parallel', planType: 'transactionPlanResult', plans }); } /** @@ -505,6 +510,7 @@ export function successfulSingleTransactionPlanResultFromTransaction< return Object.freeze({ context: Object.freeze({ ...((context ?? {}) as TContext), signature, transaction }), kind: 'single', + planType: 'transactionPlanResult', plannedMessage, status: 'successful', }); @@ -544,6 +550,7 @@ export function successfulSingleTransactionPlanResult< return Object.freeze({ context: Object.freeze({ ...context }), kind: 'single', + planType: 'transactionPlanResult', plannedMessage, status: 'successful', }); @@ -588,6 +595,7 @@ export function failedSingleTransactionPlanResult< context: Object.freeze({ ...((context ?? {}) as TContext) }), error, kind: 'single', + planType: 'transactionPlanResult', plannedMessage, status: 'failed', }); @@ -622,11 +630,47 @@ export function canceledSingleTransactionPlanResult< return Object.freeze({ context: Object.freeze({ ...((context ?? {}) as TContext) }), kind: 'single', + planType: 'transactionPlanResult', plannedMessage, status: 'canceled', }); } +/** + * Checks if the given value is a {@link TransactionPlanResult}. + * + * This type guard checks the `planType` property to determine if the value + * is a transaction plan result. This is useful when you have a value that could be + * an {@link InstructionPlan}, {@link TransactionPlan}, or {@link TransactionPlanResult} + * and need to narrow the type. + * + * @param value - The value to check. + * @return `true` if the value is a transaction plan result, `false` otherwise. + * + * @example + * ```ts + * function processItem(item: InstructionPlan | TransactionPlan | TransactionPlanResult) { + * if (isTransactionPlanResult(item)) { + * // item is narrowed to TransactionPlanResult + * console.log(item.kind); + * } + * } + * ``` + * + * @see {@link TransactionPlanResult} + * @see {@link isInstructionPlan} + * @see {@link isTransactionPlan} + */ +export function isTransactionPlanResult(value: unknown): value is TransactionPlanResult { + return ( + typeof value === 'object' && + value !== null && + 'planType' in value && + typeof value.planType === 'string' && + value.planType === 'transactionPlanResult' + ); +} + /** * Checks if the given transaction plan result is a {@link SingleTransactionPlanResult}. * diff --git a/packages/instruction-plans/src/transaction-plan.ts b/packages/instruction-plans/src/transaction-plan.ts index 7e1e03d5e..ece0ceedc 100644 --- a/packages/instruction-plans/src/transaction-plan.ts +++ b/packages/instruction-plans/src/transaction-plan.ts @@ -74,6 +74,7 @@ export type TransactionPlan = ParallelTransactionPlan | SequentialTransactionPla export type SequentialTransactionPlan = Readonly<{ divisible: boolean; kind: 'sequential'; + planType: 'transactionPlan'; plans: TransactionPlan[]; }>; @@ -110,6 +111,7 @@ export type SequentialTransactionPlan = Readonly<{ */ export type ParallelTransactionPlan = Readonly<{ kind: 'parallel'; + planType: 'transactionPlan'; plans: TransactionPlan[]; }>; @@ -134,6 +136,7 @@ export type SingleTransactionPlan< > = Readonly<{ kind: 'single'; message: TTransactionMessage; + planType: 'transactionPlan'; }>; /** @@ -162,7 +165,7 @@ export type SingleTransactionPlan< export function parallelTransactionPlan( plans: (TransactionPlan | (TransactionMessage & TransactionMessageWithFeePayer))[], ): ParallelTransactionPlan { - return Object.freeze({ kind: 'parallel', plans: parseSingleTransactionPlans(plans) }); + return Object.freeze({ kind: 'parallel', planType: 'transactionPlan', plans: parseSingleTransactionPlans(plans) }); } /** @@ -191,7 +194,12 @@ export function parallelTransactionPlan( export function sequentialTransactionPlan( plans: (TransactionPlan | (TransactionMessage & TransactionMessageWithFeePayer))[], ): SequentialTransactionPlan & { divisible: true } { - return Object.freeze({ divisible: true, kind: 'sequential', plans: parseSingleTransactionPlans(plans) }); + return Object.freeze({ + divisible: true, + kind: 'sequential', + planType: 'transactionPlan', + plans: parseSingleTransactionPlans(plans), + }); } /** @@ -220,7 +228,12 @@ export function sequentialTransactionPlan( export function nonDivisibleSequentialTransactionPlan( plans: (TransactionPlan | (TransactionMessage & TransactionMessageWithFeePayer))[], ): SequentialTransactionPlan & { divisible: false } { - return Object.freeze({ divisible: false, kind: 'sequential', plans: parseSingleTransactionPlans(plans) }); + return Object.freeze({ + divisible: false, + kind: 'sequential', + planType: 'transactionPlan', + plans: parseSingleTransactionPlans(plans), + }); } /** @@ -238,7 +251,7 @@ export function singleTransactionPlan< TTransactionMessage extends TransactionMessage & TransactionMessageWithFeePayer = TransactionMessage & TransactionMessageWithFeePayer, >(transactionMessage: TTransactionMessage): SingleTransactionPlan { - return Object.freeze({ kind: 'single', message: transactionMessage }); + return Object.freeze({ kind: 'single', message: transactionMessage, planType: 'transactionPlan' }); } function parseSingleTransactionPlans( @@ -247,6 +260,41 @@ function parseSingleTransactionPlans( return plans.map(plan => ('kind' in plan ? plan : singleTransactionPlan(plan))); } +/** + * Checks if the given value is a {@link TransactionPlan}. + * + * This type guard checks the `planType` property to determine if the value + * is a transaction plan. This is useful when you have a value that could be + * an {@link InstructionPlan}, {@link TransactionPlan}, or {@link TransactionPlanResult} + * and need to narrow the type. + * + * @param value - The value to check. + * @return `true` if the value is a transaction plan, `false` otherwise. + * + * @example + * ```ts + * function processItem(item: InstructionPlan | TransactionPlan | TransactionPlanResult) { + * if (isTransactionPlan(item)) { + * // item is narrowed to TransactionPlan + * console.log(item.kind); + * } + * } + * ``` + * + * @see {@link TransactionPlan} + * @see {@link isInstructionPlan} + * @see {@link isTransactionPlanResult} + */ +export function isTransactionPlan(value: unknown): value is TransactionPlan { + return ( + typeof value === 'object' && + value !== null && + 'planType' in value && + typeof value.planType === 'string' && + value.planType === 'transactionPlan' + ); +} + /** * Checks if the given transaction plan is a {@link SingleTransactionPlan}. * diff --git a/packages/instruction-plans/src/transaction-planner.ts b/packages/instruction-plans/src/transaction-planner.ts index c4269bfeb..725c01d66 100644 --- a/packages/instruction-plans/src/transaction-planner.ts +++ b/packages/instruction-plans/src/transaction-planner.ts @@ -203,6 +203,7 @@ async function traverseSequential( return { divisible: instructionPlan.divisible, kind: 'sequential', + planType: 'transactionPlan', plans: transactionPlans, }; } @@ -239,7 +240,7 @@ async function traverseParallel( if (transactionPlans.length === 0) { return null; } - return { kind: 'parallel', plans: transactionPlans }; + return { kind: 'parallel', planType: 'transactionPlan', plans: transactionPlans }; } async function traverseSingle( @@ -253,7 +254,7 @@ async function traverseSingle( return null; } const message = await createNewMessage(context, predicate); - return { kind: 'single', message }; + return { kind: 'single', message, planType: 'transactionPlan' }; } async function traverseMessagePacker( @@ -268,7 +269,7 @@ async function traverseMessagePacker( const candidate = await selectAndMutateCandidate(context, candidates, messagePacker.packMessageToCapacity); if (!candidate) { const message = await createNewMessage(context, messagePacker.packMessageToCapacity); - const newPlan: MutableSingleTransactionPlan = { kind: 'single', message }; + const newPlan: MutableSingleTransactionPlan = { kind: 'single', message, planType: 'transactionPlan' }; transactionPlans.push(newPlan); } } @@ -280,11 +281,12 @@ async function traverseMessagePacker( return null; } if (context.parent?.kind === 'parallel') { - return { kind: 'parallel', plans: transactionPlans }; + return { kind: 'parallel', planType: 'transactionPlan', plans: transactionPlans }; } return { divisible: context.parent?.kind === 'sequential' ? context.parent.divisible : true, kind: 'sequential', + planType: 'transactionPlan', plans: transactionPlans, }; }