npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

solana-transaction-validator

v0.1.1

Published

Declarative policy engine for validating Solana transactions

Downloads

180

Readme

solana-transaction-validator

Declarative transaction validation for Solana remote signers. Define exactly what your keys can sign with a type-safe, composable policy engine built for @solana/kit.

Secure by default: Programs and instructions are denied unless explicitly allowed.

Features

  • Declarative Policy: Define rules using simple configuration objects.
  • Type-Safe: Full TypeScript support for all programs and instructions.
  • Composable: Mix and match validators for different programs.
  • Secure Defaults: Strict allowlist approach — everything is denied unless explicitly allowed.
  • Comprehensive Support: Built-in validators for System, SPL Token, Token-2022, Compute Budget, and Memo programs.
  • Customizable: Add custom validation logic with full access to parsed instruction data.

Table of Contents

Installation

npm install solana-transaction-validator

Requires @solana/kit as a peer dependency.

Quick Start

Minimal Example

The absolute minimum to get started:

import {
    createTransactionValidator,
    createSystemProgramValidator,
    SignerRole,
    SystemInstruction,
} from "solana-transaction-validator";

// Treasury wallet that ONLY pays fees, never participates in transactions
const validator = createTransactionValidator({
    global: { signerRole: SignerRole.FeePayerOnly },
    programs: [
        createSystemProgramValidator({
            instructions: {
                [SystemInstruction.TransferSol]: true, // Allow SOL transfers
            },
        }),
    ],
});

// Validate before signing
await validator(wireTransaction, signerAddress);

Real-World Example

A hot wallet that can only send small amounts to a treasury address:

import { address } from "@solana/kit";
import {
    createTransactionValidator,
    createSystemProgramValidator,
    createComputeBudgetValidator,
    SignerRole,
    SystemInstruction,
    ComputeBudgetInstruction,
} from "solana-transaction-validator";

const TREASURY = address("Treasury111111111111111111111111111111111111");

const hotWalletValidator = createTransactionValidator({
    global: {
        signerRole: SignerRole.Any,
        maxInstructions: 10,
    },
    programs: [
        // System Program: Only small transfers to treasury
        createSystemProgramValidator({
            instructions: {
                // Declarative config: Simple and readable
                [SystemInstruction.TransferSol]: {
                    maxLamports: 100_000_000n, // Max 0.1 SOL per transfer
                    allowedDestinations: [TREASURY],
                },
                [SystemInstruction.CreateAccount]: false, // Explicitly forbidden
                // All other instructions denied by default (secure by default)
            },
        }),
        // Compute Budget: Required for all transactions
        createComputeBudgetValidator({
            instructions: {
                [ComputeBudgetInstruction.SetComputeUnitLimit]: {
                    maxUnits: 1_400_000,
                },
                [ComputeBudgetInstruction.SetComputeUnitPrice]: true,
            },
            required: true, // Must be present
        }),
    ],
});

// Use the validator
try {
    await hotWalletValidator(wireTransaction, signerAddress);
    // Transaction passed all checks - safe to sign
} catch (error) {
    // ValidationError thrown with specific reason
    console.error("Rejected:", error.message);
}

Need custom logic? Use callback validators for full programmatic control:

createSystemProgramValidator({
    instructions: {
        [SystemInstruction.TransferSol]: async (ctx, parsed) => {
            // parsed is fully typed as ParsedTransferSolInstruction
            const { amount } = parsed.data;
            const { destination } = parsed.accounts;

            // Custom validation logic
            if (amount > 100_000_000n) {
                return "Transfer exceeds 0.1 SOL limit";
            }
            if (destination.address !== TREASURY) {
                return "Can only send to treasury";
            }

            // Add time-based checks, logging, database lookups, etc.
            return true; // Allow
        },
    },
});

See Custom Validation Callbacks for more examples.

Global Policy

The global policy controls high-level transaction properties.

global: {
    // REQUIRED: Role the signer plays in the transaction
    // - FeePayerOnly: Signer must be fee payer, cannot be a participant in instructions
    // - ParticipantOnly: Signer must be a participant, cannot be fee payer
    // - Any: No restrictions
    signerRole: SignerRole.FeePayerOnly,

    // Optional: Instruction count limits
    minInstructions: 1, // Default: 1 (prevents empty transactions)
    maxInstructions: 10,

    // Optional: Allowed transaction versions
    allowedVersions: [0], // Default: [0] (v0 transactions only). Use ['legacy'] for legacy.

    // Optional: Address Lookup Table (ALT) policy (v0 only)
    addressLookupTables: false, // Default: false (deny all ALTs)
}

