sandermuller/solana-php-sdk

PHP SDK for Solana — keypairs, transactions, RPC client, and program bindings (SPL Token, SNS, System).

Maintainers

Package info

github.com/SanderMuller/solana-php-sdk

Homepage

pkg:composer/sandermuller/solana-php-sdk

Statistics

Installs: 26

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

0.1.0 2026-05-13 11:54 UTC

This package is auto-updated.

Last update: 2026-05-13 12:14:22 UTC


README

A PHP SDK for the Solana blockchain. Read accounts, sign and submit transactions, watch the chain over WebSockets, and call any Anchor program — all from PHP.

Status: open source under MIT. Works on PHP 8.3 / 8.4 with Laravel 11 / 12 / 13.

What you get

Layer What it does Class
Keys & signatures Generate keypairs, sign messages, verify Ed25519 signatures Keypair, PublicKey
Raw JSON-RPC One method: call($method, $params) — pass anything Solana defines Services\SolanaRpcClient
Typed JSON-RPC ~50 typed methods covering ~90 % of the HTTP-RPC spec Connection
WebSocket PubSub Account / signature / slot / logs / program / vote / block subscriptions Services\SolanaPubSubClient
Transactions Legacy + V0 (versioned) transactions, Address Lookup Tables Transaction, VersionedTransaction, MessageV0
Programs System, SPL Token, Token-2022, ComputeBudget, AddressLookupTable, Stake, Vote, Memo, Metaplex, DID-Sol, SNS Programs\*
Send & confirm Priority-fee estimator + sendAndConfirmTransaction helper with blockhash-expiry detection Util\PriorityFee, Connection::sendAndConfirmTransaction
Pluggable RPC transports Multi-endpoint fallback / round-robin / exponential-backoff retry Rpc\FallbackTransport, Rpc\RoundRobinTransport, Rpc\RetryTransport
Structured sendTransaction errors Decoded TransactionError + InstructionError enums, program logs, units consumed Exceptions\SendTransactionError, Errors\TransactionErrorDecoder
Auto compute-budget + priority fee Simulate → derive CU limit → inject setComputeUnitLimit + setComputeUnitPrice on build Fees\AutoComputeBudget, Fees\PriorityFeeStrategy, TransactionBuilder::withAutoComputeBudget
Laravel facade + Pest harness Solana::fake() rebinds the RPC client; toBeConfirmed / toHaveCustomCode / toBeInstructionError macros Facades\Solana, Testing\PestExpectations
Queue-based confirmation ConfirmTransactionJob polls + fires TransactionConfirmed / TransactionExpired events Queue\ConfirmTransactionJob, Events\*
PDA / ATA helpers One-line Pda::find + Ata::derive (Token-2022 aware via Ata::derive2022) Util\Pda, Util\Ata
Anchor Parse any Anchor IDL at runtime → typed TransactionInstruction builders Anchor\AnchorIdl

Who this is for

  • PHP devs new to Solana. Start with the 5-minute quickstart — you'll send a real on-chain transfer.
  • Laravel apps. The package auto-registers a service provider; resolve Connection straight from the container.
  • Standalone PHP scripts. A one-liner bootstrap (Bootstrap::createContainer) wires up the same container outside Laravel.

Install

composer require sandermuller/solana-php-sdk

The package self-registers SanderMuller\SolanaPhpSdk\ServiceProvider via Laravel's package discovery. Standalone PHP doesn't need anything extra — see "Outside Laravel" below.

Requirements: PHP 8.3+ with ext-sodium enabled (every mainstream PHP distro ships it). Composer pulls in paragonie/sodium_compat as a polyfill backup.

5-minute quickstart

We'll go from zero to a signed-and-broadcast SOL transfer on devnet.

1. Generate (or load) a keypair

use SanderMuller\SolanaPhpSdk\Keypair;

$payer = Keypair::generate();
echo $payer->getPublicKey()->toBase58(), "\n";
// → e.g.  4uTYf8w...EPnL  (this is your wallet address)

