Skip to content

Add TransactionPlanResult type and helpers #543

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: 06-10-add_transactionplan_type_and_helpers
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/afraid-tables-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solana/instruction-plans': patch
---

Add new `TransactionPlanResult` type with helpers. This type describes the execution results of transaction plans with the same structural hierarchy — capturing the execution status of each transaction message whether executed in parallel, sequentially, or as a single transaction.
1 change: 1 addition & 0 deletions packages/instruction-plans/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"maintained node versions"
],
"dependencies": {
"@solana/errors": "workspace:*",
"@solana/instructions": "workspace:*",
"@solana/transaction-messages": "workspace:*",
"@solana/transactions": "workspace:*"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import '@solana/test-matchers/toBeFrozenObject';

import { Address } from '@solana/addresses';
import { SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE, SolanaError } from '@solana/errors';
import { pipe } from '@solana/functional';
import {
CompilableTransactionMessage,
setTransactionMessageLifetimeUsingBlockhash,
} from '@solana/transaction-messages';
import { createTransactionMessage, setTransactionMessageFeePayer } from '@solana/transaction-messages';
import { Transaction } from '@solana/transactions';

import {
canceledSingleTransactionPlanResult,
failedSingleTransactionPlanResult,
nonDivisibleSequentialTransactionPlanResult,
parallelTransactionPlanResult,
sequentialTransactionPlanResult,
successfulSingleTransactionPlanResult,
} from '../transaction-plan-result';

function createMessage<TId extends string>(id: TId): CompilableTransactionMessage & { id: TId } {
return pipe(
createTransactionMessage({ version: 0 }),
m => setTransactionMessageFeePayer('E9Nykp3rSdza2moQutaJ3K3RSC8E5iFERX2SqLTsQfjJ' as Address, m),
// TODO(loris): Either remove lifetime constraint or use the new
// `fillMissingTransactionMessageLifetimeUsingProvisoryBlockhash`
// function from https://github.com/anza-xyz/kit/pull/519.
m =>
setTransactionMessageLifetimeUsingBlockhash(
{} as Parameters<typeof setTransactionMessageLifetimeUsingBlockhash>[0],
m,
),
m => Object.freeze({ ...m, id }),
);
}

function createTransaction<TId extends string>(id: TId): Transaction & { id: TId } {
return Object.freeze({ id }) as unknown as Transaction & { id: TId };
}

describe('successfulSingleTransactionPlanResult', () => {
it('creates SingleTransactionPlanResult objects with successful status', () => {
const messageA = createMessage('A');
const transactionA = createTransaction('A');
const result = successfulSingleTransactionPlanResult(messageA, transactionA);
expect(result).toEqual({
kind: 'single',
message: messageA,
status: { context: {}, kind: 'successful', transaction: transactionA },
});
});
it('accepts an optional context object', () => {
const messageA = createMessage('A');
const transactionA = createTransaction('A');
const context = { foo: 'bar' };
const result = successfulSingleTransactionPlanResult(messageA, transactionA, context);
expect(result).toEqual({
kind: 'single',
message: messageA,
status: { context, kind: 'successful', transaction: transactionA },
});
});
it('freezes created SingleTransactionPlanResult objects', () => {
const messageA = createMessage('A');
const transactionA = createTransaction('A');
const result = successfulSingleTransactionPlanResult(messageA, transactionA);
expect(result).toBeFrozenObject();
});
it('freezes the status object of created SingleTransactionPlanResult objects', () => {
const messageA = createMessage('A');
const transactionA = createTransaction('A');
const result = successfulSingleTransactionPlanResult(messageA, transactionA);
expect(result.status).toBeFrozenObject();
});
});

describe('failedSingleTransactionPlanResult', () => {
it('creates SingleTransactionPlanResult objects with failed status', () => {
const messageA = createMessage('A');
const error = new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE);
const result = failedSingleTransactionPlanResult(messageA, error);
expect(result).toEqual({
kind: 'single',
message: messageA,
status: { error, kind: 'failed' },
});
});
it('freezes created SingleTransactionPlanResult objects', () => {
const messageA = createMessage('A');
const error = new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE);
const result = failedSingleTransactionPlanResult(messageA, error);
expect(result).toBeFrozenObject();
});
it('freezes the status object of created SingleTransactionPlanResult objects', () => {
const messageA = createMessage('A');
const error = new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE);
const result = failedSingleTransactionPlanResult(messageA, error);
expect(result.status).toBeFrozenObject();
});
});

describe('canceledSingleTransactionPlanResult', () => {
it('creates SingleTransactionPlanResult objects with canceled status', () => {
const messageA = createMessage('A');
const result = canceledSingleTransactionPlanResult(messageA);
expect(result).toEqual({
kind: 'single',
message: messageA,
status: { kind: 'canceled' },
});
});
it('freezes created SingleTransactionPlanResult objects', () => {
const messageA = createMessage('A');
const result = canceledSingleTransactionPlanResult(messageA);
expect(result).toBeFrozenObject();
});
it('freezes the status object of created SingleTransactionPlanResult objects', () => {
const messageA = createMessage('A');
const result = canceledSingleTransactionPlanResult(messageA);
expect(result.status).toBeFrozenObject();
});
});