Address Lookup Tables

You can configure detailed ALT rules:

addressLookupTables: {
    allowedTables: [address("TrustedTable1111111111111111111111111111111")],
    maxTables: 2,
    maxIndexedAccounts: 32,
}

Program Validators

System Program

Validates native SOL transfers, account creation, and nonce operations.

createSystemProgramValidator({
    instructions: {
        // Simple allow/deny
        [SystemInstruction.AdvanceNonceAccount]: true,
        [SystemInstruction.UpgradeNonceAccount]: false,

        // Configuration object
        [SystemInstruction.TransferSol]: {
            maxLamports: 5_000_000_000n,
        },
        [SystemInstruction.CreateAccount]: {
            maxLamports: 10_000_000n,
            maxSpace: 1000n,
            allowedOwnerPrograms: [address("MyProgram11111111111111111111111111111111")],
        },
    },
});

SPL Token & Token-2022

Validates token operations. Both createSplTokenValidator and createToken2022Validator share the same configuration structure.

createSplTokenValidator({
    instructions: {
        [TokenInstruction.TransferChecked]: {
            maxAmount: 1_000_000n,
            allowedMints: [address("USDC...")],
        },
        [TokenInstruction.MintTo]: {
            maxAmount: 500n,
            allowedMints: [address("MyToken...")],
        },
        [TokenInstruction.Burn]: true,
        [TokenInstruction.FreezeAccount]: {
            allowedAuthorities: [address("MyAuthority...")],
        },
    },
});

Compute Budget

Control compute unit limits and pricing.

import {
    createComputeBudgetValidator,
    ComputeBudgetInstruction,
} from "solana-transaction-validator";

createComputeBudgetValidator({
    instructions: {
        [ComputeBudgetInstruction.SetComputeUnitLimit]: {
            maxUnits: 1_400_000,
        },
        [ComputeBudgetInstruction.SetComputeUnitPrice]: {
            maxMicroLamportsPerCu: 1_000_000n,
        },
    },
    required: true, // Enforce that compute budget instructions are present
});

Memo

Validate memo content and length.

import { createMemoValidator, MemoInstruction } from "solana-transaction-validator";

createMemoValidator({
    instructions: {
        [MemoInstruction.Memo]: {
            maxLength: 256,
            requiredPrefix: "app:",
        },
    },
});

Custom Programs

For programs without built-in validators, use createCustomProgramValidator. This allows you to define rules based on instruction discriminators.

import { createCustomProgramValidator } from "solana-transaction-validator";

createCustomProgramValidator({
    programAddress: address("MyCustomProgram1111111111111111111111111"),
    instructions: [
        // Allow instruction with specific 8-byte discriminator (e.g., Anchor)
        {
            discriminator: new Uint8Array([0x9a, 0x5c, 0x1b, 0x3d, 0x8f, 0x2e, 0x7a, 0x4c]),
        },
        // Allow instruction with 1-byte discriminator
        {
            discriminator: new Uint8Array([1]),
        },
        // Allow with custom validation logic
        {
            discriminator: new Uint8Array([2]),
            validate: async (ctx, ix) => {
                // Inspect raw instruction data
                if (ix.data.length > 100) return "Data too long";
                return true;
            },
        },
    ],
});

Instruction Configuration

Each instruction can be configured in five ways:

| Config | Behavior | | :--------------------- | :--------------------------------------------------------------------------------------------- | | undefined (omitted) | Denied (Implicit). Secure by default. | | false | Denied (Explicit). Use this to document known but forbidden instructions. | | true | Allowed. No constraints on parameters. | | { ...config } | Allowed with Constraints. Checks parameters against the config object (e.g., maxAmount). | | (ctx, parsed) => ... | Custom Logic. Full programmatic control with typed parsed data. |

Custom Validation Callbacks

For maximum flexibility, use callback functions instead of declarative config. Callbacks receive the ValidationContext and the fully typed parsed instruction, giving you complete programmatic control.

Note: Examples below use placeholder constants like TREASURY_ADDRESS, USDC_MINT, etc. Define these as Address values using address("...") from @solana/kit.

Basic Example: Conditional Logic