Store the secret bytes somewhere safe if you want to reuse this wallet:

$secret = $payer->getSecretKey()->toArray(); // array<int, int> — 64 bytes
// Reload with:  Keypair::fromSecretKey($secret);

2. Get some devnet SOL

use SanderMuller\SolanaPhpSdk\Connection;
use SanderMuller\SolanaPhpSdk\Enum\Network;

$connection = app(Connection::class);          // Laravel
// $connection = Bootstrap::createContainer(__DIR__.'/config.php')->get(Connection::class); // standalone

$signature = $connection->requestAirdrop([
    $payer->getPublicKey()->toBase58(),
    1_000_000_000,                              // 1 SOL in lamports
]);
$connection->confirmTransaction($signature);    // blocks until landed

3. Check your balance

$lamports = $connection->getBalance($payer->getPublicKey()->toBase58());
echo $lamports / 1_000_000_000, " SOL\n";       // → 1.0

4. Send SOL to someone

use SanderMuller\SolanaPhpSdk\Keypair;
use SanderMuller\SolanaPhpSdk\Programs\SystemProgram;
use SanderMuller\SolanaPhpSdk\Transaction;

$recipient = Keypair::generate()->getPublicKey();   // pretend this is a friend

$tx = new Transaction(feePayer: $payer->getPublicKey());
$tx->addInstructions(
    SystemProgram::transfer(
        fromPubkey:  $payer->getPublicKey(),
        toPublicKey: $recipient,
        lamports:    5_000_000,                       // 0.005 SOL
    ),
);

$signature = $connection->sendTransaction($tx, signers: [$payer]);
$connection->confirmTransaction($signature);

echo "https://explorer.solana.com/tx/{$signature}?cluster=devnet\n";

Click the URL — your transaction is on-chain.

Sign from a KMS / HSM / hardware wallet

For hosts that cannot expose secret bytes to PHP (KMS-backed keys, Ledger devices, remote signing services), implement Contracts\MessageSigner:

use SanderMuller\SolanaPhpSdk\Contracts\MessageSigner;
use SanderMuller\SolanaPhpSdk\PublicKey;

final class AwsKmsSigner implements MessageSigner
{
    public function __construct(
        private readonly string $kmsKeyId,
        private readonly PublicKey $publicKey,
        private readonly KmsClient $kms,
    ) {}

    public function getPublicKey(): PublicKey { return $this->publicKey; }

    public function signMessage(string $message): string
    {
        // Return 64-byte Ed25519 signature from your KMS / HSM.
        return $this->kms->signEd25519($this->kmsKeyId, $message);
    }
}

$tx = TransactionBuilder::new()
    ->feePayer($publicKey)
    ->recentBlockhash($connection->latestBlockhash())
    ->addInstruction($instruction)
    ->addMessageSigner(new AwsKmsSigner(/* … */))
    ->build();

For the in-process case, wrap a Keypair in Signing\InMemoryMessageSigner to get the same interface.

Sketch: HashiCorp Vault Transit
final class VaultTransitSigner implements MessageSigner
{
    public function __construct(
        private readonly string $keyName,
        private readonly PublicKey $publicKey,
        private readonly VaultClient $vault,
    ) {}

    public function getPublicKey(): PublicKey { return $this->publicKey; }

    public function signMessage(string $message): string
    {
        $reply = $this->vault->write(
            "transit/sign/{$this->keyName}/ed25519",
            ['input' => base64_encode($message), 'hash_algorithm' => 'none'],
        );

        // Vault returns "vault:v1:<base64-signature>"; strip prefix + decode.
        return base64_decode(explode(':', $reply['data']['signature'])[2]);
    }
}
Sketch: Ledger hardware wallet

The Ledger Solana app expects a base58-derivation-path-prefixed APDU. Wrap any USB/HID transport (e.g. via a sidecar process bridged over stdio) and implement signMessage() as a single INS_SIGN_MESSAGE APDU round-trip.

Sanitize-safe builder

