From 2d86e456685b84d71a4a1ccaf02b2e6014c0a443 Mon Sep 17 00:00:00 2001 From: Callum Date: Wed, 12 Aug 2026 12:26:09 +0000 Subject: [PATCH] Stop writing to the execution context in `createTransactionPlanExecutor` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `executeTransactionMessage` callback returned a `Signature` or a `Transaction`, and the executor wrote that return value into the context — overwriting anything the callback had already stored under those keys. It also derived `context.signature` by calling `getSignatureFromTransaction` on a returned transaction, and again on any transaction found on the context while handling a failure. That call throws when the fee payer slot is empty, so an executor that deliberately produces partially signed transactions could not succeed, and one that stored such a transaction before failing had its real error replaced by `SOLANA_ERROR__TRANSACTION__FEE_PAYER_SIGNATURE_MISSING` thrown from inside the catch block. The callback now returns `void` and the context is its only output. Neither derivation survives, which is what makes the partially signed case work: no code path asks a transaction for a signature any more, so an executor declares a `TContext` that does not require one, stores just the transaction, and gets results that honestly report no signature. The previous commit made the types admit that shape; this one makes the runtime match. `BaseTransactionPlanResultContext` goes with those derivations — it described fields the executor wrote on the caller's behalf, and nothing writes them now. The cost is that a signature is no longer supplied on the caller's behalf. An executor whose context requires one — including the default `TransactionPlanResultContextWithSignature` — must now assign it, and failed results carry only what was stored before the throw. `successfulSingleTransactionPlanResultFromTransaction`, the last helper that derived both on its own, goes too; construct results with `successfulSingleTransactionPlanResult` and pass the context explicitly. This supersedes #1906, which wrapped the failed-path derivation in a catch-all rather than removing it. That branch's regression test and its partially-signed-transaction fixture are carried over here. --- .changeset/cool-bushes-grow.md | 48 +++ .../transaction-plan-executor-test.ts | 332 ++++++++---------- .../__tests__/transaction-plan-result-test.ts | 247 +++++-------- .../transaction-plan-executor-typetest.ts | 104 ++++-- .../transaction-plan-result-typetest.ts | 95 ++--- .../src/transaction-plan-executor.ts | 163 ++------- .../src/transaction-plan-result.ts | 109 +----- 7 files changed, 416 insertions(+), 682 deletions(-) create mode 100644 .changeset/cool-bushes-grow.md diff --git a/.changeset/cool-bushes-grow.md b/.changeset/cool-bushes-grow.md new file mode 100644 index 000000000..480263008 --- /dev/null +++ b/.changeset/cool-bushes-grow.md @@ -0,0 +1,48 @@ +--- +'@solana/instruction-plans': major +--- + +Stop writing to the execution context in `createTransactionPlanExecutor` + +The `executeTransactionMessage` callback can no longer return a `Signature` or a `Transaction`. Those return values were deprecated when the callback gained the ability to return the context that a successful result should carry, and they are now gone: that context, a complete `TContext`, is the only thing the callback returns. Nothing is written to it on your behalf. + +```diff +const transactionPlanExecutor = createTransactionPlanExecutor({ + executeTransactionMessage: async (context, message) => { + const transaction = await signTransactionMessageWithSigners(message); + context.transaction = transaction; ++ const signature = getSignatureFromTransaction(transaction); + await sendAndConfirmTransaction(transaction, { commitment: 'confirmed' }); +- return transaction; ++ return { signature, transaction }; + }, +}); +``` + +The mutable `context` argument is still there, and still serves the failure path: whatever the callback stores on it before it throws is preserved in the resulting `FailedSingleTransactionPlanResult`. The two channels differ only in which outcome they feed. Mutating the context makes a value available to a failed result; returning it makes a value available to a successful one. On success the two are merged, with the returned value taking precedence, so a property stored but not returned is still reported. + +Note that the callback cannot simply return the context it was given — every property on it is optional, so it does not satisfy `TContext`. Build the return value from the values you have instead. This is the point of the return type: a callback that declares a context with a required `signature` and never produces one now fails to compile, rather than yielding a successful result whose `context.signature` is typed but `undefined` at runtime. + +**This unblocks executors that never obtain a fee payer signature.** Previously the executor derived `context.signature` by calling `getSignatureFromTransaction` on a returned transaction, and on any transaction found on the context while handling a failure. That call throws `SOLANA_ERROR__TRANSACTION__FEE_PAYER_SIGNATURE_MISSING` when the fee payer slot is empty, so an executor that deliberately produces partially signed transactions — signed by an authority, to be paid for and submitted by a relayer later — could not succeed, and one that stored such a transaction before failing had its original error replaced by that one. Neither derivation exists any more, so both cases now work. Declare a context type that does not require a signature and store just the transaction: + +```ts +const transactionPlanExecutor = createTransactionPlanExecutor<{ transaction: Transaction }>({ + executeTransactionMessage: async (_context, message) => { + return { transaction: await signTransactionMessageWithSigners(message) }; + }, +}); +``` + +**Signatures are no longer added behind your back.** An executor whose `TContext` requires a `signature` — including the default `TransactionPlanResultContextWithSignature` — must now produce one itself, and the compiler holds it to that. Failed results carry only what the callback stored on the context before it threw; a `signature` is no longer recovered from a stored transaction. + +**`BaseTransactionPlanResultContext` is removed.** It described the fields the executor used to write on your behalf, and nothing writes them any more — what a context holds is entirely `TContext`'s business. Use `TransactionPlanResultContextWithSignature` where you want the signature guarantee, or declare the optional `message` / `signature` / `transaction` fields your own context actually needs. + +**`successfulSingleTransactionPlanResultFromTransaction` is removed.** It was the last place that derived a `signature` on your behalf — by calling `getSignatureFromTransaction`, with the same fee-payer-signature requirement described above — and the executor no longer uses it. Construct results with `successfulSingleTransactionPlanResult` instead, passing the context explicitly: + +```diff +- successfulSingleTransactionPlanResultFromTransaction(message, transaction); ++ successfulSingleTransactionPlanResult(message, { ++ signature: getSignatureFromTransaction(transaction), ++ transaction, ++ }); +``` 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 f451ac4be..d4ff521a0 100644 --- a/packages/instruction-plans/src/__tests__/transaction-plan-executor-test.ts +++ b/packages/instruction-plans/src/__tests__/transaction-plan-executor-test.ts @@ -9,6 +9,7 @@ import { } from '@solana/errors'; import { Signature } from '@solana/keys'; import { TransactionMessage, TransactionMessageWithFeePayer } from '@solana/transaction-messages'; +import { Transaction } from '@solana/transactions'; import { canceledSingleTransactionPlanResult, @@ -22,16 +23,16 @@ import { sequentialTransactionPlanResult, singleTransactionPlan, successfulSingleTransactionPlanResult, - successfulSingleTransactionPlanResultFromTransaction, TransactionPlanResult, + TransactionPlanResultContext, TransactionPlanResultContextWithSignature, } from '../index'; import { createMessage, createPartiallySignedTransaction, createTransaction, FOREVER_PROMISE } from './__setup__'; jest.useFakeTimers(); -async function expectFailedToExecute( - promise: Promise, +async function expectFailedToExecute( + promise: Promise>, error: SolanaError, ): Promise { const transactionPlanResult = error.context.transactionPlanResult; @@ -50,10 +51,26 @@ async function expectFailedToExecute( ); } -function forwardId(_: unknown, message: TransactionMessage & TransactionMessageWithFeePayer) { - return Promise.resolve( - createTransaction((message as TransactionMessage & TransactionMessageWithFeePayer & { id: string }).id), - ); +function forwardId( + context: Partial, + message: TransactionMessage & TransactionMessageWithFeePayer, +): Promise { + const { id } = message as TransactionMessage & TransactionMessageWithFeePayer & { id: string }; + const transaction = createTransaction(id); + context.transaction = transaction; + return Promise.resolve({ signature: id as Signature, transaction }); +} + +/** Builds the successful result that executing `message` through the `forwardId` mock produces. */ +function successfulForwardIdResult( + message: TransactionMessage & TransactionMessageWithFeePayer & { id: string }, + context: TransactionPlanResultContext = {}, +) { + return successfulSingleTransactionPlanResult(message, { + ...context, + signature: message.id as Signature, + transaction: createTransaction(message.id), + }); } describe('createTransactionPlanExecutor', () => { @@ -61,14 +78,11 @@ describe('createTransactionPlanExecutor', () => { it('successfully executes a single transaction message', async () => { expect.assertions(2); const messageA = createMessage('A'); - const transactionA = createTransaction('A'); - const executeTransactionMessage = jest.fn().mockResolvedValue(transactionA); + const executeTransactionMessage = jest.fn().mockImplementation(forwardId); const executor = createTransactionPlanExecutor({ executeTransactionMessage }); const promise = executor(singleTransactionPlan(messageA)); - await expect(promise).resolves.toStrictEqual( - successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA), - ); + await expect(promise).resolves.toStrictEqual(successfulForwardIdResult(messageA)); expect(executeTransactionMessage).toHaveBeenNthCalledWith(1, expect.any(Object), messageA, { abortSignal: undefined, }); @@ -79,18 +93,18 @@ describe('createTransactionPlanExecutor', () => { const messageA = createMessage('A'); const abortController = new AbortController(); const abortSignal = abortController.signal; - const executeTransactionMessage = jest.fn().mockResolvedValue(createTransaction('A')); + const executeTransactionMessage = jest.fn().mockImplementation(forwardId); const executor = createTransactionPlanExecutor({ executeTransactionMessage }); await executor(singleTransactionPlan(messageA), { abortSignal }); expect(executeTransactionMessage).toHaveBeenNthCalledWith(1, expect.any(Object), messageA, { abortSignal }); }); - it('uses the returned signature for the successful context', async () => { + it('uses the signature returned by the callback for the successful result', async () => { expect.assertions(1); const messageA = createMessage('A'); const executor = createTransactionPlanExecutor({ - executeTransactionMessage: () => Promise.resolve('A' as Signature), + executeTransactionMessage: () => Promise.resolve({ signature: 'A' as Signature }), }); const promise = executor(singleTransactionPlan(messageA)); @@ -99,12 +113,15 @@ describe('createTransactionPlanExecutor', () => { ); }); - it('uses the signature from the returned transaction for the successful context', async () => { + it('keeps context properties that the callback stored but did not return', async () => { expect.assertions(1); const messageA = createMessage('A'); const transactionA = createTransaction('A'); const executor = createTransactionPlanExecutor({ - executeTransactionMessage: () => Promise.resolve(transactionA), + executeTransactionMessage: context => { + context.transaction = transactionA; + return Promise.resolve({ signature: 'A' as Signature }); + }, }); const promise = executor(singleTransactionPlan(messageA)); @@ -116,57 +133,33 @@ describe('createTransactionPlanExecutor', () => { ); }); - it('override any set signature with the returned signature', async () => { + it('prefers the returned context over the one stored on the context', async () => { expect.assertions(1); const messageA = createMessage('A'); const executor = createTransactionPlanExecutor({ executeTransactionMessage: context => { - context.signature = 'CONTEXT_SIGNATURE' as Signature; - return Promise.resolve('RETURNED_SIGNATURE' as Signature); + context.signature = 'stale' as Signature; + return Promise.resolve({ signature: 'A' as Signature }); }, }); const promise = executor(singleTransactionPlan(messageA)); await expect(promise).resolves.toStrictEqual( - successfulSingleTransactionPlanResult(messageA, { signature: 'RETURNED_SIGNATURE' as Signature }), + successfulSingleTransactionPlanResult(messageA, { signature: 'A' as Signature }), ); }); - it('override any set signature with the signature of the returned transaction', async () => { + it('does not derive a signature from a transaction stored on the context', async () => { expect.assertions(1); const messageA = createMessage('A'); const transactionA = createTransaction('A'); - const executor = createTransactionPlanExecutor({ - executeTransactionMessage: context => { - context.signature = 'CONTEXT_SIGNATURE' as Signature; - return Promise.resolve(transactionA); - }, + const executor = createTransactionPlanExecutor<{ transaction: Transaction }>({ + executeTransactionMessage: () => Promise.resolve({ transaction: transactionA }), }); const promise = executor(singleTransactionPlan(messageA)); await expect(promise).resolves.toStrictEqual( - successfulSingleTransactionPlanResult(messageA, { - signature: 'A' as Signature, - transaction: transactionA, - }), - ); - }); - - it('override any set transaction with the returned transaction', async () => { - expect.assertions(1); - const messageA = createMessage('A'); - const transactionA = createTransaction('A'); - const executor = createTransactionPlanExecutor({ - executeTransactionMessage: context => { - context.transaction = createTransaction('B'); - return Promise.resolve(transactionA); - }, - }); - - const promise = executor(singleTransactionPlan(messageA)); - await expect(promise).resolves.toStrictEqual( - successfulSingleTransactionPlanResult(messageA, { - signature: 'A' as Signature, + successfulSingleTransactionPlanResult<{ transaction: Transaction }>(messageA, { transaction: transactionA, }), ); @@ -191,99 +184,33 @@ describe('createTransactionPlanExecutor', () => { ); }); - it('keeps context properties that the callback stored but did not return', async () => { - expect.assertions(1); - const messageA = createMessage('A'); - const messageB = createMessage('B'); - const executor = createTransactionPlanExecutor({ - executeTransactionMessage: context => { - context.message = messageB; - return Promise.resolve({ signature: 'A' as Signature }); - }, - }); - - const promise = executor(singleTransactionPlan(messageA)); - await expect(promise).resolves.toStrictEqual( - successfulSingleTransactionPlanResult(messageA, { - message: messageB, - signature: 'A' as Signature, - }), - ); - }); - - it('prefers the returned context over the context stored by the callback', async () => { - expect.assertions(1); - const messageA = createMessage('A'); - const transactionA = createTransaction('A'); - const executor = createTransactionPlanExecutor({ - executeTransactionMessage: context => { - context.signature = 'STALE_SIGNATURE' as Signature; - context.transaction = createTransaction('B'); - return Promise.resolve({ - signature: 'RETURNED_SIGNATURE' as Signature, - transaction: transactionA, - }); - }, - }); - - const promise = executor(singleTransactionPlan(messageA)); - await expect(promise).resolves.toStrictEqual( - successfulSingleTransactionPlanResult(messageA, { - signature: 'RETURNED_SIGNATURE' as Signature, - transaction: transactionA, - }), - ); - }); - - it('succeeds when the transaction in the returned context has no fee payer signature', async () => { + it('succeeds when the stored transaction has no fee payer signature', async () => { expect.assertions(1); const messageA = createMessage('A'); const partiallySignedTransactionA = createPartiallySignedTransaction('A'); - const executor = createTransactionPlanExecutor({ - executeTransactionMessage: () => - Promise.resolve({ - signature: 'RELAYER_SIGNATURE' as Signature, - transaction: partiallySignedTransactionA, - }), + const executor = createTransactionPlanExecutor<{ transaction: Transaction }>({ + executeTransactionMessage: () => Promise.resolve({ transaction: partiallySignedTransactionA }), }); const promise = executor(singleTransactionPlan(messageA)); await expect(promise).resolves.toStrictEqual( - successfulSingleTransactionPlanResult(messageA, { - signature: 'RELAYER_SIGNATURE' as Signature, + successfulSingleTransactionPlanResult<{ transaction: Transaction }>(messageA, { transaction: partiallySignedTransactionA, }), ); }); - it('stores custom properties from the returned context', async () => { - expect.assertions(1); - const messageA = createMessage('A'); - const executor = createTransactionPlanExecutor<{ custom: string }>({ - executeTransactionMessage: () => - Promise.resolve({ custom: 'custom value', signature: 'A' as Signature }), - }); - - const promise = executor(singleTransactionPlan(messageA)); - await expect(promise).resolves.toStrictEqual( - successfulSingleTransactionPlanResult(messageA, { - custom: 'custom value', - signature: 'A' as Signature, - }), - ); - }); - it('stores the base context', async () => { expect.assertions(1); const messageA = createMessage('A'); const transactionA = createTransaction('A'); const executor = createTransactionPlanExecutor({ - executeTransactionMessage: (context, _) => { - context.message = createMessage('NEW A'); - context.transaction = transactionA; - context.signature = 'A' as Signature; - return Promise.resolve(transactionA); - }, + executeTransactionMessage: () => + Promise.resolve({ + message: createMessage('NEW A'), + signature: 'A' as Signature, + transaction: transactionA, + }), }); const promise = executor(singleTransactionPlan(messageA)); @@ -303,11 +230,12 @@ describe('createTransactionPlanExecutor', () => { const executor = createTransactionPlanExecutor< TransactionPlanResultContextWithSignature & { custom: string } >({ - executeTransactionMessage: context => { - context.custom = 'custom value'; - context.message = messageB; - return Promise.resolve('A' as Signature); - }, + executeTransactionMessage: () => + Promise.resolve({ + custom: 'custom value', + message: messageB, + signature: 'A' as Signature, + }), }); const promise = executor(singleTransactionPlan(messageA)); @@ -347,9 +275,11 @@ describe('createTransactionPlanExecutor', () => { const throwCause = (): void => { throw cause; }; - const executor = createTransactionPlanExecutor< - TransactionPlanResultContextWithSignature & { afterFailure: string; beforeFailure: string } - >({ + type Context = TransactionPlanResultContextWithSignature & { + afterFailure: string; + beforeFailure: string; + }; + const executor = createTransactionPlanExecutor({ executeTransactionMessage: async context => { context.beforeFailure = 'before failure'; context.message = messageB; @@ -357,7 +287,8 @@ describe('createTransactionPlanExecutor', () => { context.signature = 'B' as Signature; throwCause(); context.afterFailure = 'after failure'; - return await Promise.resolve('C' as Signature); + await Promise.resolve(); + return context as Context; // Never reached; the callback always throws. }, }); @@ -376,7 +307,7 @@ describe('createTransactionPlanExecutor', () => { ); }); - it('adds the signature to a failed context if a transaction is present', async () => { + it('does not add a signature to a failed context when a transaction is present', async () => { expect.assertions(2); const messageA = createMessage('A'); const transactionA = createTransaction('A'); @@ -388,7 +319,9 @@ describe('createTransactionPlanExecutor', () => { executeTransactionMessage: async context => { context.transaction = transactionA; throwCause(); - return await Promise.resolve(transactionA); + await Promise.resolve(); + // Never reached; the callback always throws. + return context as TransactionPlanResultContextWithSignature; }, }); @@ -398,13 +331,44 @@ describe('createTransactionPlanExecutor', () => { new SolanaError(SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN, { cause, transactionPlanResult: failedSingleTransactionPlanResult(messageA, cause, { - signature: 'A' as Signature, transaction: transactionA, }), }), ); }); + it('preserves the original error when the stored transaction has no fee payer signature', async () => { + expect.assertions(2); + const messageA = createMessage('A'); + const partiallySignedTransactionA = createPartiallySignedTransaction('A'); + const cause = new SolanaError(SOLANA_ERROR__INSTRUCTION_ERROR__INVALID_ARGUMENT, { index: 0 }); + const throwCause = (): void => { + throw cause; + }; + const executor = createTransactionPlanExecutor<{ transaction: Transaction }>({ + executeTransactionMessage: async context => { + context.transaction = partiallySignedTransactionA; + throwCause(); + await Promise.resolve(); + // Never reached; the callback always throws. + return context as { transaction: Transaction }; + }, + }); + + const promise = executor(singleTransactionPlan(messageA)); + await expectFailedToExecute( + promise, + new SolanaError(SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN, { + cause, + transactionPlanResult: failedSingleTransactionPlanResult<{ transaction: Transaction }>( + messageA, + cause, + { transaction: partiallySignedTransactionA }, + ), + }), + ); + }); + it('can use any error object as a failure cause', async () => { expect.assertions(2); const messageA = createMessage('A'); @@ -511,8 +475,8 @@ describe('createTransactionPlanExecutor', () => { const promise = executor(sequentialTransactionPlan([messageA, messageB])); await expect(promise).resolves.toStrictEqual( sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, createTransaction('A')), - successfulSingleTransactionPlanResultFromTransaction(messageB, createTransaction('B')), + successfulForwardIdResult(messageA), + successfulForwardIdResult(messageB), ]), ); @@ -570,23 +534,22 @@ describe('createTransactionPlanExecutor', () => { expect.assertions(1); const messageA = createMessage('A'); const messageB = createMessage('B'); - const executor = createTransactionPlanExecutor<{ custom: string }>({ - executeTransactionMessage: (context, message) => { + const executor = createTransactionPlanExecutor< + TransactionPlanResultContextWithSignature & { custom: string } + >({ + executeTransactionMessage: async (context, message) => { const id = (message as TransactionMessage & TransactionMessageWithFeePayer & { id: string }).id; - context.custom = 'Message ' + id; - return forwardId(context, message); + const custom = 'Message ' + id; + context.custom = custom; + return { ...(await forwardId(context, message)), custom }; }, }); const promise = executor(sequentialTransactionPlan([messageA, messageB])); await expect(promise).resolves.toStrictEqual( sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, createTransaction('A'), { - custom: 'Message A', - }), - successfulSingleTransactionPlanResultFromTransaction(messageB, createTransaction('B'), { - custom: 'Message B', - }), + successfulForwardIdResult(messageA, { custom: 'Message A' }), + successfulForwardIdResult(messageB, { custom: 'Message B' }), ]), ); }); @@ -605,7 +568,7 @@ describe('createTransactionPlanExecutor', () => { new SolanaError(SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN, { cause, transactionPlanResult: sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, createTransaction('A')), + successfulForwardIdResult(messageA), failedSingleTransactionPlanResult(messageB, cause), ]), }), @@ -672,7 +635,7 @@ describe('createTransactionPlanExecutor', () => { new SolanaError(SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN, { cause, transactionPlanResult: sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, createTransaction('A')), + successfulForwardIdResult(messageA), failedSingleTransactionPlanResult(messageB, cause), canceledSingleTransactionPlanResult(messageC), ]), @@ -733,8 +696,8 @@ describe('createTransactionPlanExecutor', () => { const promise = executor(parallelTransactionPlan([messageA, messageB])); await expect(promise).resolves.toStrictEqual( parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, createTransaction('A')), - successfulSingleTransactionPlanResultFromTransaction(messageB, createTransaction('B')), + successfulForwardIdResult(messageA), + successfulForwardIdResult(messageB), ]), ); @@ -765,23 +728,22 @@ describe('createTransactionPlanExecutor', () => { expect.assertions(1); const messageA = createMessage('A'); const messageB = createMessage('B'); - const executor = createTransactionPlanExecutor<{ custom: string }>({ - executeTransactionMessage: (context, message) => { + const executor = createTransactionPlanExecutor< + TransactionPlanResultContextWithSignature & { custom: string } + >({ + executeTransactionMessage: async (context, message) => { const id = (message as TransactionMessage & TransactionMessageWithFeePayer & { id: string }).id; - context.custom = 'Message ' + id; - return forwardId(context, message); + const custom = 'Message ' + id; + context.custom = custom; + return { ...(await forwardId(context, message)), custom }; }, }); const promise = executor(parallelTransactionPlan([messageA, messageB])); await expect(promise).resolves.toStrictEqual( parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, createTransaction('A'), { - custom: 'Message A', - }), - successfulSingleTransactionPlanResultFromTransaction(messageB, createTransaction('B'), { - custom: 'Message B', - }), + successfulForwardIdResult(messageA, { custom: 'Message A' }), + successfulForwardIdResult(messageB, { custom: 'Message B' }), ]), ); }); @@ -808,9 +770,9 @@ describe('createTransactionPlanExecutor', () => { new SolanaError(SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN, { cause, transactionPlanResult: parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, createTransaction('A')), + successfulForwardIdResult(messageA), failedSingleTransactionPlanResult(messageB, cause), - successfulSingleTransactionPlanResultFromTransaction(messageC, createTransaction('C')), + successfulForwardIdResult(messageC), ]), }), ); @@ -845,9 +807,9 @@ describe('createTransactionPlanExecutor', () => { new SolanaError(SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN, { cause, transactionPlanResult: parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, createTransaction('A')), + successfulForwardIdResult(messageA), failedSingleTransactionPlanResult(messageB, cause), - successfulSingleTransactionPlanResultFromTransaction(messageC, createTransaction('C')), + successfulForwardIdResult(messageC), ]), }), ); @@ -914,17 +876,17 @@ describe('createTransactionPlanExecutor', () => { await expect(promise).resolves.toStrictEqual( parallelTransactionPlanResult([ sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, createTransaction('A')), + successfulForwardIdResult(messageA), parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageB, createTransaction('B')), - successfulSingleTransactionPlanResultFromTransaction(messageC, createTransaction('C')), + successfulForwardIdResult(messageB), + successfulForwardIdResult(messageC), ]), - successfulSingleTransactionPlanResultFromTransaction(messageD, createTransaction('D')), + successfulForwardIdResult(messageD), ]), - successfulSingleTransactionPlanResultFromTransaction(messageE, createTransaction('E')), + successfulForwardIdResult(messageE), sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageF, createTransaction('F')), - successfulSingleTransactionPlanResultFromTransaction(messageG, createTransaction('G')), + successfulForwardIdResult(messageF), + successfulForwardIdResult(messageG), ]), ]), ); @@ -966,17 +928,17 @@ describe('createTransactionPlanExecutor', () => { cause, transactionPlanResult: parallelTransactionPlanResult([ sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, createTransaction('A')), + successfulForwardIdResult(messageA), parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageB, createTransaction('B')), + successfulForwardIdResult(messageB), failedSingleTransactionPlanResult(messageC, cause), ]), canceledSingleTransactionPlanResult(messageD), ]), - successfulSingleTransactionPlanResultFromTransaction(messageE, createTransaction('E')), + successfulForwardIdResult(messageE), sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageF, createTransaction('F')), - successfulSingleTransactionPlanResultFromTransaction(messageG, createTransaction('G')), + successfulForwardIdResult(messageF), + successfulForwardIdResult(messageG), ]), ]), }), @@ -1025,17 +987,17 @@ describe('createTransactionPlanExecutor', () => { cause, transactionPlanResult: parallelTransactionPlanResult([ sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, createTransaction('A')), + successfulForwardIdResult(messageA), parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageB, createTransaction('B')), + successfulForwardIdResult(messageB), failedSingleTransactionPlanResult(messageC, cause), ]), canceledSingleTransactionPlanResult(messageD), ]), - successfulSingleTransactionPlanResultFromTransaction(messageE, createTransaction('E')), + successfulForwardIdResult(messageE), sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageF, createTransaction('F')), - successfulSingleTransactionPlanResultFromTransaction(messageG, createTransaction('G')), + successfulForwardIdResult(messageF), + successfulForwardIdResult(messageG), ]), ]), }), @@ -1095,7 +1057,7 @@ describe('createTransactionPlanExecutor', () => { describe('passthroughFailedTransactionPlanExecution', () => { it('returns the resolved result as-is', async () => { expect.assertions(1); - const result = successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')); + const result = successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }); const promise = Promise.resolve(result); await expect(passthroughFailedTransactionPlanExecution(promise)).resolves.toBe(result); }); 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 431b13f2a..90f2142f9 100644 --- a/packages/instruction-plans/src/__tests__/transaction-plan-result-test.ts +++ b/packages/instruction-plans/src/__tests__/transaction-plan-result-test.ts @@ -36,51 +36,10 @@ import { parallelTransactionPlanResult, sequentialTransactionPlanResult, successfulSingleTransactionPlanResult, - successfulSingleTransactionPlanResultFromTransaction, summarizeTransactionPlanResult, transformTransactionPlanResult, } from '../index'; -import { createMessage, createTransaction } from './__setup__'; - -describe('successfulSingleTransactionPlanResultFromTransaction', () => { - it('creates SingleTransactionPlanResult objects with successful status', () => { - const messageA = createMessage('A'); - const transactionA = createTransaction('A'); - const result = successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA); - expect(result).toEqual({ - context: { signature: 'A', transaction: transactionA }, - kind: 'single', - planType: 'transactionPlanResult', - plannedMessage: messageA, - status: 'successful', - }); - }); - it('accepts an optional context object', () => { - const messageA = createMessage('A'); - const transactionA = createTransaction('A'); - const context = { foo: 'bar' }; - const result = successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA, context); - expect(result).toEqual({ - context: { ...context, signature: 'A', transaction: transactionA }, - kind: 'single', - planType: 'transactionPlanResult', - plannedMessage: messageA, - status: 'successful', - }); - }); - it('freezes created SingleTransactionPlanResult objects', () => { - const messageA = createMessage('A'); - const transactionA = createTransaction('A'); - const result = successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA); - expect(result).toBeFrozenObject(); - }); - it('freezes the status object of created SingleTransactionPlanResult objects', () => { - const messageA = createMessage('A'); - const transactionA = createTransaction('A'); - const result = successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA); - expect(result.status).toBeFrozenObject(); - }); -}); +import { createMessage } from './__setup__'; describe('successfulSingleTransactionPlanResult', () => { it('creates SingleTransactionPlanResult objects with successful status', () => { @@ -279,11 +238,6 @@ describe('nonDivisibleSequentialTransactionPlanResult', () => { describe('isSingleTransactionPlanResult', () => { it('returns true for any SingleTransactionPlanResult', () => { - expect( - isSingleTransactionPlanResult( - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), - ), - ).toBe(true); expect( isSingleTransactionPlanResult( successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), @@ -308,11 +262,6 @@ describe('isSingleTransactionPlanResult', () => { describe('assertIsSingleTransactionPlanResult', () => { it('does nothing for any SingleTransactionPlanResult', () => { - expect(() => - assertIsSingleTransactionPlanResult( - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), - ), - ).not.toThrow(); expect(() => assertIsSingleTransactionPlanResult( successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), @@ -345,11 +294,6 @@ describe('assertIsSingleTransactionPlanResult', () => { describe('isSuccessfulSingleTransactionPlanResult', () => { it('returns true for successful SingleTransactionPlanResult', () => { - expect( - isSuccessfulSingleTransactionPlanResult( - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), - ), - ).toBe(true); expect( isSuccessfulSingleTransactionPlanResult( successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), @@ -376,11 +320,6 @@ describe('isSuccessfulSingleTransactionPlanResult', () => { describe('assertIsSuccessfulSingleTransactionPlanResult', () => { it('does nothing for successful SingleTransactionPlanResult', () => { - expect(() => - assertIsSuccessfulSingleTransactionPlanResult( - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), - ), - ).not.toThrow(); expect(() => assertIsSuccessfulSingleTransactionPlanResult( successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), @@ -425,7 +364,7 @@ describe('isFailedSingleTransactionPlanResult', () => { it('returns false for other plans', () => { expect( isFailedSingleTransactionPlanResult( - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), ), ).toBe(false); expect(isFailedSingleTransactionPlanResult(canceledSingleTransactionPlanResult(createMessage('A')))).toBe( @@ -451,7 +390,7 @@ describe('assertIsFailedSingleTransactionPlanResult', () => { it('throws for other plans', () => { expect(() => assertIsFailedSingleTransactionPlanResult( - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), ), ).toThrow('Unexpected transaction plan result. Expected failed single plan, got successful single plan.'); expect(() => @@ -478,7 +417,7 @@ describe('isCanceledSingleTransactionPlanResult', () => { it('returns false for other plans', () => { expect( isCanceledSingleTransactionPlanResult( - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), ), ).toBe(false); expect( @@ -504,7 +443,7 @@ describe('assertIsCanceledSingleTransactionPlanResult', () => { it('throws for other plans', () => { expect(() => assertIsCanceledSingleTransactionPlanResult( - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), ), ).toThrow('Unexpected transaction plan result. Expected canceled single plan, got successful single plan.'); expect(() => @@ -535,7 +474,7 @@ describe('isSequentialTransactionPlanResult', () => { it('returns false for other plans', () => { expect( isSequentialTransactionPlanResult( - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), ), ).toBe(false); expect( @@ -561,7 +500,7 @@ describe('assertIsSequentialTransactionPlanResult', () => { it('throws for other plans', () => { expect(() => assertIsSequentialTransactionPlanResult( - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), ), ).toThrow('Unexpected transaction plan result. Expected sequential plan, got single plan.'); expect(() => @@ -590,7 +529,7 @@ describe('isNonDivisibleSequentialTransactionPlanResult', () => { it('returns false for other plans', () => { expect( isNonDivisibleSequentialTransactionPlanResult( - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), ), ).toBe(false); expect( @@ -618,7 +557,7 @@ describe('assertIsNonDivisibleSequentialTransactionPlanResult', () => { it('throws for other plans', () => { expect(() => assertIsNonDivisibleSequentialTransactionPlanResult( - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), ), ).toThrow('Unexpected transaction plan result. Expected non-divisible sequential plan, got single plan.'); expect(() => @@ -650,7 +589,7 @@ describe('isParallelTransactionPlanResult', () => { it('returns false for other plans', () => { expect( isParallelTransactionPlanResult( - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), ), ).toBe(false); expect( @@ -674,7 +613,7 @@ describe('assertIsParallelTransactionPlanResult', () => { it('throws for other plans', () => { expect(() => assertIsParallelTransactionPlanResult( - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), ), ).toThrow('Unexpected transaction plan result. Expected parallel plan, got single plan.'); expect(() => @@ -701,16 +640,16 @@ describe('isSuccessfulTransactionPlanResult', () => { it('returns true for a single successful result', () => { expect( isSuccessfulTransactionPlanResult( - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), ), ).toBe(true); }); it('returns true for nested results that are all successful', () => { const result = parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('B'), createTransaction('B')), - successfulSingleTransactionPlanResultFromTransaction(createMessage('C'), createTransaction('C')), + successfulSingleTransactionPlanResult(createMessage('B'), { signature: 'B' as Signature }), + successfulSingleTransactionPlanResult(createMessage('C'), { signature: 'C' as Signature }), ]), ]); expect(isSuccessfulTransactionPlanResult(result)).toBe(true); @@ -723,7 +662,7 @@ describe('isSuccessfulTransactionPlanResult', () => { }); it('returns false when any single result is failed', () => { const result = parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), failedSingleTransactionPlanResult( createMessage('B'), new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE), @@ -733,7 +672,7 @@ describe('isSuccessfulTransactionPlanResult', () => { }); it('returns false when any single result is canceled', () => { const result = sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), canceledSingleTransactionPlanResult(createMessage('B')), ]); expect(isSuccessfulTransactionPlanResult(result)).toBe(false); @@ -755,7 +694,7 @@ describe('isSuccessfulTransactionPlanResult', () => { const result = parallelTransactionPlanResult([ sequentialTransactionPlanResult([ parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), failedSingleTransactionPlanResult( createMessage('B'), new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE), @@ -771,16 +710,16 @@ describe('assertIsSuccessfulTransactionPlanResult', () => { it('does nothing for a single successful result', () => { expect(() => assertIsSuccessfulTransactionPlanResult( - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), ), ).not.toThrow(); }); it('does nothing for nested results that are all successful', () => { const result = parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('B'), createTransaction('B')), - successfulSingleTransactionPlanResultFromTransaction(createMessage('C'), createTransaction('C')), + successfulSingleTransactionPlanResult(createMessage('B'), { signature: 'B' as Signature }), + successfulSingleTransactionPlanResult(createMessage('C'), { signature: 'C' as Signature }), ]), ]); expect(() => assertIsSuccessfulTransactionPlanResult(result)).not.toThrow(); @@ -793,7 +732,7 @@ describe('assertIsSuccessfulTransactionPlanResult', () => { }); it('throws when any single result is failed', () => { const result = parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), failedSingleTransactionPlanResult( createMessage('B'), new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE), @@ -807,7 +746,7 @@ describe('assertIsSuccessfulTransactionPlanResult', () => { }); it('throws when any single result is canceled', () => { const result = sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), canceledSingleTransactionPlanResult(createMessage('B')), ]); expect(() => assertIsSuccessfulTransactionPlanResult(result)).toThrow( @@ -937,7 +876,7 @@ describe('findTransactionPlanResult', () => { const error = new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE); const failedResult = failedSingleTransactionPlanResult(messageB, error); const result = parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, createTransaction('A')), + successfulSingleTransactionPlanResult(messageA, { signature: 'A' as Signature }), failedResult, ]); const found = findTransactionPlanResult( @@ -950,7 +889,7 @@ describe('findTransactionPlanResult', () => { it('finds a successful single transaction result', () => { const messageA = createMessage('A'); const messageB = createMessage('B'); - const successfulResult = successfulSingleTransactionPlanResultFromTransaction(messageA, createTransaction('A')); + const successfulResult = successfulSingleTransactionPlanResult(messageA, { signature: 'A' as Signature }); const result = sequentialTransactionPlanResult([ successfulResult, canceledSingleTransactionPlanResult(messageB), @@ -982,7 +921,7 @@ describe('everyTransactionPlanResult', () => { expect(result).toBe(false); }); it('matches successful single transaction plans', () => { - const plan = successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')); + const plan = successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }); // eslint-disable-next-line jest/no-conditional-in-test const result = everyTransactionPlanResult(plan, p => p.kind === 'single' && p.status === 'successful'); expect(result).toBe(true); @@ -1019,10 +958,7 @@ describe('everyTransactionPlanResult', () => { expect(result).toBe(true); }); it('matches complex transaction plans', () => { - const resultA = successfulSingleTransactionPlanResultFromTransaction( - createMessage('A'), - createTransaction('A'), - ); + const resultA = successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }); const resultB = failedSingleTransactionPlanResult( createMessage('B'), new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE), @@ -1041,10 +977,7 @@ describe('everyTransactionPlanResult', () => { expect(result).toBe(true); }); it('returns false on complex transaction plans', () => { - const resultA = successfulSingleTransactionPlanResultFromTransaction( - createMessage('A'), - createTransaction('A'), - ); + const resultA = successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }); const resultB = failedSingleTransactionPlanResult( createMessage('B'), new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE), @@ -1060,14 +993,8 @@ describe('everyTransactionPlanResult', () => { }); it('fails fast before evaluating children', () => { const predicate = jest.fn().mockReturnValueOnce(false); - const messageA = successfulSingleTransactionPlanResultFromTransaction( - createMessage('A'), - createTransaction('A'), - ); - const messageB = successfulSingleTransactionPlanResultFromTransaction( - createMessage('B'), - createTransaction('B'), - ); + const messageA = successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }); + const messageB = successfulSingleTransactionPlanResult(createMessage('B'), { signature: 'B' as Signature }); const plan = sequentialTransactionPlanResult([messageA, messageB]); const result = everyTransactionPlanResult(plan, predicate); expect(result).toBe(false); @@ -1078,14 +1005,8 @@ describe('everyTransactionPlanResult', () => { }); it('fails fast before evaluating siblings', () => { const predicate = jest.fn().mockReturnValueOnce(true).mockReturnValueOnce(false); - const messageA = successfulSingleTransactionPlanResultFromTransaction( - createMessage('A'), - createTransaction('A'), - ); - const messageB = successfulSingleTransactionPlanResultFromTransaction( - createMessage('B'), - createTransaction('B'), - ); + const messageA = successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }); + const messageB = successfulSingleTransactionPlanResult(createMessage('B'), { signature: 'B' as Signature }); const plan = sequentialTransactionPlanResult([messageA, messageB]); const result = everyTransactionPlanResult(plan, predicate); expect(result).toBe(false); @@ -1098,13 +1019,13 @@ describe('everyTransactionPlanResult', () => { describe('transformTransactionPlanResult', () => { it('transforms successful single transaction plan results', () => { - const plan = successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')); + const plan = successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }); const transformedPlan = transformTransactionPlanResult(plan, p => // eslint-disable-next-line jest/no-conditional-in-test p.kind === 'single' ? { ...p, plannedMessage: { ...p.plannedMessage, id: 'New A' } } : p, ); expect(transformedPlan).toStrictEqual( - successfulSingleTransactionPlanResultFromTransaction(createMessage('New A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('New A'), { signature: 'A' as Signature }), ); }); it('transforms failed single transaction plan results', () => { @@ -1126,8 +1047,8 @@ describe('transformTransactionPlanResult', () => { }); it('transforms sequential transaction plan results', () => { const plan = sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), - successfulSingleTransactionPlanResultFromTransaction(createMessage('B'), createTransaction('B')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), + successfulSingleTransactionPlanResult(createMessage('B'), { signature: 'B' as Signature }), ]); const transformedPlan = transformTransactionPlanResult(plan, p => // eslint-disable-next-line jest/no-conditional-in-test @@ -1135,15 +1056,15 @@ describe('transformTransactionPlanResult', () => { ); expect(transformedPlan).toStrictEqual( sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('B'), createTransaction('B')), - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('B'), { signature: 'B' as Signature }), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), ]), ); }); it('transforms non-divisible sequential transaction plan results', () => { const plan = nonDivisibleSequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), - successfulSingleTransactionPlanResultFromTransaction(createMessage('B'), createTransaction('B')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), + successfulSingleTransactionPlanResult(createMessage('B'), { signature: 'B' as Signature }), ]); const transformedPlan = transformTransactionPlanResult(plan, p => // eslint-disable-next-line jest/no-conditional-in-test @@ -1151,15 +1072,15 @@ describe('transformTransactionPlanResult', () => { ); expect(transformedPlan).toStrictEqual( nonDivisibleSequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('B'), createTransaction('B')), - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('B'), { signature: 'B' as Signature }), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), ]), ); }); it('transforms parallel transaction plan results', () => { const plan = parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), - successfulSingleTransactionPlanResultFromTransaction(createMessage('B'), createTransaction('B')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), + successfulSingleTransactionPlanResult(createMessage('B'), { signature: 'B' as Signature }), ]); const transformedPlan = transformTransactionPlanResult(plan, p => // eslint-disable-next-line jest/no-conditional-in-test @@ -1167,18 +1088,18 @@ describe('transformTransactionPlanResult', () => { ); expect(transformedPlan).toStrictEqual( parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('B'), createTransaction('B')), - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('B'), { signature: 'B' as Signature }), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), ]), ); }); it('transforms using a bottom-up approach', () => { // Given the following nested plans. const plan = sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('B'), createTransaction('B')), - successfulSingleTransactionPlanResultFromTransaction(createMessage('C'), createTransaction('C')), + successfulSingleTransactionPlanResult(createMessage('B'), { signature: 'B' as Signature }), + successfulSingleTransactionPlanResult(createMessage('C'), { signature: 'C' as Signature }), ]), ]); @@ -1209,8 +1130,8 @@ describe('transformTransactionPlanResult', () => { }); it('can be used to duplicate transaction results', () => { const plan = sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), - successfulSingleTransactionPlanResultFromTransaction(createMessage('B'), createTransaction('B')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), + successfulSingleTransactionPlanResult(createMessage('B'), { signature: 'B' as Signature }), ]); const transformedPlan = transformTransactionPlanResult(plan, p => // eslint-disable-next-line jest/no-conditional-in-test @@ -1219,20 +1140,20 @@ describe('transformTransactionPlanResult', () => { expect(transformedPlan).toStrictEqual( sequentialTransactionPlanResult([ sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), ]), sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('B'), createTransaction('B')), - successfulSingleTransactionPlanResultFromTransaction(createMessage('B'), createTransaction('B')), + successfulSingleTransactionPlanResult(createMessage('B'), { signature: 'B' as Signature }), + successfulSingleTransactionPlanResult(createMessage('B'), { signature: 'B' as Signature }), ]), ]), ); }); it('can be used to remove parallelism', () => { const plan = parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), - successfulSingleTransactionPlanResultFromTransaction(createMessage('B'), createTransaction('B')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), + successfulSingleTransactionPlanResult(createMessage('B'), { signature: 'B' as Signature }), ]); const transformedPlan = transformTransactionPlanResult(plan, p => // eslint-disable-next-line jest/no-conditional-in-test @@ -1240,22 +1161,22 @@ describe('transformTransactionPlanResult', () => { ); expect(transformedPlan).toStrictEqual( sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), - successfulSingleTransactionPlanResultFromTransaction(createMessage('B'), createTransaction('B')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), + successfulSingleTransactionPlanResult(createMessage('B'), { signature: 'B' as Signature }), ]), ); }); it('can be used to flatten nested transaction plan results', () => { const plan = sequentialTransactionPlanResult([ sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), - successfulSingleTransactionPlanResultFromTransaction(createMessage('B'), createTransaction('B')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), + successfulSingleTransactionPlanResult(createMessage('B'), { signature: 'B' as Signature }), ]), sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('C'), createTransaction('C')), + successfulSingleTransactionPlanResult(createMessage('C'), { signature: 'C' as Signature }), sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('D'), createTransaction('D')), - successfulSingleTransactionPlanResultFromTransaction(createMessage('E'), createTransaction('E')), + successfulSingleTransactionPlanResult(createMessage('D'), { signature: 'D' as Signature }), + successfulSingleTransactionPlanResult(createMessage('E'), { signature: 'E' as Signature }), ]), ]), ]); @@ -1270,16 +1191,16 @@ describe('transformTransactionPlanResult', () => { }); expect(transformedPlan).toStrictEqual( sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), - successfulSingleTransactionPlanResultFromTransaction(createMessage('B'), createTransaction('B')), - successfulSingleTransactionPlanResultFromTransaction(createMessage('C'), createTransaction('C')), - successfulSingleTransactionPlanResultFromTransaction(createMessage('D'), createTransaction('D')), - successfulSingleTransactionPlanResultFromTransaction(createMessage('E'), createTransaction('E')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), + successfulSingleTransactionPlanResult(createMessage('B'), { signature: 'B' as Signature }), + successfulSingleTransactionPlanResult(createMessage('C'), { signature: 'C' as Signature }), + successfulSingleTransactionPlanResult(createMessage('D'), { signature: 'D' as Signature }), + successfulSingleTransactionPlanResult(createMessage('E'), { signature: 'E' as Signature }), ]), ); }); it('keeps transformed successful single transaction plan results frozen', () => { - const plan = successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')); + const plan = successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }); const transformedPlan = transformTransactionPlanResult(plan, p => ({ ...p })); expect(transformedPlan).toBeFrozenObject(); }); @@ -1298,14 +1219,14 @@ describe('transformTransactionPlanResult', () => { }); it('keeps transformed sequential transaction plan results frozen', () => { const plan = sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), ]); const transformedPlan = transformTransactionPlanResult(plan, p => ({ ...p })); expect(transformedPlan).toBeFrozenObject(); }); it('keeps transformed parallel transaction plan results frozen', () => { const plan = parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')), + successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }), ]); const transformedPlan = transformTransactionPlanResult(plan, p => ({ ...p })); expect(transformedPlan).toBeFrozenObject(); @@ -1313,9 +1234,9 @@ describe('transformTransactionPlanResult', () => { }); describe('flattenTransactionPlanResult', () => { - const plan1 = successfulSingleTransactionPlanResultFromTransaction(createMessage('A'), createTransaction('A')); - const plan2 = successfulSingleTransactionPlanResultFromTransaction(createMessage('B'), createTransaction('B')); - const plan3 = successfulSingleTransactionPlanResultFromTransaction(createMessage('C'), createTransaction('C')); + const plan1 = successfulSingleTransactionPlanResult(createMessage('A'), { signature: 'A' as Signature }); + const plan2 = successfulSingleTransactionPlanResult(createMessage('B'), { signature: 'B' as Signature }); + const plan3 = successfulSingleTransactionPlanResult(createMessage('C'), { signature: 'C' as Signature }); it('flattens a parallel transaction plan result', () => { const result = parallelTransactionPlanResult([plan1, plan2, plan3]); @@ -1479,7 +1400,7 @@ describe('getFirstFailedSingleTransactionPlanResult', () => { const error = new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE); const failedResult = failedSingleTransactionPlanResult(messageB, error); const parallelResult = parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, createTransaction('A')), + successfulSingleTransactionPlanResult(messageA, { signature: 'A' as Signature }), failedResult, ]); @@ -1493,7 +1414,7 @@ describe('getFirstFailedSingleTransactionPlanResult', () => { const error = new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE); const failedResult = failedSingleTransactionPlanResult(messageB, error); const sequentialResult = sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, createTransaction('A')), + successfulSingleTransactionPlanResult(messageA, { signature: 'A' as Signature }), failedResult, ]); @@ -1509,9 +1430,9 @@ describe('getFirstFailedSingleTransactionPlanResult', () => { const failedResult = failedSingleTransactionPlanResult(messageC, error); const nestedResult = parallelTransactionPlanResult([ sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, createTransaction('A')), + successfulSingleTransactionPlanResult(messageA, { signature: 'A' as Signature }), parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageB, createTransaction('B')), + successfulSingleTransactionPlanResult(messageB, { signature: 'B' as Signature }), failedResult, ]), ]), @@ -1537,8 +1458,8 @@ describe('getFirstFailedSingleTransactionPlanResult', () => { const messageA = createMessage('A'); const messageB = createMessage('B'); const successfulResult = parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, createTransaction('A')), - successfulSingleTransactionPlanResultFromTransaction(messageB, createTransaction('B')), + successfulSingleTransactionPlanResult(messageA, { signature: 'A' as Signature }), + successfulSingleTransactionPlanResult(messageB, { signature: 'B' as Signature }), ]); expect(() => getFirstFailedSingleTransactionPlanResult(successfulResult)).toThrow( @@ -1569,7 +1490,7 @@ describe('getFirstFailedSingleTransactionPlanResult', () => { const messageA = createMessage('A'); const messageB = createMessage('B'); const mixedResult = sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, createTransaction('A')), + successfulSingleTransactionPlanResult(messageA, { signature: 'A' as Signature }), canceledSingleTransactionPlanResult(messageB), ]); @@ -1584,7 +1505,7 @@ describe('getFirstFailedSingleTransactionPlanResult', () => { it('throws an error where context contains transactionPlanResult as non-enumerable', () => { const messageA = createMessage('A'); - const successfulResult = successfulSingleTransactionPlanResultFromTransaction(messageA, createTransaction('A')); + const successfulResult = successfulSingleTransactionPlanResult(messageA, { signature: 'A' as Signature }); let caughtError: | SolanaError diff --git a/packages/instruction-plans/src/__typetests__/transaction-plan-executor-typetest.ts b/packages/instruction-plans/src/__typetests__/transaction-plan-executor-typetest.ts index f2033bc48..e1e558e74 100644 --- a/packages/instruction-plans/src/__typetests__/transaction-plan-executor-typetest.ts +++ b/packages/instruction-plans/src/__typetests__/transaction-plan-executor-typetest.ts @@ -46,41 +46,66 @@ import { // [DESCRIBE] createTransactionPlanExecutor { - // It can still return a signature or a full transaction, using the deprecated overload. + // Its callback returns the context that a successful result should carry. { createTransactionPlanExecutor({ - executeTransactionMessage: () => Promise.resolve({} as Signature), + executeTransactionMessage: () => Promise.resolve({ signature: {} as Signature }), }); createTransactionPlanExecutor({ - executeTransactionMessage: () => Promise.resolve({} as Transaction), + // @ts-expect-error The context is no longer the callback's only output; it must be returned. + executeTransactionMessage: () => Promise.resolve(), + }); + createTransactionPlanExecutor({ + // @ts-expect-error A bare signature is not a context; it belongs under a `signature` key. + executeTransactionMessage: () => Promise.resolve({} as Signature), }); } - // It can return the context that a successful result should carry. + // Its callback must produce every property its context requires. This is what stops a + // successful result from promising a property that was never populated. + { + createTransactionPlanExecutor({ + // @ts-expect-error This context requires a signature and the callback returns none. + executeTransactionMessage: () => Promise.resolve({ transaction: {} as Transaction }), + }); + createTransactionPlanExecutor<{ custom: string; other: string }>({ + // @ts-expect-error This context requires an `other` property and the callback returns none. + executeTransactionMessage: () => Promise.resolve({ custom: 'value' }), + }); + createTransactionPlanExecutor({ + // @ts-expect-error Mutating the context does not discharge the obligation to return it. + executeTransactionMessage: context => { + context.custom = 'value'; + return Promise.resolve({ signature: {} as Signature }); + }, + }); + } + + // Its callback cannot return the context it was given, since every property on it is optional. { createTransactionPlanExecutor({ - executeTransactionMessage: () => Promise.resolve({ signature: {} as Signature }), + // @ts-expect-error The mutable context does not satisfy `TContext` on its own. + executeTransactionMessage: context => Promise.resolve(context), }); createTransactionPlanExecutor({ - executeTransactionMessage: () => - Promise.resolve({ signature: {} as Signature, transaction: {} as Transaction }), + // @ts-expect-error Spreading it does not help; the spread is optional throughout. + executeTransactionMessage: context => Promise.resolve({ ...context }), }); } - // A returned context needs no signature of its own; since `TContext` alone decides what a - // context carries, one inferred from the return value drops the signature guarantee. + // When the callback declares no parameters, `TContext` is inferred from the context it + // returns rather than falling back to the default. Declaring a parameter — which any callback + // that needs the message must do — makes the callback context-sensitive, at which point the + // default applies and the returned context must satisfy it. { const executor = createTransactionPlanExecutor({ - executeTransactionMessage: () => Promise.resolve({ sent: true }), + executeTransactionMessage: () => Promise.resolve({ transaction: {} as Transaction }), }); - executor satisfies TransactionPlanExecutor<{ sent: boolean }>; - } + executor satisfies TransactionPlanExecutor<{ transaction: Transaction }>; - // It requires a returned context to carry the signature when `TContext` guarantees one. - { - createTransactionPlanExecutor({ - // @ts-expect-error The returned context is missing the guaranteed `signature` property. - executeTransactionMessage: () => Promise.resolve({ sent: true }), + createTransactionPlanExecutor({ + // @ts-expect-error The default context applies here, and it requires a signature. + executeTransactionMessage: (_, _message) => Promise.resolve({ transaction: {} as Transaction }), }); } @@ -89,7 +114,7 @@ import { createTransactionPlanExecutor({ executeTransactionMessage: (_, message) => { message satisfies TransactionMessage & TransactionMessageWithFeePayer; - return Promise.resolve({} as Transaction); + return Promise.resolve({ signature: {} as Signature }); }, }); } @@ -103,7 +128,7 @@ import { context.signature satisfies Signature | undefined; // @ts-expect-error Populating the signature is the callback's job; it is absent on entry. context.signature satisfies Signature; - return Promise.resolve({} as Signature); + return Promise.resolve({ signature: {} as Signature }); }, }); } @@ -118,7 +143,7 @@ import { const mySignedTransaction = {} as unknown as Transaction; context.transaction = mySignedTransaction; context.transaction satisfies Transaction; - return Promise.resolve(context.transaction); + return Promise.resolve({ signature: {} as Signature }); }, }); } @@ -127,7 +152,7 @@ import { { const executor = createTransactionPlanExecutor({ executeTransactionMessage: (_: { custom?: string }) => { - return Promise.resolve({} as Signature); + return Promise.resolve({ custom: 'value' }); }, }); executor satisfies TransactionPlanExecutor<{ custom?: string }>; @@ -140,7 +165,7 @@ import { createTransactionPlanExecutor({ // @ts-expect-error The context starts empty, so `custom` cannot be present on entry. executeTransactionMessage: (_: { custom: string }) => { - return Promise.resolve({} as Signature); + return Promise.resolve({ custom: 'value' }); }, }); } @@ -160,7 +185,7 @@ import { context.signature satisfies Signature | undefined; // @ts-expect-error Populating the signature is the callback's job; it is absent on entry. context.signature satisfies Signature; - return Promise.resolve({} as Signature); + return Promise.resolve({ custom: 'value', signature: {} as Signature }); }, }); executor satisfies TransactionPlanExecutor; @@ -173,7 +198,7 @@ import { context.custom satisfies string | undefined; // @ts-expect-error This context declares no `message`, so the executor does not add one. void context.message; - return Promise.resolve({} as Signature); + return Promise.resolve({ custom: 'value' }); }, }); } @@ -205,7 +230,7 @@ import { messageWithBlockhash satisfies TransactionMessageWithBlockhashLifetime; const transaction = compileTransaction(messageWithBlockhash); transaction satisfies TransactionWithBlockhashLifetime; - return Promise.resolve(transaction); + return Promise.resolve({ signature: {} as Signature }); }, }); } @@ -267,7 +292,7 @@ import { // Its results guarantee a signature by default, as they did before the context types were loosened. { const executor = createTransactionPlanExecutor({ - executeTransactionMessage: () => Promise.resolve({} as Signature), + executeTransactionMessage: () => Promise.resolve({ signature: {} as Signature }), }); void executor(null as unknown as TransactionPlan).then(result => { if (result.kind === 'single' && result.status === 'successful') { @@ -279,7 +304,7 @@ import { // Its failed results guarantee nothing. { const executor = createTransactionPlanExecutor({ - executeTransactionMessage: () => Promise.resolve({} as Signature), + executeTransactionMessage: () => Promise.resolve({ signature: {} as Signature }), }); void executor(null as unknown as TransactionPlan).then(result => { if (result.kind === 'single' && result.status === 'failed') { @@ -294,10 +319,7 @@ import { // intersecting it in, not from the executor adding it behind the caller's back. { const executor = createTransactionPlanExecutor({ - executeTransactionMessage: context => { - context.custom = 'value'; - return Promise.resolve({} as Signature); - }, + executeTransactionMessage: () => Promise.resolve({ custom: 'value', signature: {} as Signature }), }); void executor(null as unknown as TransactionPlan).then(result => { if (result.kind === 'single' && result.status === 'successful') { @@ -307,15 +329,12 @@ import { }); } - // A custom context that omits the signature does not get one back. The executor still - // populates `context.signature` at runtime, but it makes no type-level promise the caller - // did not ask for, which is what lets an executor be typed with no signature at all. + // A custom context that omits the signature does not get one back. Nothing writes to the + // context but the callback, so a context that never mentions a signature reports none — at + // the type level and at runtime alike. { const executor = createTransactionPlanExecutor<{ custom: string }>({ - executeTransactionMessage: context => { - context.custom = 'value'; - return Promise.resolve({} as Signature); - }, + executeTransactionMessage: () => Promise.resolve({ custom: 'value' }), }); void executor(null as unknown as TransactionPlan).then(result => { if (result.kind === 'single' && result.status === 'successful') { @@ -339,6 +358,15 @@ import { } } + // `createTransactionPlanExecutor` builds one. Returning a transaction the fee payer has not + // signed is enough, because nothing downstream tries to read a signature out of it. + { + const executor = createTransactionPlanExecutor<{ transaction: Transaction }>({ + executeTransactionMessage: () => Promise.resolve({ transaction: {} as Transaction }), + }); + executor satisfies TransactionPlanExecutor<{ transaction: Transaction }>; + } + // Such a result is still a TransactionPlanResult and works with the traversal helpers. { const result = null as unknown as TransactionPlanResult<{ transaction: Transaction }>; 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 9c3ed6824..a42b5bcc9 100644 --- a/packages/instruction-plans/src/__typetests__/transaction-plan-result-typetest.ts +++ b/packages/instruction-plans/src/__typetests__/transaction-plan-result-typetest.ts @@ -33,7 +33,6 @@ import { SingleTransactionPlanResult, SuccessfulSingleTransactionPlanResult, successfulSingleTransactionPlanResult, - successfulSingleTransactionPlanResultFromTransaction, SuccessfulTransactionPlanResult, TransactionPlanResult, TransactionPlanResultContext, @@ -43,8 +42,6 @@ import { const messageA = null as unknown as TransactionMessage & TransactionMessageWithFeePayer & { id: 'A' }; const messageB = null as unknown as TransactionMessage & TransactionMessageWithFeePayer & { id: 'B' }; const messageC = null as unknown as TransactionMessage & TransactionMessageWithFeePayer & { id: 'C' }; -const transactionA = null as unknown as Transaction & { id: 'A' }; -const transactionB = null as unknown as Transaction & { id: 'B' }; const error = null as unknown as Error; type CustomContext = { customData: string }; @@ -54,8 +51,8 @@ type CustomContext = { customData: string }; // It satisfies ParallelTransactionPlanResult. { const result = parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA), - successfulSingleTransactionPlanResultFromTransaction(messageB, transactionB), + successfulSingleTransactionPlanResult(messageA, { signature: 'A' as Signature }), + successfulSingleTransactionPlanResult(messageB, { signature: 'B' as Signature }), ]); result satisfies ParallelTransactionPlanResult; result satisfies TransactionPlanResult; @@ -64,8 +61,8 @@ type CustomContext = { customData: string }; // It can work with custom context. { const result = parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA, { customData: 'A' }), - successfulSingleTransactionPlanResultFromTransaction(messageB, transactionB, { customData: 'B' }), + successfulSingleTransactionPlanResult(messageA, { customData: 'A', signature: 'A' as Signature }), + successfulSingleTransactionPlanResult(messageB, { customData: 'B', signature: 'B' as Signature }), ]); result satisfies ParallelTransactionPlanResult; result satisfies TransactionPlanResult; @@ -74,9 +71,9 @@ type CustomContext = { customData: string }; // It can nest other result plans. { const result = parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA), + successfulSingleTransactionPlanResult(messageA, { signature: 'A' as Signature }), parallelTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageB, transactionB), + successfulSingleTransactionPlanResult(messageB, { signature: 'B' as Signature }), canceledSingleTransactionPlanResult(messageC), ]), ]); @@ -90,8 +87,8 @@ type CustomContext = { customData: string }; // It satisfies a divisible SequentialTransactionPlanResult. { const result = sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA), - successfulSingleTransactionPlanResultFromTransaction(messageB, transactionB), + successfulSingleTransactionPlanResult(messageA, { signature: 'A' as Signature }), + successfulSingleTransactionPlanResult(messageB, { signature: 'B' as Signature }), ]); result satisfies SequentialTransactionPlanResult & { divisible: true }; result satisfies TransactionPlanResult; @@ -100,8 +97,8 @@ type CustomContext = { customData: string }; // It can work with custom context. { const result = sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA, { customData: 'A' }), - successfulSingleTransactionPlanResultFromTransaction(messageB, transactionB, { customData: 'B' }), + successfulSingleTransactionPlanResult(messageA, { customData: 'A', signature: 'A' as Signature }), + successfulSingleTransactionPlanResult(messageB, { customData: 'B', signature: 'B' as Signature }), ]); result satisfies SequentialTransactionPlanResult & { divisible: true }; result satisfies TransactionPlanResult; @@ -110,9 +107,9 @@ type CustomContext = { customData: string }; // It can nest other result plans. { const result = sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA), + successfulSingleTransactionPlanResult(messageA, { signature: 'A' as Signature }), sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageB, transactionB), + successfulSingleTransactionPlanResult(messageB, { signature: 'B' as Signature }), canceledSingleTransactionPlanResult(messageC), ]), ]); @@ -126,8 +123,8 @@ type CustomContext = { customData: string }; // It satisfies a non-divisible SequentialTransactionPlanResult. { const result = nonDivisibleSequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA), - successfulSingleTransactionPlanResultFromTransaction(messageB, transactionB), + successfulSingleTransactionPlanResult(messageA, { signature: 'A' as Signature }), + successfulSingleTransactionPlanResult(messageB, { signature: 'B' as Signature }), ]); result satisfies SequentialTransactionPlanResult & { divisible: false }; result satisfies TransactionPlanResult; @@ -136,8 +133,8 @@ type CustomContext = { customData: string }; // It can work with custom context. { const result = nonDivisibleSequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA, { customData: 'A' }), - successfulSingleTransactionPlanResultFromTransaction(messageB, transactionB, { customData: 'B' }), + successfulSingleTransactionPlanResult(messageA, { customData: 'A', signature: 'A' as Signature }), + successfulSingleTransactionPlanResult(messageB, { customData: 'B', signature: 'B' as Signature }), ]); result satisfies SequentialTransactionPlanResult & { divisible: false }; result satisfies TransactionPlanResult; @@ -146,9 +143,9 @@ type CustomContext = { customData: string }; // It can nest other result plans. { const result = nonDivisibleSequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA), + successfulSingleTransactionPlanResult(messageA, { signature: 'A' as Signature }), nonDivisibleSequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageB, transactionB), + successfulSingleTransactionPlanResult(messageB, { signature: 'B' as Signature }), canceledSingleTransactionPlanResult(messageC), ]), ]); @@ -157,56 +154,6 @@ type CustomContext = { customData: string }; } } -// [DESCRIBE] successfulSingleTransactionPlanResultFromTransaction -{ - // It satisfies SingleTransactionPlanResult with a successful status. - { - const result = successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA); - result satisfies SuccessfulSingleTransactionPlanResult; - result satisfies TransactionPlanResult; - } - - // It can include a custom context. - { - const result = successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA, { - customData: 'test', - }); - result satisfies SuccessfulSingleTransactionPlanResult; - result satisfies TransactionPlanResult; - } - - // The result's context claims exactly what was passed — custom properties stay required — - // plus the signature and transaction derived from the transaction argument, both required. - { - const result = successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA, { - customData: 'test', - }); - result.context.customData satisfies string; - result.context.signature satisfies Signature; - result.context.transaction satisfies Transaction; - } - - // It does not add the optional `message` property of the default context. - { - const result = successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA, { - customData: 'test', - }); - // @ts-expect-error The context claims nothing but `customData`, `signature` and `transaction`. - void result.context.message; - } - - // With an explicit type argument, a passed context must supply the declared properties; - // nothing but the derived `signature` and `transaction` is asserted on the caller's behalf. - { - successfulSingleTransactionPlanResultFromTransaction( - messageA, - transactionA, - // @ts-expect-error The declared `customData` property is missing. - {}, - ); - } -} - // [DESCRIBE] successfulSingleTransactionPlanResult { // It satisfies SingleTransactionPlanResult with a successful status. @@ -265,7 +212,7 @@ type CustomContext = { customData: string }; { // It extracts single plan results from a simple plan result. { - const result = successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA); + const result = successfulSingleTransactionPlanResult(messageA, { signature: 'A' as Signature }); const results = flattenTransactionPlanResult(result); results satisfies SingleTransactionPlanResult[]; } @@ -274,8 +221,8 @@ type CustomContext = { customData: string }; { const result = parallelTransactionPlanResult([ sequentialTransactionPlanResult([ - successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA), - successfulSingleTransactionPlanResultFromTransaction(messageB, transactionB), + successfulSingleTransactionPlanResult(messageA, { signature: 'A' as Signature }), + successfulSingleTransactionPlanResult(messageB, { signature: 'B' as Signature }), ]), ]); const results = flattenTransactionPlanResult(result); diff --git a/packages/instruction-plans/src/transaction-plan-executor.ts b/packages/instruction-plans/src/transaction-plan-executor.ts index b5fbb4b20..9d8766de4 100644 --- a/packages/instruction-plans/src/transaction-plan-executor.ts +++ b/packages/instruction-plans/src/transaction-plan-executor.ts @@ -5,10 +5,8 @@ import { SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_TRANSACTION_PLAN_KIND, SolanaError, } from '@solana/errors'; -import type { Signature } from '@solana/keys'; import { getAbortablePromise } from '@solana/promises'; import type { TransactionMessage, TransactionMessageWithFeePayer } from '@solana/transaction-messages'; -import { getSignatureFromTransaction, type Transaction } from '@solana/transactions'; import type { ParallelTransactionPlan, @@ -18,14 +16,12 @@ import type { } from './transaction-plan'; import { createFailedToExecuteTransactionPlanError } from './transaction-plan-errors'; import { - BaseTransactionPlanResultContext, canceledSingleTransactionPlanResult, failedSingleTransactionPlanResult, parallelTransactionPlanResult, sequentialTransactionPlanResult, SingleTransactionPlanResult, successfulSingleTransactionPlanResult, - successfulSingleTransactionPlanResultFromTransaction, type TransactionPlanResult, type TransactionPlanResultContext, type TransactionPlanResultContextWithSignature, @@ -57,14 +53,11 @@ export type TransactionPlanExecutor< config?: { abortSignal?: AbortSignal }, ) => Promise>; -type ExecuteTransactionMessage< - TContext extends TransactionPlanResultContext, - TReturn = Signature | TContext | Transaction, -> = ( +type ExecuteTransactionMessage = ( context: Partial, transactionMessage: TransactionMessage & TransactionMessageWithFeePayer, config?: { abortSignal?: AbortSignal }, -) => Promise; +) => Promise; /** * Configuration object for creating a new transaction plan executor. @@ -78,40 +71,11 @@ export type TransactionPlanExecutorConfig< * Called whenever a transaction message must be sent to the blockchain. * * It should return the context that the successful result must carry — every property - * `TContext` promises, which by default includes a `signature`. Returning a {@link Signature} or - * a {@link Transaction} instead is deprecated. + * `TContext` promises, which by default includes a `signature`. */ executeTransactionMessage: ExecuteTransactionMessage; }; -/** - * Creates a new transaction plan executor based on the provided configuration. - * - * @param config - Configuration object containing the transaction message executor function. - * @return A {@link TransactionPlanExecutor} function that can execute transaction plans. - * - * @deprecated Returning a `Signature` or a `Transaction` from `executeTransactionMessage` is - * deprecated. Return the context that the successful result must carry instead — at minimum - * `{ signature }`, or `{ signature, transaction }` to keep reporting the transaction: - * ```diff - * executeTransactionMessage: async (context, message) => { - * const transaction = await signTransactionMessageWithSigners(message); - * + const signature = getSignatureFromTransaction(transaction); - * await sendAndConfirmTransaction(transaction, { commitment: 'confirmed' }); - * - return transaction; - * + return { signature, transaction }; - * } - * ``` - * Unlike this deprecated path, returning a context never derives a signature from a transaction, so - * it also works for transactions their fee payer has not signed. - * - * @see {@link TransactionPlanExecutorConfig} - */ -export function createTransactionPlanExecutor< - TContext extends TransactionPlanResultContext = TransactionPlanResultContextWithSignature, ->(config: { - executeTransactionMessage: ExecuteTransactionMessage; -}): TransactionPlanExecutor; /** * Creates a new transaction plan executor based on the provided configuration. * @@ -121,17 +85,23 @@ export function createTransactionPlanExecutor< * The `executeTransactionMessage` callback receives a mutable context object as its first * argument, which can be used to incrementally store useful data as execution progresses * (e.g. the latest version of the transaction message after setting its lifetime, the - * compiled and signed transaction, or any custom properties). This context is included - * in the resulting {@link SingleTransactionPlanResult} regardless of the outcome. This - * means that if an error is thrown at any point in the callback, any attributes already - * saved to the context will still be available in the plan result, which can be useful - * for debugging failures or building recovery plans. + * compiled and signed transaction, the transaction signature, or any custom properties). + * This context is included in the resulting {@link SingleTransactionPlanResult} regardless + * of the outcome. This means that if an error is thrown at any point in the callback, any + * attributes already saved to the context will still be available in the plan result, which + * can be useful for debugging failures or building recovery plans. + * + * The callback then returns the context a successful result should carry, as a complete + * `TContext`. The executor writes nothing to it on the callback's behalf — notably, it does not + * derive a `signature` from a stored transaction. Producing `signature` is therefore the callback's + * job, and an executor that produces transactions its fee payer has not signed can simply leave the + * property out and declare a `TContext` that does not require it. * - * The callback should return the context that a successful result must carry — every property - * `TContext` promises, which by default includes a `signature`, since the executor derives nothing - * on the callback's behalf. On success the returned context is merged over the one the callback - * mutated, with the returned value taking precedence, so a property stored on the context but left - * out of the return value is still reported. + * Requiring that return value is what keeps `TContext` honest: a callback that declares a context + * with a required `signature` and never produces one fails to compile, rather than yielding a + * result whose `context.signature` is typed but `undefined` at runtime. Note that the mutable + * context cannot itself be returned — every property on it is optional, so it does not satisfy + * `TContext`. Return an object built from the values you have instead: * * ```ts * executeTransactionMessage: async (context, message) => { @@ -143,15 +113,10 @@ export function createTransactionPlanExecutor< * } * ``` * - * Note that the callback cannot simply return the context it was given, since every property on it - * is optional. Build the return value from the values you have instead. - * - * Returning a {@link Signature} or a full {@link Transaction} object instead of a context is - * deprecated. Those return values are still honoured — a returned signature is stored as - * `context.signature`, and a returned transaction is stored as `context.transaction` with its - * signature derived from it — but deriving that signature throws - * `SOLANA_ERROR__TRANSACTION__FEE_PAYER_SIGNATURE_MISSING` when - * the fee payer has not signed, which returning a context avoids. + * The two channels serve different outcomes. Mutating the context makes a value available to a + * *failed* result; returning it makes a value available to a *successful* one. On success the two + * are merged, with the returned value taking precedence, so a property stored on the context but + * omitted from the return value is still reported. * * `TContext` is the only thing that says what a context contains — the executor adds nothing of its * own on top, in either direction. It defaults to {@link TransactionPlanResultContextWithSignature}, @@ -164,23 +129,20 @@ export function createTransactionPlanExecutor< * createTransactionPlanExecutor(config); * ``` * - * Note the asymmetry between the callback and the result it produces. A fresh context is created for + * Note the asymmetry between the callback's two context types. A fresh context is created for * every single transaction plan, so on entry it is empty and *every* property of `TContext` is - * optional inside the callback — populating them is the callback's job, not a guarantee the executor - * makes. The `TransactionPlanExecutor` this factory returns, on the other hand, reports the context - * as fully populated. Declare the properties you intend to set as an explicit type argument to this + * optional on the parameter — populating them is the callback's job, not a guarantee the executor + * makes. The value it returns is a complete `TContext`, which is what lets the + * `TransactionPlanExecutor` this factory returns report a successful result's context as fully + * populated. Declare the properties you intend to produce as an explicit type argument to this * function; a callback cannot annotate its own context parameter with required properties, because * none of them are present when it is called. * * - If that function is successful, the executor will return a successful `TransactionPlanResult` - * for that message, carrying the context described above. + * for that message, carrying the context the callback returned merged over the one it mutated. * - If that function throws an error, the executor will stop processing and cancel all * remaining transaction messages in the plan. The context accumulated up to the point of - * failure is preserved in the resulting {@link FailedSingleTransactionPlanResult}, with a - * `signature` derived from any `transaction` left on it. That derivation is unaffected by what the - * callback returns — it never got the chance to return anything — so a callback that works with - * transactions their fee payer has not signed should avoid storing them on the context, lest - * deriving a signature from one replace the error it meant to report. + * failure is preserved in the resulting {@link FailedSingleTransactionPlanResult}. * - If the `abortSignal` is triggered, the executor will immediately stop processing the plan and * return a `TransactionPlanResult` with the status set to `canceled`. * @@ -211,9 +173,6 @@ export function createTransactionPlanExecutor< * * @see {@link TransactionPlanExecutorConfig} */ -export function createTransactionPlanExecutor< - TContext extends TransactionPlanResultContext = TransactionPlanResultContextWithSignature, ->(config: TransactionPlanExecutorConfig): TransactionPlanExecutor; export function createTransactionPlanExecutor< TContext extends TransactionPlanResultContext = TransactionPlanResultContextWithSignature, >(config: TransactionPlanExecutorConfig): TransactionPlanExecutor { @@ -307,69 +266,25 @@ async function traverseSingle( } try { - const result = await getAbortablePromise( + const returnedContext = await getAbortablePromise( traverseConfig.executeTransactionMessage(context, transactionPlan.message, { abortSignal: traverseConfig.abortSignal, }), traverseConfig.abortSignal, ); - // Only on the happy path do we claim the context is fully populated. Except when the - // callback returned that context itself, we cannot verify that — the callback promised - // these properties by way of `TContext` and we take it at its word — so this is the one - // place an assertion is unavoidable. - if (typeof result === 'string') { - return successfulSingleTransactionPlanResult(transactionPlan.message, { - ...context, - signature: result, - } as unknown as TContext); - } - if (!isTransaction(result)) { - // The callback told us what context the result should carry, so we take it as-is and - // derive nothing from it. Anything it stored on the mutable context but left out of its - // return value is kept, since dropping it would lose data the callback deliberately - // recorded. - return successfulSingleTransactionPlanResult(transactionPlan.message, { - ...context, - ...result, - }); - } - return successfulSingleTransactionPlanResultFromTransaction( - transactionPlan.message, - result, - context as unknown as TContext, - ); + // The callback proved the context is fully populated by returning it, so no assertion is + // needed here. Anything it stored on the mutable context but left out of its return value + // is kept, since dropping it would lose data the callback deliberately recorded. + return successfulSingleTransactionPlanResult(transactionPlan.message, { + ...context, + ...returnedContext, + }); } catch (error) { traverseConfig.canceled = true; - // `TContext` no longer promises that a stored transaction is a `Transaction`, so this - // reads it back through the base context's shape before narrowing it at runtime. - const storedTransaction = context.transaction as BaseTransactionPlanResultContext['transaction']; - const contextWithSignature = - 'transaction' in context && typeof storedTransaction === 'object' && context.signature == null - ? { ...context, signature: getSignatureFromTransaction(storedTransaction) } - : context; - return failedSingleTransactionPlanResult( - transactionPlan.message, - error as Error, - contextWithSignature, - ); + return failedSingleTransactionPlanResult(transactionPlan.message, error as Error, context); } } -/** - * Tells apart the two things the `executeTransactionMessage` callback may return once a - * {@link Signature} has been ruled out: the deprecated {@link Transaction}, or the context a - * successful result should carry. Since `TContext` alone decides what a context contains, a - * returned context is not guaranteed to carry any particular property — not even a `signature` — - * so this discriminates on the shape of a `Transaction` instead. Every `Transaction` keeps its - * signatures in a `signatures` map, and no context type declares one, so that property is a - * reliable discriminator. - */ -function isTransaction( - returnValue: TContext | Transaction, -): returnValue is Transaction { - return 'signatures' in returnValue; -} - function assertDivisibleSequentialPlansOnly(transactionPlan: TransactionPlan): void { const kind = transactionPlan.kind; switch (kind) { diff --git a/packages/instruction-plans/src/transaction-plan-result.ts b/packages/instruction-plans/src/transaction-plan-result.ts index b3e2bbe2a..6fa62b185 100644 --- a/packages/instruction-plans/src/transaction-plan-result.ts +++ b/packages/instruction-plans/src/transaction-plan-result.ts @@ -6,7 +6,7 @@ import { } from '@solana/errors'; import { Signature } from '@solana/keys'; import { TransactionMessage, TransactionMessageWithFeePayer } from '@solana/transaction-messages'; -import { getSignatureFromTransaction, Transaction } from '@solana/transactions'; +import { Transaction } from '@solana/transactions'; /** * The result of executing a transaction plan. @@ -93,48 +93,20 @@ export type SuccessfulTransactionPlanResult< */ export type TransactionPlanResultContext = { [key: number | string | symbol]: unknown }; -/** - * The base context fields that {@link SuccessfulBaseTransactionPlanResultContext} builds upon. - * - * This type provides optional fields for the transaction message, signature, and - * full transaction object. These fields may or may not be populated depending on - * how far execution progressed before the result was produced. - * - * @deprecated This type will be removed in the next major version. It is no longer part of any - * result type — the context of a result is entirely caller-defined, exactly the `TContext` you - * supply — so there is no separate base shape to merge in. If you refer to this type, declare - * whichever of its fields you need on your own context type instead: - * ```ts - * type MyContext = { - * message?: TransactionMessage & TransactionMessageWithFeePayer; - * signature?: Signature; - * transaction?: Transaction; - * }; - * ``` - * - * @see {@link TransactionPlanResultContextWithSignature} - * @see {@link successfulSingleTransactionPlanResultFromTransaction} - */ -export interface BaseTransactionPlanResultContext { - message?: TransactionMessage & TransactionMessageWithFeePayer; - signature?: Signature; - transaction?: Transaction; -} - /** * The base context fields for a {@link SuccessfulSingleTransactionPlanResult}. * - * This extends the base context by requiring a {@link Signature}, since a - * successful transaction always produces one. The transaction message and full - * transaction object remain optional. + * This requires a {@link Signature}, since a successful transaction always produces + * one. The transaction message and full transaction object remain optional. * * @deprecated use {@link TransactionPlanResultContextWithSignature} instead as the context type argument. * * @see {@link TransactionPlanResultContextWithSignature} - * @see {@link BaseTransactionPlanResultContext} */ -export interface SuccessfulBaseTransactionPlanResultContext extends BaseTransactionPlanResultContext { +export interface SuccessfulBaseTransactionPlanResultContext { + message?: TransactionMessage & TransactionMessageWithFeePayer; signature: Signature; + transaction?: Transaction; } /** @@ -150,9 +122,11 @@ export interface SuccessfulBaseTransactionPlanResultContext extends BaseTransact * const executor = createTransactionPlanExecutor< * TransactionPlanResultContextWithSignature & { startedAt: number } * >({ - * executeTransactionMessage: async context => { - * context.startedAt = Date.now(); - * // ... + * executeTransactionMessage: async (context, message) => { + * const startedAt = Date.now(); + * context.startedAt = startedAt; + * const signature = await sendAndConfirm(message); + * return { signature, startedAt }; * }, * }); * ``` @@ -511,67 +485,6 @@ export function parallelTransactionPlanResult< return Object.freeze({ kind: 'parallel', planType: 'transactionPlanResult', plans }); } -/** - * Creates a successful {@link SingleTransactionPlanResult} from a transaction message and transaction. - * - * This function creates a single result with a 'successful' status, indicating that - * the transaction was successfully executed. It also includes the original transaction - * message, the executed transaction, and an optional context object. - * - * @typeParam TContext - The type of the context object - * @typeParam TTransactionMessage - The type of the transaction message - * @param plannedMessage - The original transaction message - * @param transaction - The successfully executed transaction - * @param context - Optional context object to be included with the result. The result's context - * claims exactly what you pass, plus the `signature` and `transaction` derived from the - * `transaction` argument. Anything you do pass for those two keys is overwritten by the derived - * values - * - * @example - * ```ts - * const result = successfulSingleTransactionPlanResultFromTransaction( - * transactionMessage, - * transaction - * ); - * result satisfies SingleTransactionPlanResult; - * ``` - * - * @deprecated Call {@link successfulSingleTransactionPlanResult} instead, passing the context - * explicitly. This helper derives the `signature` for you by calling - * {@link getSignatureFromTransaction}, which throws when the transaction's fee payer has not - * signed it — the explicit spelling makes that step visible and avoidable: - * ```diff - * - successfulSingleTransactionPlanResultFromTransaction(message, transaction); - * + successfulSingleTransactionPlanResult(message, { - * + signature: getSignatureFromTransaction(transaction), - * + transaction, - * + }); - * ``` - * - * @see {@link SingleTransactionPlanResult} - */ -export function successfulSingleTransactionPlanResultFromTransaction< - TContext extends TransactionPlanResultContext = TransactionPlanResultContextWithSignature, - TTransactionMessage extends TransactionMessage & TransactionMessageWithFeePayer = TransactionMessage & - TransactionMessageWithFeePayer, ->( - plannedMessage: TTransactionMessage, - transaction: Transaction, - context?: TContext, -): SuccessfulSingleTransactionPlanResult< - TContext & { signature: Signature; transaction: Transaction }, - TTransactionMessage -> { - const signature = getSignatureFromTransaction(transaction); - return Object.freeze({ - context: Object.freeze({ ...((context ?? {}) as TContext), signature, transaction }), - kind: 'single', - planType: 'transactionPlanResult', - plannedMessage, - status: 'successful', - }); -} - /** * Creates a successful {@link SingleTransactionPlanResult} from a transaction message and context. *