createSystemProgramValidator({
    instructions: {
        [SystemInstruction.TransferSol]: async (ctx, parsed) => {
            // 'parsed' is fully typed as ParsedTransferSolInstruction
            const { amount } = parsed.data;
            const { destination } = parsed.accounts;

            // TypeScript knows exact types:
            // - amount: bigint
            // - destination.address: Address

            // High-value transfers need extra approval
            if (amount > 1_000_000_000n && destination.address !== TREASURY_ADDRESS) {
                return "Transfers over 1 SOL must go to treasury";
            }

            return true; // Allow
        },
    },
});

Using ValidationContext

Access transaction-wide information through the ValidationContext:

createSplTokenValidator({
    instructions: {
        [TokenInstruction.TransferChecked]: async (ctx, parsed) => {
            // Check if this signer is allowed to transfer
            if (ctx.signer === RESTRICTED_WALLET) {
                return "This wallet is restricted from token transfers";
            }

            // Inspect the full transaction
            const instructionCount = ctx.decompiledMessage.instructions.length;
            if (instructionCount > 3) {
                return "Token transfers not allowed in complex transactions";
            }

            // Access other instructions in the transaction
            const hasComputeBudget = ctx.decompiledMessage.instructions.some(
                (ix) => ix.programAddress === COMPUTE_BUDGET_PROGRAM_ADDRESS,
            );
            if (!hasComputeBudget) {
                return "Token transfers must include compute budget instructions";
            }

            return true;
        },
    },
});

Async Operations: Database Lookups

createSystemProgramValidator({
    instructions: {
        [SystemInstruction.TransferSol]: async (ctx, parsed) => {
            const recipient = parsed.accounts.destination.address;

            // Async database lookup
            const isBlacklisted = await checkBlacklist(recipient);
            if (isBlacklisted) {
                return `Recipient ${recipient} is blacklisted`;
            }

            // Check rate limits
            const recentTransfers = await getRecentTransfers(ctx.signer);
            if (recentTransfers.length > 10) {
                return "Rate limit exceeded: max 10 transfers per hour";
            }

            return true;
        },
    },
});

Complex Multi-Condition Validation

createSystemProgramValidator({
    instructions: {
        [SystemInstruction.CreateAccount]: async (ctx, parsed) => {
            const { lamports, space, programAddress } = parsed.data;
            const { payer } = parsed.accounts;

            // Multi-step validation with detailed error messages
            if (lamports > 10_000_000n) {
                return "CreateAccount: Cannot fund accounts with more than 0.01 SOL";
            }

            if (space > 10_000n) {
                return "CreateAccount: Space allocation too large (max 10KB)";
            }

            // Allowlist owner programs
            const allowedPrograms = [TOKEN_PROGRAM_ADDRESS, TOKEN_2022_PROGRAM_ADDRESS];
            if (!allowedPrograms.includes(programAddress)) {
                return `CreateAccount: Owner program ${programAddress} not in allowlist`;
            }

            // Ensure payer matches expected wallet
            if (payer.address !== ctx.signer) {
                return "CreateAccount: Payer must be the signer";
            }

            return true;
        },
    },
});

Stateful Validation with Counters

// Track approvals per session
const approvalCounts = new Map<Address, bigint>();

createSplTokenValidator({
    instructions: {
        [TokenInstruction.Approve]: async (ctx, parsed) => {
            const { delegate } = parsed.accounts;
            const { amount } = parsed.data;

            // Track total approved amount per delegate
            const currentTotal = approvalCounts.get(delegate.address) ?? 0n;
            const newTotal = currentTotal + amount;

            if (newTotal > 1_000_000n) {
                return `Delegate ${delegate.address} would exceed approval limit`;
            }

            // Update counter on success
            approvalCounts.set(delegate.address, newTotal);
            return true;
        },
    },
});

Combining Declarative + Callback

You can also use callbacks alongside declarative config in the same validator:

createSystemProgramValidator({
    instructions: {
        // Declarative: Simple and readable
        [SystemInstruction.AdvanceNonceAccount]: true,

        // Declarative with config
        [SystemInstruction.TransferSol]: {
            maxLamports: 5_000_000_000n,
        },

        // Custom callback: Full control when needed
        [SystemInstruction.CreateAccount]: async (ctx, parsed) => {
            // Complex logic here
            return myCustomValidation(ctx, parsed);
        },
    },
});

Return Values

Callbacks can return three types of values:

// ✅ Allow
return true;

// ❌ Deny with generic message
return false;

// ❌ Deny with specific reason (recommended)
return "Transfer amount exceeds daily limit";

Type Safety

All callbacks are fully typed based on the instruction:

