Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/petite-signs-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@solana/instruction-plans': minor
'@solana/errors': minor
---

Add `createTransactionPlanner` implementation for the `TransactionPlanner` type.
6 changes: 6 additions & 0 deletions packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ export const SOLANA_ERROR__TRANSACTION_ERROR__UNBALANCED_TRANSACTION = 7050036;
// Reserve error codes in the range [7618000-7618999].
export const SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN = 7618000;
export const SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE = 7618001;
export const SOLANA_ERROR__INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN = 7618002;

// Codec-related errors.
// Reserve error codes in the range [8078000-8078999].
Expand Down Expand Up @@ -311,6 +312,8 @@ export const SOLANA_ERROR__INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_MUST_NOT_P
export const SOLANA_ERROR__INVARIANT_VIOLATION__CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING = 9900002;
export const SOLANA_ERROR__INVARIANT_VIOLATION__SWITCH_MUST_BE_EXHAUSTIVE = 9900003;
export const SOLANA_ERROR__INVARIANT_VIOLATION__DATA_PUBLISHER_CHANNEL_UNIMPLEMENTED = 9900004;
export const SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_INSTRUCTION_PLAN_KIND = 9900005;
export const SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_TRANSACTION_PLAN_KIND = 9900006;

/**
* A union of every Solana error code
Expand Down Expand Up @@ -430,12 +433,15 @@ export type SolanaErrorCode =
| typeof SOLANA_ERROR__INSTRUCTION_ERROR__UNKNOWN
| typeof SOLANA_ERROR__INSTRUCTION_ERROR__UNSUPPORTED_PROGRAM_ID
| typeof SOLANA_ERROR__INSTRUCTION_ERROR__UNSUPPORTED_SYSVAR
| typeof SOLANA_ERROR__INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN
| typeof SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN
| typeof SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE
| typeof SOLANA_ERROR__INVALID_BLOCKHASH_BYTE_LENGTH
| typeof SOLANA_ERROR__INVALID_NONCE
| typeof SOLANA_ERROR__INVARIANT_VIOLATION__CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING
| typeof SOLANA_ERROR__INVARIANT_VIOLATION__DATA_PUBLISHER_CHANNEL_UNIMPLEMENTED
| typeof SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_INSTRUCTION_PLAN_KIND
| typeof SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_TRANSACTION_PLAN_KIND
| typeof SOLANA_ERROR__INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE
| typeof SOLANA_ERROR__INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_STATE_MISSING
| typeof SOLANA_ERROR__INVARIANT_VIOLATION__SWITCH_MUST_BE_EXHAUSTIVE
Expand Down
8 changes: 8 additions & 0 deletions packages/errors/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ import {
SOLANA_ERROR__INVALID_NONCE,
SOLANA_ERROR__INVARIANT_VIOLATION__CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING,
SOLANA_ERROR__INVARIANT_VIOLATION__DATA_PUBLISHER_CHANNEL_UNIMPLEMENTED,
SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_INSTRUCTION_PLAN_KIND,
SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_TRANSACTION_PLAN_KIND,
SOLANA_ERROR__INVARIANT_VIOLATION__SWITCH_MUST_BE_EXHAUSTIVE,
SOLANA_ERROR__JSON_RPC__INTERNAL_ERROR,
SOLANA_ERROR__JSON_RPC__INVALID_PARAMS,
Expand Down Expand Up @@ -427,6 +429,12 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<
channelName: string;
supportedChannelNames: string[];
};
[SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_INSTRUCTION_PLAN_KIND]: {
kind: string;
};
[SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_TRANSACTION_PLAN_KIND]: {
kind: string;
};
[SOLANA_ERROR__INVARIANT_VIOLATION__SWITCH_MUST_BE_EXHAUSTIVE]: {
unexpectedValue: unknown;
};
Expand Down
6 changes: 6 additions & 0 deletions packages/errors/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,15 @@ import {
SOLANA_ERROR__INSTRUCTION_ERROR__UNKNOWN,
SOLANA_ERROR__INSTRUCTION_ERROR__UNSUPPORTED_PROGRAM_ID,
SOLANA_ERROR__INSTRUCTION_ERROR__UNSUPPORTED_SYSVAR,
SOLANA_ERROR__INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN,
SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN,
SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE,
SOLANA_ERROR__INVALID_BLOCKHASH_BYTE_LENGTH,
SOLANA_ERROR__INVALID_NONCE,
SOLANA_ERROR__INVARIANT_VIOLATION__CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING,
SOLANA_ERROR__INVARIANT_VIOLATION__DATA_PUBLISHER_CHANNEL_UNIMPLEMENTED,
SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_INSTRUCTION_PLAN_KIND,
SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_TRANSACTION_PLAN_KIND,
SOLANA_ERROR__INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE,
SOLANA_ERROR__INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_STATE_MISSING,
SOLANA_ERROR__INVARIANT_VIOLATION__SWITCH_MUST_BE_EXHAUSTIVE,
Expand Down Expand Up @@ -397,8 +400,11 @@ export const SolanaErrorMessages: Readonly<{
[SOLANA_ERROR__INSTRUCTION_ERROR__UNKNOWN]: '',
[SOLANA_ERROR__INSTRUCTION_ERROR__UNSUPPORTED_PROGRAM_ID]: 'Unsupported program id',
[SOLANA_ERROR__INSTRUCTION_ERROR__UNSUPPORTED_SYSVAR]: 'Unsupported sysvar',
[SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_INSTRUCTION_PLAN_KIND]: 'Invalid instruction plan kind: $kind.',
[SOLANA_ERROR__INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN]: 'The provided instruction plan is empty.',
[SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN]:
'The provided message has insufficient capacity to accommodate the next instruction(s) in this plan. Expected at least $numBytesRequired free byte(s), got $numFreeBytes byte(s).',
[SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_TRANSACTION_PLAN_KIND]: 'Invalid transaction plan kind: $kind.',
[SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE]:
'No more instructions to pack; the message packer has completed the instruction plan.',
[SOLANA_ERROR__INSTRUCTION__EXPECTED_TO_HAVE_ACCOUNTS]: 'The instruction does not have any accounts.',
Expand Down
2 changes: 2 additions & 0 deletions packages/instruction-plans/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,13 @@
"dependencies": {
"@solana/errors": "workspace:*",
"@solana/instructions": "workspace:*",
"@solana/promises": "workspace:*",
"@solana/transaction-messages": "workspace:*",
"@solana/transactions": "workspace:*"
},
"devDependencies": {
"@solana/addresses": "workspace:*",
"@solana/codecs": "workspace:*",
"@solana/functional": "workspace:*"
},
"peerDependencies": {
Expand Down
89 changes: 89 additions & 0 deletions packages/instruction-plans/src/__tests__/__setup__.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Address, getAddressDecoder } from '@solana/addresses';
import { fixEncoderSize, getUtf8Encoder } from '@solana/codecs';
import { SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN, SolanaError } from '@solana/errors';
import type { Instruction } from '@solana/instructions';
import {
appendTransactionMessageInstruction,
type BaseTransactionMessage,
type TransactionMessageWithFeePayer,
} from '@solana/transaction-messages';
import { getTransactionMessageSize, TRANSACTION_SIZE_LIMIT } from '@solana/transactions';

import { MessagePackerInstructionPlan } from '../instruction-plan';

const MINIMUM_INSTRUCTION_SIZE = 35;

export function instructionFactory(baseSeed?: string) {
const seedPrefix = baseSeed ? `${baseSeed}-` : '';
const seedEncoder = fixEncoderSize(getUtf8Encoder(), 32);
const addressDecoder = getAddressDecoder();
const getProgramAddress = (seed: string): Address => addressDecoder.decode(seedEncoder.encode(seed));

return (seed: string, bytes: number): Instruction => {
if (bytes < MINIMUM_INSTRUCTION_SIZE) {
throw new Error(`Instruction size must be at least ${MINIMUM_INSTRUCTION_SIZE} bytes`);
}
return {
data: new Uint8Array(bytes - MINIMUM_INSTRUCTION_SIZE),
programAddress: getProgramAddress(`${seedPrefix}${seed}`),
};
};
}

export function transactionPercentFactory(
createTransactionMessage: () => BaseTransactionMessage & TransactionMessageWithFeePayer,
) {
const minimumTransactionSize = getTransactionMessageSize(createTransactionMessage());
const remainingSize = TRANSACTION_SIZE_LIMIT - minimumTransactionSize - 1; /* For shortU16. */
return (percent: number) => Math.floor((remainingSize * percent) / 100);
}

