diff --git a/.changeset/olive-gifts-say.md b/.changeset/olive-gifts-say.md new file mode 100644 index 000000000..293e9dab1 --- /dev/null +++ b/.changeset/olive-gifts-say.md @@ -0,0 +1,6 @@ +--- +'@solana/instruction-plans': patch +'@solana/errors': patch +--- + +Throw early when the default transaction plan executor encounters a non-divisible transaction plan. diff --git a/packages/errors/src/codes.ts b/packages/errors/src/codes.ts index 13646b66a..90bcadeb2 100644 --- a/packages/errors/src/codes.ts +++ b/packages/errors/src/codes.ts @@ -285,6 +285,7 @@ export const SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN = export const SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE = 7618001; export const SOLANA_ERROR__INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN = 7618002; export const SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN = 7618003; +export const SOLANA_ERROR__INSTRUCTION_PLANS__NON_DIVISIBLE_TRANSACTION_PLANS_NOT_SUPPORTED = 7618004; // Codec-related errors. // Reserve error codes in the range [8078000-8078999]. @@ -463,6 +464,7 @@ export type SolanaErrorCode = | typeof SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN | typeof SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN | typeof SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE + | typeof SOLANA_ERROR__INSTRUCTION_PLANS__NON_DIVISIBLE_TRANSACTION_PLANS_NOT_SUPPORTED | typeof SOLANA_ERROR__INVALID_BLOCKHASH_BYTE_LENGTH | typeof SOLANA_ERROR__INVALID_NONCE | typeof SOLANA_ERROR__INVARIANT_VIOLATION__CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING diff --git a/packages/errors/src/messages.ts b/packages/errors/src/messages.ts index 72ee07a07..d29ca52d9 100644 --- a/packages/errors/src/messages.ts +++ b/packages/errors/src/messages.ts @@ -113,6 +113,7 @@ import { SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN, SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN, SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE, + SOLANA_ERROR__INSTRUCTION_PLANS__NON_DIVISIBLE_TRANSACTION_PLANS_NOT_SUPPORTED, SOLANA_ERROR__INVALID_BLOCKHASH_BYTE_LENGTH, SOLANA_ERROR__INVALID_NONCE, SOLANA_ERROR__INVARIANT_VIOLATION__CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING, @@ -426,6 +427,8 @@ export const SolanaErrorMessages: Readonly<{ [SOLANA_ERROR__INSTRUCTION_ERROR__UNSUPPORTED_SYSVAR]: 'Unsupported sysvar', [SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_INSTRUCTION_PLAN_KIND]: 'Invalid instruction plan kind: $kind.', [SOLANA_ERROR__INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN]: 'The provided instruction plan is empty.', + [SOLANA_ERROR__INSTRUCTION_PLANS__NON_DIVISIBLE_TRANSACTION_PLANS_NOT_SUPPORTED]: + 'This transaction plan executor does not support non-divisible sequential plans. To support them, you may create your own executor such that multi-transaction atomicity is preserved — e.g. by targetting RPCs that support transaction bundles.', [SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN]: 'The provided transaction plan failed to execute. See the `transactionPlanResult` attribute and the `cause` error for more details.', [SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN]: diff --git a/packages/instruction-plans/src/__tests__/transaction-plan-executor-test.ts b/packages/instruction-plans/src/__tests__/transaction-plan-executor-test.ts index 8b4b1ea65..3d2bd2cbd 100644 --- a/packages/instruction-plans/src/__tests__/transaction-plan-executor-test.ts +++ b/packages/instruction-plans/src/__tests__/transaction-plan-executor-test.ts @@ -3,6 +3,7 @@ import '@solana/test-matchers/toBeFrozenObject'; import { SOLANA_ERROR__INSTRUCTION_ERROR__INVALID_ARGUMENT, SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN, + SOLANA_ERROR__INSTRUCTION_PLANS__NON_DIVISIBLE_TRANSACTION_PLANS_NOT_SUPPORTED, SolanaError, } from '@solana/errors'; @@ -16,7 +17,6 @@ import { createTransactionPlanExecutor } from '../transaction-plan-executor'; import { canceledSingleTransactionPlanResult, failedSingleTransactionPlanResult, - nonDivisibleSequentialTransactionPlanResult, parallelTransactionPlanResult, sequentialTransactionPlanResult, successfulSingleTransactionPlanResult, @@ -195,7 +195,7 @@ describe('createTransactionPlanExecutor', () => { expect(executeTransactionMessage).toHaveBeenNthCalledWith(2, messageB, { abortSignal: undefined }); }); - it('successfully executes a non-divisible sequential transaction plan', async () => { + it('throws when encountering a non-divisible sequential transaction plan', async () => { expect.assertions(1); const messageA = createMessage('A'); const messageB = createMessage('B'); @@ -203,14 +203,25 @@ describe('createTransactionPlanExecutor', () => { const executor = createTransactionPlanExecutor({ executeTransactionMessage }); const promise = executor(nonDivisibleSequentialTransactionPlan([messageA, messageB])); - await expect(promise).resolves.toStrictEqual( - nonDivisibleSequentialTransactionPlanResult([ - successfulSingleTransactionPlanResult(messageA, createTransaction('A')), - successfulSingleTransactionPlanResult(messageB, createTransaction('B')), - ]), + await expect(promise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__INSTRUCTION_PLANS__NON_DIVISIBLE_TRANSACTION_PLANS_NOT_SUPPORTED), ); }); + it('does no execute transactions before checking for non-divisible plans', async () => { + expect.assertions(1); + const messageA = createMessage('A'); + const messageB = createMessage('B'); + const messageC = createMessage('C'); + const executeTransactionMessage = jest.fn().mockImplementation(forwardId); + const executor = createTransactionPlanExecutor({ executeTransactionMessage }); + + await executor( + sequentialTransactionPlan([messageA, nonDivisibleSequentialTransactionPlan([messageB, messageC])]), + ).catch(() => {}); + expect(executeTransactionMessage).not.toHaveBeenCalled(); + }); + it('passes the abort signal to the `executeTransactionMessage` function', async () => { expect.assertions(2); const messageA = createMessage('A'); @@ -544,7 +555,7 @@ describe('createTransactionPlanExecutor', () => { parallelTransactionPlan([ sequentialTransactionPlan([messageA, parallelTransactionPlan([messageB, messageC]), messageD]), messageE, - nonDivisibleSequentialTransactionPlan([messageF, messageG]), + sequentialTransactionPlan([messageF, messageG]), ]), ); @@ -559,7 +570,7 @@ describe('createTransactionPlanExecutor', () => { successfulSingleTransactionPlanResult(messageD, createTransaction('D')), ]), successfulSingleTransactionPlanResult(messageE, createTransaction('E')), - nonDivisibleSequentialTransactionPlanResult([ + sequentialTransactionPlanResult([ successfulSingleTransactionPlanResult(messageF, createTransaction('F')), successfulSingleTransactionPlanResult(messageG, createTransaction('G')), ]), @@ -589,7 +600,7 @@ describe('createTransactionPlanExecutor', () => { parallelTransactionPlan([ sequentialTransactionPlan([messageA, parallelTransactionPlan([messageB, messageC]), messageD]), messageE, - nonDivisibleSequentialTransactionPlan([messageF, messageG]), + sequentialTransactionPlan([messageF, messageG]), ]), ); @@ -607,7 +618,7 @@ describe('createTransactionPlanExecutor', () => { canceledSingleTransactionPlanResult(messageD), ]), successfulSingleTransactionPlanResult(messageE, createTransaction('E')), - nonDivisibleSequentialTransactionPlanResult([ + sequentialTransactionPlanResult([ successfulSingleTransactionPlanResult(messageF, createTransaction('F')), successfulSingleTransactionPlanResult(messageG, createTransaction('G')), ]), @@ -640,7 +651,7 @@ describe('createTransactionPlanExecutor', () => { parallelTransactionPlan([ sequentialTransactionPlan([messageA, parallelTransactionPlan([messageB, messageC]), messageD]), messageE, - nonDivisibleSequentialTransactionPlan([messageF, messageG]), + sequentialTransactionPlan([messageF, messageG]), ]), { abortSignal }, ); @@ -662,7 +673,7 @@ describe('createTransactionPlanExecutor', () => { canceledSingleTransactionPlanResult(messageD), ]), successfulSingleTransactionPlanResult(messageE, createTransaction('E')), - nonDivisibleSequentialTransactionPlanResult([ + sequentialTransactionPlanResult([ successfulSingleTransactionPlanResult(messageF, createTransaction('F')), successfulSingleTransactionPlanResult(messageG, createTransaction('G')), ]), @@ -691,7 +702,7 @@ describe('createTransactionPlanExecutor', () => { parallelTransactionPlan([ sequentialTransactionPlan([messageA, parallelTransactionPlan([messageB, messageC]), messageD]), messageE, - nonDivisibleSequentialTransactionPlan([messageF, messageG]), + sequentialTransactionPlan([messageF, messageG]), ]), { abortSignal }, ); @@ -710,7 +721,7 @@ describe('createTransactionPlanExecutor', () => { canceledSingleTransactionPlanResult(messageD), ]), canceledSingleTransactionPlanResult(messageE), - nonDivisibleSequentialTransactionPlanResult([ + sequentialTransactionPlanResult([ canceledSingleTransactionPlanResult(messageF), canceledSingleTransactionPlanResult(messageG), ]), diff --git a/packages/instruction-plans/src/transaction-plan-executor.ts b/packages/instruction-plans/src/transaction-plan-executor.ts index 602fc32a2..ded076ef6 100644 --- a/packages/instruction-plans/src/transaction-plan-executor.ts +++ b/packages/instruction-plans/src/transaction-plan-executor.ts @@ -1,5 +1,6 @@ import { SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN, + SOLANA_ERROR__INSTRUCTION_PLANS__NON_DIVISIBLE_TRANSACTION_PLANS_NOT_SUPPORTED, SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_TRANSACTION_PLAN_KIND, SolanaError, } from '@solana/errors'; @@ -17,7 +18,6 @@ import type { import { canceledSingleTransactionPlanResult, failedSingleTransactionPlanResult, - nonDivisibleSequentialTransactionPlanResult, parallelTransactionPlanResult, sequentialTransactionPlanResult, successfulSingleTransactionPlanResult, @@ -86,6 +86,10 @@ export function createTransactionPlanExecutor(config: TransactionPlanExecutorCon canceled: abortSignal?.aborted ?? false, }; + // Fail early if there are non-divisible sequential plans in the + // transaction plan as they are not supported by this executor. + assertDivisibleSequentialPlansOnly(plan); + const cancelHandler = () => { context.canceled = true; }; @@ -136,6 +140,10 @@ async function traverseSequential( transactionPlan: SequentialTransactionPlan, context: TraverseContext, ): Promise { + if (!transactionPlan.divisible) { + throw new SolanaError(SOLANA_ERROR__INSTRUCTION_PLANS__NON_DIVISIBLE_TRANSACTION_PLANS_NOT_SUPPORTED); + } + const results: TransactionPlanResult[] = []; for (const subPlan of transactionPlan.plans) { @@ -143,9 +151,7 @@ async function traverseSequential( results.push(result); } - return transactionPlan.divisible - ? sequentialTransactionPlanResult(results) - : nonDivisibleSequentialTransactionPlanResult(results); + return sequentialTransactionPlanResult(results); } async function traverseParallel( @@ -195,3 +201,25 @@ function findErrorFromTransactionPlanResult(result: TransactionPlanResult): Erro } } } + +function assertDivisibleSequentialPlansOnly(transactionPlan: TransactionPlan): void { + const kind = transactionPlan.kind; + switch (kind) { + case 'sequential': + if (!transactionPlan.divisible) { + throw new SolanaError(SOLANA_ERROR__INSTRUCTION_PLANS__NON_DIVISIBLE_TRANSACTION_PLANS_NOT_SUPPORTED); + } + for (const subPlan of transactionPlan.plans) { + assertDivisibleSequentialPlansOnly(subPlan); + } + return; + case 'parallel': + for (const subPlan of transactionPlan.plans) { + assertDivisibleSequentialPlansOnly(subPlan); + } + return; + case 'single': + default: + return; + } +}