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
5 changes: 5 additions & 0 deletions .changeset/yummy-paths-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solana/instruction-plans": patch
---

The transaction planner now handles the four new transaction compilation constraint errors (`TOO_MANY_ACCOUNT_ADDRESSES`, `TOO_MANY_SIGNER_ADDRESSES`, `TOO_MANY_INSTRUCTIONS`, `TOO_MANY_ACCOUNTS_IN_INSTRUCTION`) gracefully. When adding an instruction to an existing candidate transaction would violate a constraint, the planner splits it into a new transaction — the same behaviour it already had for transactions that exceed the byte size limit. If even a fresh transaction cannot accommodate the instruction, the constraint error propagates to the caller.
186 changes: 184 additions & 2 deletions packages/instruction-plans/src/__tests__/transaction-planner-test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import '@solana/test-matchers/toBeFrozenObject';

import { SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN, SolanaError } from '@solana/errors';
import { Instruction } from '@solana/instructions';
import { Address, getAddressDecoder } from '@solana/addresses';
import {
SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN,
SOLANA_ERROR__TRANSACTION__TOO_MANY_ACCOUNT_ADDRESSES,
SOLANA_ERROR__TRANSACTION__TOO_MANY_ACCOUNTS_IN_INSTRUCTION,
SOLANA_ERROR__TRANSACTION__TOO_MANY_INSTRUCTIONS,
SOLANA_ERROR__TRANSACTION__TOO_MANY_SIGNER_ADDRESSES,
SolanaError,
} from '@solana/errors';
import { AccountRole, Instruction } from '@solana/instructions';
import {
appendTransactionMessageInstructions,
TransactionMessage,
Expand Down Expand Up @@ -91,6 +99,180 @@ describe('createTransactionPlanner', () => {
});
});