export function createMessagePackerInstructionPlan(
totalBytes: number,
baseSeed?: string,
): MessagePackerInstructionPlan & Readonly<{ get: (offset: number, length: number) => Instruction }> {
const getInstruction = instructionFactory(baseSeed ? `message-packer-${baseSeed}` : 'message-packer');
const getInstructionFromOffsetAndLength = (offset: number, length: number): Instruction =>
getInstruction(`${offset}-${length}`, length);

// Note that we cannot use `getLinearMessagePackerInstructionPlan` here because
// we want the `MINIMUM_INSTRUCTION_SIZE` to be included in our calculations.
// For instance, if an instruction that takes 50% of the transaction size,
// This should include the base instruction size to simplify our expectations.
const baseInstruction = getInstructionFromOffsetAndLength(0, MINIMUM_INSTRUCTION_SIZE);
return Object.freeze({
get: getInstructionFromOffsetAndLength,
getMessagePacker: () => {
let offset = 0;
return {
done: () => offset >= totalBytes,
packMessageToCapacity: message => {
const messageSizeWithBaseInstruction = getTransactionMessageSize(
appendTransactionMessageInstruction(baseInstruction, message),
);
const freeSpace =
TRANSACTION_SIZE_LIMIT -
messageSizeWithBaseInstruction /* Includes the base instruction (length: 0). */ -
1; /* Leeway for shortU16 numbers in transaction headers. */

if (freeSpace <= 0) {
const messageSize = getTransactionMessageSize(message);
throw new SolanaError(SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN, {
// (+1) We need to pack at least one byte of data otherwise
// there is no point packing the base instruction alone.
numBytesRequired: messageSizeWithBaseInstruction - messageSize + 1,
// (-1) Leeway for shortU16 numbers in transaction headers.
numFreeBytes: TRANSACTION_SIZE_LIMIT - messageSize - 1,
});
}

const length = Math.min(totalBytes - offset, freeSpace + MINIMUM_INSTRUCTION_SIZE);
const instruction = getInstructionFromOffsetAndLength(offset, length);
offset += length;
return appendTransactionMessageInstruction(instruction, message);
},
};
},
kind: 'messagePacker',
});
}
Loading