describe('parallelTransactionPlanResult', () => {
it('creates ParallelTransactionPlanResult objects from other results', () => {
const planA = canceledSingleTransactionPlanResult(createMessage('A'));
const planB = canceledSingleTransactionPlanResult(createMessage('B'));
const result = parallelTransactionPlanResult([planA, planB]);
expect(result).toEqual({
kind: 'parallel',
plans: [planA, planB],
});
});
it('can nest other result types', () => {
const planA = canceledSingleTransactionPlanResult(createMessage('A'));
const planB = canceledSingleTransactionPlanResult(createMessage('B'));
const planC = canceledSingleTransactionPlanResult(createMessage('C'));
const result = parallelTransactionPlanResult([planA, parallelTransactionPlanResult([planB, planC])]);
expect(result).toEqual({
kind: 'parallel',
plans: [planA, { kind: 'parallel', plans: [planB, planC] }],
});
});
it('freezes created ParallelTransactionPlanResult objects', () => {
const planA = canceledSingleTransactionPlanResult(createMessage('A'));
const planB = canceledSingleTransactionPlanResult(createMessage('B'));
const result = parallelTransactionPlanResult([planA, planB]);
expect(result).toBeFrozenObject();
});
});

describe('sequentialTransactionPlanResult', () => {
it('creates divisible SequentialTransactionPlanResult objects from other results', () => {
const planA = canceledSingleTransactionPlanResult(createMessage('A'));
const planB = canceledSingleTransactionPlanResult(createMessage('B'));
const result = sequentialTransactionPlanResult([planA, planB]);
expect(result).toEqual({
divisible: true,
kind: 'sequential',
plans: [planA, planB],
});
});
it('can nest other result types', () => {
const planA = canceledSingleTransactionPlanResult(createMessage('A'));
const planB = canceledSingleTransactionPlanResult(createMessage('B'));
const planC = canceledSingleTransactionPlanResult(createMessage('C'));
const result = sequentialTransactionPlanResult([planA, sequentialTransactionPlanResult([planB, planC])]);
expect(result).toEqual({
divisible: true,
kind: 'sequential',
plans: [planA, { divisible: true, kind: 'sequential', plans: [planB, planC] }],
});
});
it('freezes created SequentialTransactionPlanResult objects', () => {
const planA = canceledSingleTransactionPlanResult(createMessage('A'));
const planB = canceledSingleTransactionPlanResult(createMessage('B'));
const result = sequentialTransactionPlanResult([planA, planB]);
expect(result).toBeFrozenObject();
});
});