Most transaction failures across Solana SDKs hit the same opaque error: "Transaction failed to sanitize accounts offsets correctly". The fluent TransactionBuilder catches the causes locally so you never see that message:

use SanderMuller\SolanaPhpSdk\TransactionBuilder;
use SanderMuller\SolanaPhpSdk\Programs\SystemProgram;

$tx = TransactionBuilder::new()
    ->feePayer($payer->getPublicKey())
    ->recentBlockhash($connection->latestBlockhash())   // BlockhashInfo or string
    ->addInstruction(SystemProgram::transfer($payer->getPublicKey(), $to, 1))
    ->addSigner($payer)
    ->build();

build() throws UnsanitizedTransactionException if the fee payer is missing, the blockhash is missing, an instruction marks an account isSigner with no matching keypair, or two instructions disagree on whether the same account is a signer. Every detected reason is surfaced at once on $exception->reasons.

One-shot send-and-confirm

Most apps want both steps in one call, with blockhash-expiry built in:

$status = $connection->sendAndConfirmTransaction(
    transaction: $tx,
    signers:     [$payer],
);
// $status->confirmationStatus === 'confirmed'

The helper fetches getLatestBlockhash, signs, sends, and polls getSignatureStatuses until the commitment is reached or the blockhash expires — no manual retry loop.

Core concepts (the 90-second glossary)

Term Plain English
Lamport Smallest unit of SOL. 1 SOL = 1 000 000 000 (10⁹) lamports.
Pubkey / address 32 bytes, base58-encoded. Identifies a wallet, token mint, program, anything.
Keypair Pubkey + 64-byte Ed25519 secret. Sign things with the secret. Never log it.
Blockhash Recent slot hash that anchors a transaction in time. Expires in ~60–90 s. The SDK fetches one for you if you don't supply one.
Instruction One call to a program: program id + ordered accounts + opaque data bytes.
Transaction One or more instructions, signed by one or more keypairs, submitted atomically.
Commitment How sure you are the network agrees: processed (fastest, can roll back) → confirmedfinalized (irreversible).
RPC The HTTP JSON-RPC endpoint at https://api.mainnet-beta.solana.com (or devnet/testnet). Call getBalance, sendTransaction, etc.
PubSub The matching WebSocket endpoint at wss://api.mainnet-beta.solana.com. Subscribe and get pushed events.
Program Solana's word for a smart contract. Has a fixed pubkey.
SPL Token The token standard. Every fungible / NFT token is an account owned by the SPL Token program.
Anchor A Rust framework most Solana programs are written in. Ships an "IDL" JSON describing its instructions — point this SDK at one and you can call any Anchor program.

Common recipes

Read an account

$info = $connection->accountInfo('11111111111111111111111111111111');

// Typed DTO — no array-shape memorisation needed.
$info->lamports;     // int
$info->owner;        // PublicKey
$info->executable;   // bool
$info->data;         // Buffer — base64 already decoded
$info->data?->toArray();

Legacy $connection->getAccountInfo(...) still returns the raw RPC array.

Read multiple accounts at once

$infos = $connection->multipleAccounts(['Acc1...', 'Acc2...', 'Acc3...']);

foreach ($infos as $info) {
    if ($info === null) continue;   // missing slot
    echo $info->owner->toBase58(), ' ', $info->lamports, "\n";
}

List the SPL Token accounts owned by a wallet