describe('transaction constraint scenarios', () => {
const addressDecoder = getAddressDecoder();
function makeAddress(i: number): Address {
return addressDecoder.decode(new Uint8Array(32).fill(i));
}
function makeAddresses(n: number, offset = 0): Address[] {
return Array.from({ length: n }, (_, i) => makeAddress(offset + i));
}

/**
* [Seq] ──────────────────────────────────────▶ [Seq]
* │ (65 instructions, same program address) │
* ├── [I0] ├── [Tx: I0…I63]
* ├── … └── [Tx: I64]
* └── [I64]
*/
it('splits into a new transaction when there are too many instructions', async () => {
expect.assertions(1);
const createTransactionMessage = createMockTransactionMessage;
const { singleTransactionPlan } = getHelpers(createTransactionMessage);
const planner = createTransactionPlanner({ createTransactionMessage });

// All 65 instructions share the same program address so they don't add
// to the unique-account count. The 65th triggers TOO_MANY_INSTRUCTIONS.
const instruction: Instruction = { programAddress: makeAddress(1) };
const instructions = Array.from({ length: 65 }, () => instruction);

await expect(
planner(sequentialInstructionPlan(instructions.map(i => singleInstructionPlan(i)))),
).resolves.toEqual(
sequentialTransactionPlan([
singleTransactionPlan(instructions.slice(0, 64)),
singleTransactionPlan([instruction]),
]),
);
});

/**
* [Seq] ──────────────────────────────────────────────────▶ [Seq]
* │ (A fills to 33 unique accounts; B adds 33 more │
* │ pushing the combined total to 66 > 64) ├── [Tx: A]
* ├── [A] └── [Tx: B]
* └── [B]
*/
it('splits into a new transaction when there are too many account addresses', async () => {
expect.assertions(1);
const createTransactionMessage = createMockTransactionMessage;
const { singleTransactionPlan } = getHelpers(createTransactionMessage);
const planner = createTransactionPlanner({ createTransactionMessage });

// Base message: feePayer (1)
// A: A's program (1) + 31 READONLY accounts = 32 new unique accounts.
// B: B's program (1) + 32 new READONLY accounts = 33 new unique accounts.
// Combined: 66 unique accounts → TOO_MANY_ACCOUNT_ADDRESSES.
// A alone: fee payer (1) + A's program (1) + 31 accounts = 33 unique accounts (within limit).
// B alone: fee payer (1) + B's program (1) + 32 accounts = 34 accounts (within limit).
const instructionA: Instruction = {
accounts: makeAddresses(31).map(address => ({ address, role: AccountRole.READONLY })),
programAddress: makeAddress(32),
};
const instructionB: Instruction = {
accounts: makeAddresses(32, 32).map(address => ({ address, role: AccountRole.READONLY })),
programAddress: makeAddress(100),
};

await expect(
planner(
sequentialInstructionPlan([
singleInstructionPlan(instructionA),
singleInstructionPlan(instructionB),
]),
),
).resolves.toEqual(
sequentialTransactionPlan([
singleTransactionPlan([instructionA]),
singleTransactionPlan([instructionB]),
]),
);
});

/**
* [Seq] ──────────────────────────────────────────────────▶ [Seq]
* │ (A fills to 11 signers; B adds 2 more │
* │ pushing the combined total to 13 > 12) ├── [Tx: A]
* ├── [A] └── [Tx: B]
* └── [B]
*/
it('splits into a new transaction when there are too many signer addresses', async () => {
expect.assertions(1);
const createTransactionMessage = createMockTransactionMessage;
const { singleTransactionPlan } = getHelpers(createTransactionMessage);
const planner = createTransactionPlanner({ createTransactionMessage });

// Base message: feePayer (1 signer)
// A: 10 new READONLY_SIGNER
// B: 2 new READONLY_SIGNER
// Combined: 13 signers → TOO_MANY_SIGNER_ADDRESSES.
// A alone: fee payer (1) + A's 10 signers = 11 signers (within limit).
// B alone: fee payer (1) + B's 2 signers = 3 signers (within limit).
const instructionA: Instruction = {
accounts: makeAddresses(10).map(address => ({ address, role: AccountRole.READONLY_SIGNER })),
programAddress: makeAddress(20),
};
const instructionB: Instruction = {
accounts: makeAddresses(2, 10).map(address => ({ address, role: AccountRole.READONLY_SIGNER })),
programAddress: makeAddress(30),
};

await expect(
planner(
sequentialInstructionPlan([
singleInstructionPlan(instructionA),
singleInstructionPlan(instructionB),
]),
),
).resolves.toEqual(
sequentialTransactionPlan([
singleTransactionPlan([instructionA]),
singleTransactionPlan([instructionB]),
]),
);
});

const CONSTRAINT_ERRORS: [string, SolanaError][] = [
[
'TOO_MANY_ACCOUNT_ADDRESSES',
new SolanaError(SOLANA_ERROR__TRANSACTION__TOO_MANY_ACCOUNT_ADDRESSES, {
actualCount: 65,
maxAllowed: 64,
}),
],
[
'TOO_MANY_SIGNER_ADDRESSES',
new SolanaError(SOLANA_ERROR__TRANSACTION__TOO_MANY_SIGNER_ADDRESSES, {
actualCount: 13,
maxAllowed: 12,
}),
],
[
'TOO_MANY_INSTRUCTIONS',
new SolanaError(SOLANA_ERROR__TRANSACTION__TOO_MANY_INSTRUCTIONS, {
actualCount: 65,
maxAllowed: 64,
}),
],
[
'TOO_MANY_ACCOUNTS_IN_INSTRUCTION',
new SolanaError(SOLANA_ERROR__TRANSACTION__TOO_MANY_ACCOUNTS_IN_INSTRUCTION, {
actualCount: 256,
instructionIndex: 0,
maxAllowed: 255,
}),
],
];

/**
* [A] ──────────▶ Error
* (createTransactionMessage always throws the constraint error)
*/
it.each(CONSTRAINT_ERRORS)(
'propagates %s when createTransactionMessage cannot create a fresh message',
async (_name, constraintError) => {
expect.assertions(1);
const instruction: Instruction = { programAddress: makeAddress(1) };
const planner = createTransactionPlanner({
createTransactionMessage: () => {
throw constraintError;
},
});
await expect(planner(singleInstructionPlan(instruction))).rejects.toThrow(constraintError);
},
);
});

describe('sequential scenarios', () => {
/**
* [Seq] ───────────────────▶ [Tx: A + B]
Expand Down
12 changes: 11 additions & 1 deletion packages/instruction-plans/src/transaction-planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import {
SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN,
SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_INSTRUCTION_PLAN_KIND,
SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_TRANSACTION_PLAN_KIND,
SOLANA_ERROR__TRANSACTION__TOO_MANY_ACCOUNT_ADDRESSES,
SOLANA_ERROR__TRANSACTION__TOO_MANY_ACCOUNTS_IN_INSTRUCTION,
SOLANA_ERROR__TRANSACTION__TOO_MANY_INSTRUCTIONS,
SOLANA_ERROR__TRANSACTION__TOO_MANY_SIGNER_ADDRESSES,
SolanaError,
} from '@solana/errors';
import { getAbortablePromise } from '@solana/promises';
Expand Down Expand Up @@ -327,7 +331,13 @@ async function selectAndMutateCandidate(
return candidate;
}
} catch (error) {
if (isSolanaError(error, SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN)) {
if (
isSolanaError(error, SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN) ||
isSolanaError(error, SOLANA_ERROR__TRANSACTION__TOO_MANY_ACCOUNT_ADDRESSES) ||
isSolanaError(error, SOLANA_ERROR__TRANSACTION__TOO_MANY_ACCOUNTS_IN_INSTRUCTION) ||
isSolanaError(error, SOLANA_ERROR__TRANSACTION__TOO_MANY_INSTRUCTIONS) ||
isSolanaError(error, SOLANA_ERROR__TRANSACTION__TOO_MANY_SIGNER_ADDRESSES)
) {
// Try the next candidate.
} else {
throw error;
Expand Down
Loading