describe('nonDivisibleSequentialTransactionPlanResult', () => {
it('creates non-divisible SequentialTransactionPlanResult objects from other results', () => {
const planA = canceledSingleTransactionPlanResult(createMessage('A'));
const planB = canceledSingleTransactionPlanResult(createMessage('B'));
const result = nonDivisibleSequentialTransactionPlanResult([planA, planB]);
expect(result).toEqual({
divisible: false,
kind: 'sequential',
plans: [planA, planB],
});
});
it('can nest other result types', () => {
const planA = canceledSingleTransactionPlanResult(createMessage('A'));
const planB = canceledSingleTransactionPlanResult(createMessage('B'));
const planC = canceledSingleTransactionPlanResult(createMessage('C'));
const result = nonDivisibleSequentialTransactionPlanResult([
planA,
nonDivisibleSequentialTransactionPlanResult([planB, planC]),
]);
expect(result).toEqual({
divisible: false,
kind: 'sequential',
plans: [planA, { divisible: false, kind: 'sequential', plans: [planB, planC] }],
});
});
it('freezes created SequentialTransactionPlanResult objects', () => {
const planA = canceledSingleTransactionPlanResult(createMessage('A'));
const planB = canceledSingleTransactionPlanResult(createMessage('B'));
const result = nonDivisibleSequentialTransactionPlanResult([planA, planB]);
expect(result).toBeFrozenObject();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import type { SolanaError } from '@solana/errors';
import type { CompilableTransactionMessage } from '@solana/transaction-messages';
import type { Transaction } from '@solana/transactions';

import {
canceledSingleTransactionPlanResult,
failedSingleTransactionPlanResult,
nonDivisibleSequentialTransactionPlanResult,
ParallelTransactionPlanResult,
parallelTransactionPlanResult,
SequentialTransactionPlanResult,
sequentialTransactionPlanResult,
SingleTransactionPlanResult,
successfulSingleTransactionPlanResult,
TransactionPlanResult,
} from '../transaction-plan-result';

const messageA = null as unknown as CompilableTransactionMessage & { id: 'A' };
const messageB = null as unknown as CompilableTransactionMessage & { id: 'B' };
const messageC = null as unknown as CompilableTransactionMessage & { id: 'C' };
const transactionA = null as unknown as Transaction;
const transactionB = null as unknown as Transaction;
const error = null as unknown as SolanaError;

type CustomContext = { customData: string };

// [DESCRIBE] parallelTransactionPlanResult
{
// It satisfies ParallelTransactionPlanResult.
{
const result = parallelTransactionPlanResult([
successfulSingleTransactionPlanResult(messageA, transactionA),
successfulSingleTransactionPlanResult(messageB, transactionB),
]);
result satisfies ParallelTransactionPlanResult;
result satisfies TransactionPlanResult;
}

// It can work with custom context.
{
const result = parallelTransactionPlanResult([
successfulSingleTransactionPlanResult(messageA, transactionA, { customData: 'A' }),
successfulSingleTransactionPlanResult(messageB, transactionB, { customData: 'B' }),
]);
result satisfies ParallelTransactionPlanResult<CustomContext>;
result satisfies TransactionPlanResult;
}

// It can nest other result plans.
{
const result = parallelTransactionPlanResult([
successfulSingleTransactionPlanResult(messageA, transactionA),
parallelTransactionPlanResult([
successfulSingleTransactionPlanResult(messageB, transactionB),
canceledSingleTransactionPlanResult(messageC),
]),
]);
result satisfies ParallelTransactionPlanResult;
result satisfies TransactionPlanResult;
}
}

// [DESCRIBE] sequentialTransactionPlanResult
{
// It satisfies a divisible SequentialTransactionPlanResult.
{
const result = sequentialTransactionPlanResult([
successfulSingleTransactionPlanResult(messageA, transactionA),
successfulSingleTransactionPlanResult(messageB, transactionB),
]);
result satisfies SequentialTransactionPlanResult & { divisible: true };
result satisfies TransactionPlanResult;
}

// It can work with custom context.
{
const result = sequentialTransactionPlanResult([
successfulSingleTransactionPlanResult(messageA, transactionA, { customData: 'A' }),
successfulSingleTransactionPlanResult(messageB, transactionB, { customData: 'B' }),
]);
result satisfies SequentialTransactionPlanResult<CustomContext> & { divisible: true };
result satisfies TransactionPlanResult;
}

// It can nest other result plans.
{
const result = sequentialTransactionPlanResult([
successfulSingleTransactionPlanResult(messageA, transactionA),
sequentialTransactionPlanResult([
successfulSingleTransactionPlanResult(messageB, transactionB),
canceledSingleTransactionPlanResult(messageC),
]),
]);
result satisfies SequentialTransactionPlanResult & { divisible: true };
result satisfies TransactionPlanResult;
}
}

// [DESCRIBE] nonDivisibleSequentialTransactionPlanResult
{
// It satisfies a non-divisible SequentialTransactionPlanResult.
{
const result = nonDivisibleSequentialTransactionPlanResult([
successfulSingleTransactionPlanResult(messageA, transactionA),
successfulSingleTransactionPlanResult(messageB, transactionB),
]);
result satisfies SequentialTransactionPlanResult & { divisible: false };
result satisfies TransactionPlanResult;
}

// It can work with custom context.
{
const result = nonDivisibleSequentialTransactionPlanResult([
successfulSingleTransactionPlanResult(messageA, transactionA, { customData: 'A' }),
successfulSingleTransactionPlanResult(messageB, transactionB, { customData: 'B' }),
]);
result satisfies SequentialTransactionPlanResult<CustomContext> & { divisible: false };
result satisfies TransactionPlanResult;
}

// It can nest other result plans.
{
const result = nonDivisibleSequentialTransactionPlanResult([
successfulSingleTransactionPlanResult(messageA, transactionA),
nonDivisibleSequentialTransactionPlanResult([
successfulSingleTransactionPlanResult(messageB, transactionB),
canceledSingleTransactionPlanResult(messageC),
]),
]);
result satisfies SequentialTransactionPlanResult & { divisible: false };
result satisfies TransactionPlanResult;
}
}

// [DESCRIBE] successfulSingleTransactionPlanResult
{
// It satisfies SingleTransactionPlanResult with a successful status.
{
const result = successfulSingleTransactionPlanResult(messageA, transactionA);
result satisfies SingleTransactionPlanResult<object, typeof messageA>;
result satisfies TransactionPlanResult;
}

// It can include a custom context.
{
const result = successfulSingleTransactionPlanResult(messageA, transactionA, { customData: 'test' });
result satisfies SingleTransactionPlanResult<CustomContext, typeof messageA>;
result satisfies TransactionPlanResult;
}
}

// [DESCRIBE] failedSingleTransactionPlanResult
{
// It satisfies SingleTransactionPlanResult with a failed status.
{
const result = failedSingleTransactionPlanResult(messageA, error);
result satisfies SingleTransactionPlanResult<object, typeof messageA>;
result satisfies TransactionPlanResult;
}
}

// [DESCRIBE] canceledSingleTransactionPlanResult
{
// It satisfies SingleTransactionPlanResult with a canceled status.
{
const result = canceledSingleTransactionPlanResult(messageA);
result satisfies SingleTransactionPlanResult<object, typeof messageA>;
result satisfies TransactionPlanResult;
}
}
Loading