createSystemProgramValidator({
    instructions: {
        [SystemInstruction.TransferSol]: async (ctx, parsed) => {
            // parsed: ParsedTransferSolInstruction
            // TypeScript autocomplete shows:
            // - parsed.data.amount: bigint
            // - parsed.accounts.source.address: Address
            // - parsed.accounts.destination.address: Address
        },
        [SystemInstruction.CreateAccount]: async (ctx, parsed) => {
            // parsed: ParsedCreateAccountInstruction (different type!)
            // - parsed.data.lamports: bigint
            // - parsed.data.space: bigint
            // - parsed.data.programAddress: Address
            // - parsed.accounts.payer.address: Address
        },
    },
});

Required Programs

You can enforce that specific programs or instructions must be present in the transaction.

createSystemProgramValidator({
    instructions: { /* ... */ },
    // Simple requirement: Program must be present
    required: true,

    // Specific requirement: Program must be present AND contain these instructions
    // required: [SystemInstruction.TransferSol],
}),

Error Handling

The validator throws a ValidationError when validation fails. Always wrap validation calls in try-catch:

import { ValidationError } from "solana-transaction-validator";

try {
    await validator(wireTransaction, signerAddress);
    // Transaction passed all checks - safe to sign
    await signAndSendTransaction(transaction);
} catch (error) {
    if (error instanceof ValidationError) {
        // Validation failed with a specific reason
        console.error("Transaction rejected:", error.message);
        // Examples:
        // - "Global policy rejected transaction"
        // - "Instruction 2 uses unauthorized program ..."
        // - "System Program: TransferSol amount 2000000000 exceeds limit 1000000000"
    } else {
        // Other error (network, parsing, etc.)
        throw error;
    }
}

Advanced Patterns

Multi-Program Composition

Combine validators for complex use cases:

const validator = createTransactionValidator({
    global: {
        signerRole: SignerRole.FeePayerOnly,
        maxInstructions: 10,
    },
    programs: [
        // Allow specific System Program operations
        createSystemProgramValidator({
            instructions: {
                [SystemInstruction.TransferSol]: { maxLamports: 1_000_000_000n },
            },
        }),
        // Require compute budget in all transactions
        createComputeBudgetValidator({
            instructions: {
                [ComputeBudgetInstruction.SetComputeUnitLimit]: {
                    maxUnits: 1_400_000,
                },
                [ComputeBudgetInstruction.SetComputeUnitPrice]: true,
            },
            required: true,
        }),
        // Allow token transfers with strict limits
        createSplTokenValidator({
            instructions: {
                [TokenInstruction.TransferChecked]: {
                    maxAmount: 100_000n,
                    allowedMints: [USDC_MINT],
                },
            },
        }),
        // Custom program with discriminator-based allowlisting
        createCustomProgramValidator({
            programAddress: MY_PROTOCOL_PROGRAM,
            instructions: [
                { discriminator: new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]) },
            ],
        }),
    ],
});

Transaction Input Formats

The validator accepts three input formats:

// 1. Base64 string (most common - from wallets)
await validator("AQABAgMEBQYH...", signerAddress);

// 2. Raw bytes (Uint8Array)
await validator(transactionBytes, signerAddress);

// 3. Transaction object (most efficient - skip decoding)
import { getTransactionDecoder } from "@solana/kit";
const transaction = getTransactionDecoder().decode(transactionBytes);
await validator(transaction, signerAddress);

API Reference

Core Types

  • TransactionValidator: The main validation function returned by createTransactionValidator.
  • ValidationContext: Context object passed to custom callbacks containing:
    • signer: Address - The public key attempting to sign
    • transaction: Transaction - The full transaction object
    • decompiledMessage: DecompiledTransactionMessage - High-level view with resolved addresses
    • compiledMessage: CompiledTransactionMessage - Low-level view with account indices
  • ValidationResult: Return type for validators (true | false | string)
  • ValidationError: Error thrown when validation fails

Built-in Validators

| Function | Program | Enum | | :----------------------------- | :---------------------------- | :------------------------- | | createSystemProgramValidator | System Program | SystemInstruction | | createSplTokenValidator | SPL Token | TokenInstruction | | createToken2022Validator | Token Extensions (Token-2022) | Token2022Instruction | | createComputeBudgetValidator | Compute Budget | ComputeBudgetInstruction | | createMemoValidator | Memo | MemoInstruction | | createCustomProgramValidator | Custom/Unknown Programs | N/A |

Contributing

See CONTRIBUTING.md for development setup and guidelines.

License

MIT