$tokens = $connection->getTokenAccountsByOwner(
    $payer->getPublicKey(),
    ['programId' => 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'],
);

Paginate a large program scan

For programs with too many accounts to fetch in a single getProgramAccounts round-trip, partition the scan by account dataSize. The generator emits a ProgramAccount per row across one RPC call per bucket:

$rows = $connection->programAccountsPaged(
    programId: SplTokenProgram::TOKEN_PROGRAM_ID,
    dataSizes: [82, 165],   // mints + token accounts
);

foreach ($rows as $row) {
    echo $row->pubkey->toBase58(), "\n";
}

Scan a program for accounts matching a filter

use SanderMuller\SolanaPhpSdk\DataObjects\GpaFilter;
use SanderMuller\SolanaPhpSdk\Programs\SplTokenProgram;

// All SPL Token accounts holding a specific mint (offset 0..32 = mint pubkey).
$rows = $connection->programAccounts(
    programId: SplTokenProgram::TOKEN_PROGRAM_ID,
    filters: [
        GpaFilter::dataSize(165),                 // SPL Token Account = 165 bytes
        GpaFilter::memcmp(0, $mintPublicKey),     // mint at offset 0
    ],
);

foreach ($rows as $row) {
    echo $row->pubkey->toBase58(), ' ', $row->account->lamports, "\n";
    // $row->account->data is a base64-decoded Buffer
}

GpaFilter::memcmp() accepts a PublicKey, Buffer, or raw base58 string — the most common drift point across Solana SDKs (offset / encoding swap) is locked in the builder.

Look up recent signatures for an address

$txs = $connection->getSignaturesForAddress($payer->getPublicKey(), ['limit' => 25]);
foreach ($txs as $tx) {
    echo $tx['signature'], ' ', $tx['blockTime'], "\n";
}

Watch a signature land (WebSocket)

use SanderMuller\SolanaPhpSdk\Services\SolanaPubSubClient;
use SanderMuller\SolanaPhpSdk\Enum\Network;

$pubsub = new SolanaPubSubClient(network: Network::MAINNET);

$subId = $pubsub->signatureSubscribe(
    $signature,
    ['commitment' => 'finalized'],
);

foreach ($pubsub->listen(maxEvents: 1) as $event) {
    // $event['result']['value']['err'] === null when the tx succeeded
}

$pubsub->unsubscribe($subId);
$pubsub->close();

Survive socket drops

Long-running consumers (indexers, queue workers) can opt in to transparent reconnect — the client respawns the socket, replays every active subscription, and resumes yielding notifications without the caller noticing the gap:

$pubsub->enableAutoReconnect(maxRetries: 0, baseDelayMs: 500);  // 0 = forever

Backoff is exponential with full jitter (baseDelayMs * 2^attempt, randomly shrunk to avoid thundering-herd on a cluster outage). Subscription ids change across the reconnect — the new id ships inside each notification's subscription field, so don't key per-id state on the integer if you enable this.

Add a memo to a transaction

use SanderMuller\SolanaPhpSdk\Programs\MemoProgram;

$tx->addInstructions(
    MemoProgram::build('order-id=42', signers: [$payer->getPublicKey()]),
);

The Memo program writes raw UTF-8 bytes into the transaction log so off-chain indexers can correlate transactions with application data. signers is optional — list any public keys that must be verified as transaction signers (Memo v2 behaviour).

Pay the priority fee the network is currently asking

use SanderMuller\SolanaPhpSdk\Util\PriorityFee;

[$limit, $price] = PriorityFee::buildInstructions(
    connection:        $connection,
    computeUnitLimit:  200_000,
    writableAccounts:  [$payer->getPublicKey()],   // tighter estimate
    percentile:        0.75,                       // beat 75% of recent traffic
);

$tx = new Transaction(feePayer: $payer->getPublicKey());
$tx->addInstructions($limit, $price, /* your real instructions */);

Samples getRecentPrioritizationFees and returns the requested percentile in micro-lamports per compute unit. Returns 0 when the network is idle — no fee is necessary in that regime.

Token-2022 mint extensions

use SanderMuller\SolanaPhpSdk\Programs\Token2022Program;

$program = new Token2022Program();

// 1. Allocate the mint account (system program), then queue extensions
//    BEFORE InitializeMint — Token-2022 ordering rule.
$tx = TransactionBuilder::new()
    ->feePayer($payer->getPublicKey())
    ->recentBlockhash($connection->latestBlockhash())
    ->addInstructions(
        // Allocate + assign owner via SystemProgram::createAccount (omitted)
        $program->createInitializeNonTransferableMintInstruction($mint->getPublicKey()),
        $program->createInitializeMintCloseAuthorityInstruction($mint->getPublicKey(), $payer->getPublicKey()),
        $program->createInitializeTransferFeeConfigInstruction(
            mint: $mint->getPublicKey(),
            transferFeeConfigAuthority: $payer->getPublicKey(),
            withdrawWithheldAuthority: $payer->getPublicKey(),
            transferFeeBasisPoints: 250,            // 2.5%
            maximumFee: 1_000_000,
        ),
        // Finally initialize the mint
        $program->createInitializeMint2Instruction(
            mint: $mint->getPublicKey(),
            decimals: 6,
            mintAuthority: $payer->getPublicKey(),
            freezeAuthority: null,
        ),
    )
    ->addSigner($payer)
    ->addSigner($mint)
    ->build();

Memo-transfer (require a Memo instruction in every incoming transfer):

// Enable both initializes the extension and turns the requirement on.
$tx->addInstructions(
    $program->createMemoTransferToggleInstruction($tokenAccount, $owner->getPublicKey(), enable: true),
);

// Later — turn it off:
$program->createMemoTransferToggleInstruction($tokenAccount, $owner->getPublicKey(), enable: false);

Send a Token-2022 transfer

use SanderMuller\SolanaPhpSdk\Programs\Token2022Program;

$program = new Token2022Program();

$ix = $program->createTransferCheckedInstruction(
    source:      $sourceAta,
    mint:        $mint,
    destination: $destAta,
    owner:       $owner->getPublicKey(),
    amount:      1_000_000,
    decimals:    6,
);

Token-2022 inherits every core discriminator from legacy SPL Token — the program-id is the only difference. The ATA address differs between the two programs even for the same (owner, mint) pair, so derive it through $program->getAssociatedTokenAddressSync() rather than reusing a legacy-Token ATA.

Build a v0 transaction with an Address Lookup Table

use SanderMuller\SolanaPhpSdk\Programs\AddressLookupTableProgram;

// 1. Create the lookup table (one-time, returns a PDA address + bump)
$slot = $connection->getSlot();
$create = AddressLookupTableProgram::createLookupTable($authority, $payer, $slot);

// 2. Extend it with up to 256 addresses (across calls)
$extend = AddressLookupTableProgram::extendLookupTable(
    lookupTable: $create['lookupTableAddress'],
    authority:   $authority,
    payer:       $payer,
    addresses:   [$mint, $programId, /* ... */],
);

Use the resulting lookupTableAddress as an AddressLookupTableAccount when compiling a MessageV0.

Verify an Ed25519 signature

use SanderMuller\SolanaPhpSdk\PublicKey;

$ok = PublicKey::verify($pubkeyBytes, $messageBytes, $signatureBytes);

Call an Anchor program from its IDL

use SanderMuller\SolanaPhpSdk\Anchor\AnchorIdl;

$idl = AnchorIdl::fromFile(__DIR__ . '/my_program.json');

$ix = $idl->instruction('initialize')->build(
    accounts: [
        'state' => $statePubkey,
        'user'  => $payer->getPublicKey(),
        // `systemProgram` etc. are auto-filled from the IDL's fixed `address` field
    ],
    args: [
        'amount' => 1_000_000,
        'label'  => 'hello',
    ],
);

$tx = (new Transaction(feePayer: $payer->getPublicKey()))->addInstructions($ix);
$connection->sendTransaction($tx, signers: [$payer]);

Supported IDL types: every primitive in the Anchor 0.30 spec (u8..u64, i8..i64, f32/f64, bool, string, bytes, pubkey) plus vec<T>, array<T, N>, and option<T>. User-defined struct types raise a clear validation error so you pre-encode them.

Outside Laravel

The SDK doesn't require a Laravel app. Use the bootstrap helper:

require __DIR__ . '/vendor/autoload.php';

use SanderMuller\SolanaPhpSdk\Bootstrap;
use SanderMuller\SolanaPhpSdk\Connection;

$container  = Bootstrap::createContainer(__DIR__ . '/config/solana-php-sdk.php');
$connection = $container->get(Connection::class);

echo $connection->getSlot();

The config file lives at config/solana-php-sdk.php once you publish it (php artisan vendor:publish --tag=solana-php-sdk-config inside Laravel, or copy the package's default) and defines the network and SPL Token program id.

Reference — every typed RPC method on Connection

Click to expand

Cluster / node infogetSlot, getTransactionCount, getFirstAvailableBlock, getGenesisHash, getHealth, getVersion, getIdentity, getClusterNodes, getEpochInfo, getEpochSchedule, getHighestSnapshotSlot, minimumLedgerSlot, getMaxRetransmitSlot, getMaxShredInsertSlot, getSlotLeader, getSlotLeaders.

Blocks / slotsgetBlockHeight, getBlock, getBlocks, getBlocksWithLimit, getBlockTime, getBlockCommitment, getBlockProduction, getLeaderSchedule, isBlockhashValid, getLatestBlockhash, getFeeForMessage, getRecentPrioritizationFees, getRecentPerformanceSamples.

AccountsgetAccountInfo, getBalance, getMultipleAccounts, getProgramAccounts, getMinimumBalanceForRentExemption, getLargestAccounts.

Transactions / signaturesgetTransaction, sendTransaction, sendRawTransaction, sendVersionedTransaction, simulateTransaction, getSignaturesForAddress, getSignatureStatuses, confirmTransaction, requestAirdrop.

TokensgetTokenAccountBalance, getTokenSupply, getTokenAccountsByOwner, getTokenAccountsByDelegate, getTokenLargestAccounts.

Stake / vote / supplygetSupply, getVoteAccounts, getInflationGovernor, getInflationRate, getInflationReward, getStakeMinimumDelegation, getStakeActivation.

Any method not yet wrapped is reachable via SolanaRpcClient::call($method, $params).

Reference — PubSub subscriptions

SolanaPubSubClient covers every documented WebSocket method: accountSubscribe, signatureSubscribe, slotSubscribe, rootSubscribe, logsSubscribe, programSubscribe, voteSubscribe, blockSubscribe. Use the generic subscribe(method, params) for anything else.

The constructor accepts a clientFactory: Closure(): WebSocket\Client so tests can inject a scripted-reply double instead of opening a real socket.

Troubleshooting

Account not found when reading a fresh wallet. Solana doesn't allocate storage until the first deposit. Airdrop / transfer something in first, or catch AccountNotFoundException.

Transaction simulation failed: Blockhash not found. The blockhash you attached has expired (~60–90 s). Call getLatestBlockhash() again and rebuild the transaction.

Transaction signature verification failed. You signed with the wrong keypair, or modified the transaction after signing. Re-sign after every mutation. Confirm feePayer matches your first signer.

SolanaPubSubClient blocks forever in listen(). Pass maxEvents: to stop after N events, or wrap the loop in a deadline and call close().

PublicKey::verify() returning false even with the right key. Solana signatures are over the raw message bytes, not a hash. Pass the exact bytes you signed, with no JSON-encoding or string interpolation in between.

Airdrop ignored. Devnet rate-limits per IP and per recipient. Wait a few minutes or use a faucet website.

Examples

Runnable scripts under examples/:

  • examples/quickstart-transfer.php — airdrop, balance, transfer, confirm.
  • examples/anchor-call.php — load an IDL, build an instruction, simulate.
  • examples/pubsub-watch-signature.php — subscribe and block until a tx finalises.

Each script is standalone — composer install then php examples/quickstart-transfer.php.

Comparison to other Solana clients

Capability this SDK attestto/solana-php-sdk Solnet (.NET) @solana/kit (TS)
Typed RPC method coverage ~90 % basic full full
WebSocket PubSub ✅ full
Anchor IDL runtime builder partial
V0 / ALT transactions
SNS (name service) community community
Static analysis posture PHPStan max + bleeding edge + strict none

Compared to the canonical Solana SDKs across all languages

This SDK is built against the recurring pain points operators hit in @solana/web3.js, @solana/kit, solana-py / solders, solana-go, and solana-sdk / anchor-client. The matrix below is the short version; see the rationale beneath it.

Capability this SDK @solana/kit (TS) @solana/web3.js (TS) solana-py + solders solana-go solana-sdk + anchor-client (Rust)
Pluggable RPC transports (fallback / round-robin / retry) partial
Structured SendTransactionError (decoded InstructionError, logs, units) partial partial
Auto compute-unit limit + priority-fee injection on build
Sanitize-safe transaction builder ✅ (type-level)
KMS / HSM / hardware-wallet signer abstraction community community
Queue-based confirmation + lifecycle events ✅ (Laravel)
In-process test fakes + assertion helpers ✅ (Pest + facade) partial partial partial
Token-2022 extensions partial partial partial partial

What we built against

  • Blockhash expiry is typed, not opaque. sendAndConfirmTransaction tracks lastValidBlockHeight and trips BlockhashExpiredException instead of swallowing the failure or silently re-signing — the #1 cross-SDK footgun in web3.js issues #1987 / #2361.
  • Structured errors instead of raw RPC strings. sendTransaction / simulateTransaction failures raise a SendTransactionError carrying a decoded TransactionError (InstructionError(2, Custom(6000)), BlockhashNotFound, InsufficientFundsForRent { account_index }, …), full program logs, and unitsConsumed.
  • Pluggable RPC transports. Add a Helius primary + Triton backup + public fallback in a config block — the SDK round-robins or fails over on transient errors. Per-endpoint exponential-backoff retry decorator included.
  • Auto compute-budget + priority-fee on build. TransactionBuilder::withAutoComputeBudget() simulates, reads unitsConsumed, scales by a safety buffer, and injects both setComputeUnitLimit + setComputeUnitPrice before signing. No other SDK ships this by default — every other ecosystem leaves it as a Helius / Quicknode blog-post recipe.
  • Laravel-native async story. ConfirmTransactionJob::dispatch($sig, $lvbh) hands the long-tail confirmation phase to a queue worker, then fires TransactionConfirmed / TransactionExpired events. Inherits retries + DLQ from your queue backend — Kit's async-iterator API cannot match this on PHP-FPM.
  • Solana::fake() for tests. One line swaps the RPC client for an in-memory stub. Pest expectations (toBeConfirmed, toHaveCustomCode, toBeInstructionError) ship out of the box. Other SDKs stand up a local validator just to write a unit test.
  • Sanitize-safe builder. Catches duplicate-account flag conflicts, missing signers, and orphaned isSigner flags locally before the RPC round-trip — so you never see "Transaction failed to sanitize accounts offsets correctly", the single most-reported cross-SDK error.
  • PDA + ATA one-liners. Pda::find($programId, $seeds) and Ata::derive($owner, $mint) (with a Token2022Program::TOKEN_PROGRAM_ID argument or the Ata::derive2022() shortcut). Most SDKs hardcode the legacy SPL Token program in the ATA helper.

Roadmap

Tracked separately as GitHub issues; the high-level next steps are:

  • In-process test validator (litesvm-style) — drive transactions against a deterministic SVM execution layer without an RPC round-trip. Out of scope for this release; the Rust litesvm crate is the reference. Track issues tagged harness.
  • Token-2022 advanced extensions — confidential transfer (ZK proofs), transfer hook (CPI to a host program), metadata pointer. Initialize-only versions of transfer-fee, mint-close-authority, permanent-delegate, non-transferable, immutable-owner, memo-transfer already ship.
  • Concrete KMS adapters — AWS KMS, GCP KMS, HashiCorp Vault, Ledger. The MessageSigner interface is stable; hosts implement against their preferred backend (see README sketches above).

Where to ask for help

Development

composer install
composer test           # PHPUnit
composer phpstan        # level max + bleeding edge
composer rector         # apply automated refactors
composer rector-dry-run
composer format         # apply Pint
composer qa             # rector + pint + phpstan

Contributing

Issues and PRs welcome. Please run composer qa before opening a PR.

License

MIT — see LICENSE.