{"id":51540528,"url":"https://github.com/sandermuller/solana-php-sdk","last_synced_at":"2026-07-09T13:01:44.637Z","repository":{"id":358548603,"uuid":"919126660","full_name":"SanderMuller/solana-php-sdk","owner":"SanderMuller","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-24T06:26:27.000Z","size":978,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-24T08:22:58.324Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SanderMuller.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-01-19T19:11:59.000Z","updated_at":"2026-06-24T06:26:24.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/SanderMuller/solana-php-sdk","commit_stats":null,"previous_names":["sandermuller/solana-php-sdk"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/SanderMuller/solana-php-sdk","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SanderMuller%2Fsolana-php-sdk","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SanderMuller%2Fsolana-php-sdk/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SanderMuller%2Fsolana-php-sdk/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SanderMuller%2Fsolana-php-sdk/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SanderMuller","download_url":"https://codeload.github.com/SanderMuller/solana-php-sdk/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SanderMuller%2Fsolana-php-sdk/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35299763,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-09T02:00:07.329Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-09T13:01:41.136Z","updated_at":"2026-07-09T13:01:44.623Z","avatar_url":"https://github.com/SanderMuller.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# solana-php-sdk\n\n[![Latest Version on Packagist](https://img.shields.io/packagist/v/sandermuller/solana-php-sdk.svg?style=flat-square)](https://packagist.org/packages/sandermuller/solana-php-sdk)\n[![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/sandermuller/solana-php-sdk/run-tests.yml?branch=main\u0026label=tests\u0026style=flat-square)](https://github.com/sandermuller/solana-php-sdk/actions/workflows/run-tests.yml)\n[![Total Downloads](https://img.shields.io/packagist/dt/sandermuller/solana-php-sdk.svg?style=flat-square)](https://packagist.org/packages/sandermuller/solana-php-sdk)\n[![License](https://img.shields.io/packagist/l/sandermuller/solana-php-sdk.svg?style=flat-square)](LICENSE)\n\nA PHP SDK for the [Solana](https://solana.com) blockchain. Read accounts,\nsign and submit transactions, watch the chain over WebSockets, and call any\nAnchor program — all from PHP.\n\n\u003e **Status:** open source under [MIT](LICENSE). Works on PHP 8.3 / 8.4 with\n\u003e Laravel 11 / 12 / 13.\n\n---\n\n## What you get\n\n| Layer | What it does | Class |\n|---|---|---|\n| Keys \u0026 signatures | Generate keypairs, sign messages, verify Ed25519 signatures | `Keypair`, `PublicKey` |\n| Raw JSON-RPC | One method: `call($method, $params)` — pass anything Solana defines | `Services\\SolanaRpcClient` |\n| Typed JSON-RPC | ~50 typed methods covering ~90 % of the HTTP-RPC spec | `Connection` |\n| WebSocket PubSub | Account / signature / slot / logs / program / vote / block subscriptions | `Services\\SolanaPubSubClient` |\n| Transactions | Legacy + V0 (versioned) transactions, Address Lookup Tables | `Transaction`, `VersionedTransaction`, `MessageV0` |\n| Programs | System, SPL Token, **Token-2022**, ComputeBudget, AddressLookupTable, Stake, Vote, Memo, Metaplex, DID-Sol, SNS | `Programs\\*` |\n| Send \u0026 confirm | Priority-fee estimator + `sendAndConfirmTransaction` helper with blockhash-expiry detection | `Util\\PriorityFee`, `Connection::sendAndConfirmTransaction` |\n| Pluggable RPC transports | Multi-endpoint fallback / round-robin / exponential-backoff retry | `Rpc\\FallbackTransport`, `Rpc\\RoundRobinTransport`, `Rpc\\RetryTransport` |\n| Structured `sendTransaction` errors | Decoded `TransactionError` + `InstructionError` enums, program logs, units consumed | `Exceptions\\SendTransactionError`, `Errors\\TransactionErrorDecoder` |\n| Auto compute-budget + priority fee | Simulate → derive CU limit → inject `setComputeUnitLimit` + `setComputeUnitPrice` on build | `Fees\\AutoComputeBudget`, `Fees\\PriorityFeeStrategy`, `TransactionBuilder::withAutoComputeBudget` |\n| In-memory RPC stub + Pest macros | `InMemoryRpcStub::script([...])` swaps the RPC client; `toBeConfirmed` / `toHaveCustomCode` / `toBeInstructionError` expectations — works in any test runner, Laravel not required | `Testing\\InMemoryRpcStub`, `Testing\\PestExpectations` |\n| Laravel facade + queue job + artisan commands | Lives in the sister wrapper [`sandermuller/laravel-solana-sdk`](https://github.com/SanderMuller/laravel-solana-sdk): `Solana::fake()`, `ConfirmTransactionJob::dispatch($sig, $lvbh)`, env-driven config, 7 artisan commands (balance, account, transaction, …) | (wrapper) |\n| PDA / ATA helpers | One-line `Pda::find` + `Ata::derive` (Token-2022 aware via `Ata::derive2022`) | `Util\\Pda`, `Util\\Ata` |\n| Anchor | Parse any Anchor IDL at runtime → typed `TransactionInstruction` builders | `Anchor\\AnchorIdl` |\n\n## Who this is for\n\n- **PHP devs new to Solana.** Start with the [5-minute quickstart](#5-minute-quickstart) — you'll send a real on-chain transfer.\n- **Laravel apps.** The package auto-registers a service provider; resolve `Connection` straight from the container.\n- **Standalone PHP scripts.** A one-liner bootstrap (`Bootstrap::createContainer`) wires up the same container outside Laravel.\n\n---\n\n## Install\n\n```bash\ncomposer require sandermuller/solana-php-sdk\n```\n\nThe package self-registers `SanderMuller\\SolanaPhpSdk\\ServiceProvider` via\nLaravel's [package discovery](https://laravel.com/docs/packages#package-discovery).\nStandalone PHP doesn't need anything extra — see\n[\"Outside Laravel\"](#outside-laravel) below.\n\nRequirements: PHP 8.3+ with `ext-sodium` enabled (every mainstream PHP distro\nships it). Composer pulls in `paragonie/sodium_compat` as a polyfill backup.\n\n\u003e **Building a Laravel app?** Install the sister wrapper instead — it adds the\n\u003e `Solana` facade, `ConfirmTransactionJob`, env-driven config, and 7 artisan\n\u003e commands (balance, account, transaction, fees, health, …) on top of this SDK:\n\u003e ```bash\n\u003e composer require sandermuller/laravel-solana-sdk\n\u003e ```\n\u003e Repo: \u003chttps://github.com/SanderMuller/laravel-solana-sdk\u003e. You get this SDK\n\u003e as a transitive dep — no need to require both.\n\n---\n\n## 5-minute quickstart\n\nWe'll go from zero to a signed-and-broadcast SOL transfer on devnet.\n\n### 1. Generate (or load) a keypair\n\n```php\nuse SanderMuller\\SolanaPhpSdk\\Keypair;\n\n$payer = Keypair::generate();\necho $payer-\u003egetPublicKey()-\u003etoBase58(), \"\\n\";\n// → e.g.  4uTYf8w...EPnL  (this is your wallet address)\n```\n\nStore the secret bytes somewhere safe if you want to reuse this wallet:\n\n```php\n$secret = $payer-\u003egetSecretKey()-\u003etoArray(); // array\u003cint, int\u003e — 64 bytes\n// Reload with:  Keypair::fromSecretKey($secret);\n```\n\n### 2. Get some devnet SOL\n\n```php\nuse SanderMuller\\SolanaPhpSdk\\Connection;\nuse SanderMuller\\SolanaPhpSdk\\Enum\\Network;\n\n$connection = app(Connection::class);          // Laravel\n// $connection = Bootstrap::createContainer(__DIR__.'/config.php')-\u003eget(Connection::class); // standalone\n\n$signature = $connection-\u003erequestAirdrop([\n    $payer-\u003egetPublicKey()-\u003etoBase58(),\n    1_000_000_000,                              // 1 SOL in lamports\n]);\n$connection-\u003econfirmTransaction($signature);    // blocks until landed\n```\n\n### 3. Check your balance\n\n```php\n$lamports = $connection-\u003egetBalance($payer-\u003egetPublicKey()-\u003etoBase58());\necho $lamports / 1_000_000_000, \" SOL\\n\";       // → 1.0\n```\n\n### 4. Send SOL to someone\n\n```php\nuse SanderMuller\\SolanaPhpSdk\\Keypair;\nuse SanderMuller\\SolanaPhpSdk\\Programs\\SystemProgram;\nuse SanderMuller\\SolanaPhpSdk\\Transaction;\n\n$recipient = Keypair::generate()-\u003egetPublicKey();   // pretend this is a friend\n\n$tx = new Transaction(feePayer: $payer-\u003egetPublicKey());\n$tx-\u003eaddInstructions(\n    SystemProgram::transfer(\n        fromPubkey:  $payer-\u003egetPublicKey(),\n        toPublicKey: $recipient,\n        lamports:    5_000_000,                       // 0.005 SOL\n    ),\n);\n\n$signature = $connection-\u003esendTransaction($tx, signers: [$payer]);\n$connection-\u003econfirmTransaction($signature);\n\necho \"https://explorer.solana.com/tx/{$signature}?cluster=devnet\\n\";\n```\n\nClick the URL — your transaction is on-chain.\n\n#### Sign from a KMS / HSM / hardware wallet\n\nFor hosts that cannot expose secret bytes to PHP (KMS-backed keys,\nLedger devices, remote signing services), implement\n`Contracts\\MessageSigner`:\n\n```php\nuse SanderMuller\\SolanaPhpSdk\\Contracts\\MessageSigner;\nuse SanderMuller\\SolanaPhpSdk\\PublicKey;\n\nfinal class AwsKmsSigner implements MessageSigner\n{\n    public function __construct(\n        private readonly string $kmsKeyId,\n        private readonly PublicKey $publicKey,\n        private readonly KmsClient $kms,\n    ) {}\n\n    public function getPublicKey(): PublicKey { return $this-\u003epublicKey; }\n\n    public function signMessage(string $message): string\n    {\n        // Return 64-byte Ed25519 signature from your KMS / HSM.\n        return $this-\u003ekms-\u003esignEd25519($this-\u003ekmsKeyId, $message);\n    }\n}\n\n$tx = TransactionBuilder::new()\n    -\u003efeePayer($publicKey)\n    -\u003erecentBlockhash($connection-\u003elatestBlockhash())\n    -\u003eaddInstruction($instruction)\n    -\u003eaddMessageSigner(new AwsKmsSigner(/* … */))\n    -\u003ebuild();\n```\n\nFor the in-process case, wrap a `Keypair` in\n`Signing\\InMemoryMessageSigner` to get the same interface.\n\n##### Sketch: HashiCorp Vault Transit\n\n```php\nfinal class VaultTransitSigner implements MessageSigner\n{\n    public function __construct(\n        private readonly string $keyName,\n        private readonly PublicKey $publicKey,\n        private readonly VaultClient $vault,\n    ) {}\n\n    public function getPublicKey(): PublicKey { return $this-\u003epublicKey; }\n\n    public function signMessage(string $message): string\n    {\n        $reply = $this-\u003evault-\u003ewrite(\n            \"transit/sign/{$this-\u003ekeyName}/ed25519\",\n            ['input' =\u003e base64_encode($message), 'hash_algorithm' =\u003e 'none'],\n        );\n\n        // Vault returns \"vault:v1:\u003cbase64-signature\u003e\"; strip prefix + decode.\n        return base64_decode(explode(':', $reply['data']['signature'])[2]);\n    }\n}\n```\n\n##### Sketch: Ledger hardware wallet\n\nThe Ledger Solana app expects a base58-derivation-path-prefixed APDU.\nWrap any USB/HID transport (e.g. via a sidecar process bridged over\nstdio) and implement `signMessage()` as a single `INS_SIGN_MESSAGE`\nAPDU round-trip.\n\n#### Sanitize-safe builder\n\nMost transaction failures across Solana SDKs hit the same opaque error:\n`\"Transaction failed to sanitize accounts offsets correctly\"`. The fluent\n`TransactionBuilder` catches the causes locally so you never see that\nmessage:\n\n```php\nuse SanderMuller\\SolanaPhpSdk\\TransactionBuilder;\nuse SanderMuller\\SolanaPhpSdk\\Programs\\SystemProgram;\n\n$tx = TransactionBuilder::new()\n    -\u003efeePayer($payer-\u003egetPublicKey())\n    -\u003erecentBlockhash($connection-\u003elatestBlockhash())   // BlockhashInfo or string\n    -\u003eaddInstruction(SystemProgram::transfer($payer-\u003egetPublicKey(), $to, 1))\n    -\u003eaddSigner($payer)\n    -\u003ebuild();\n```\n\n`build()` throws `UnsanitizedTransactionException` if the fee payer is\nmissing, the blockhash is missing, an instruction marks an account\n`isSigner` with no matching keypair, or two instructions disagree on\nwhether the same account is a signer. Every detected reason is surfaced\nat once on `$exception-\u003ereasons`.\n\n#### One-shot send-and-confirm\n\nMost apps want both steps in one call, with blockhash-expiry built in:\n\n```php\n$status = $connection-\u003esendAndConfirmTransaction(\n    transaction: $tx,\n    signers:     [$payer],\n);\n// $status-\u003econfirmationStatus === 'confirmed'\n```\n\nThe helper fetches `getLatestBlockhash`, signs, sends, and polls\n`getSignatureStatuses` until the commitment is reached or the blockhash\nexpires — no manual retry loop.\n\n---\n\n## Core concepts (the 90-second glossary)\n\n| Term | Plain English |\n|---|---|\n| **Lamport** | Smallest unit of SOL. 1 SOL = 1 000 000 000 (10⁹) lamports. |\n| **Pubkey / address** | 32 bytes, base58-encoded. Identifies a wallet, token mint, program, anything. |\n| **Keypair** | Pubkey + 64-byte Ed25519 secret. Sign things with the secret. Never log it. |\n| **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. |\n| **Instruction** | One call to a program: program id + ordered accounts + opaque data bytes. |\n| **Transaction** | One or more instructions, signed by one or more keypairs, submitted atomically. |\n| **Commitment** | How sure you are the network agrees: `processed` (fastest, can roll back) → `confirmed` → `finalized` (irreversible). |\n| **RPC** | The HTTP JSON-RPC endpoint at `https://api.mainnet-beta.solana.com` (or devnet/testnet). Call `getBalance`, `sendTransaction`, etc. |\n| **PubSub** | The matching WebSocket endpoint at `wss://api.mainnet-beta.solana.com`. Subscribe and get pushed events. |\n| **Program** | Solana's word for a smart contract. Has a fixed pubkey. |\n| **SPL Token** | The token standard. Every fungible / NFT token is an account owned by the SPL Token program. |\n| **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. |\n\n---\n\n## Common recipes\n\n### Read an account\n\n```php\n$info = $connection-\u003eaccountInfo('11111111111111111111111111111111');\n\n// Typed DTO — no array-shape memorisation needed.\n$info-\u003elamports;     // int\n$info-\u003eowner;        // PublicKey\n$info-\u003eexecutable;   // bool\n$info-\u003edata;         // Buffer — base64 already decoded\n$info-\u003edata?-\u003etoArray();\n```\n\nLegacy `$connection-\u003egetAccountInfo(...)` still returns the raw RPC array.\n\n### Read multiple accounts at once\n\n```php\n$infos = $connection-\u003emultipleAccounts(['Acc1...', 'Acc2...', 'Acc3...']);\n\nforeach ($infos as $info) {\n    if ($info === null) continue;   // missing slot\n    echo $info-\u003eowner-\u003etoBase58(), ' ', $info-\u003elamports, \"\\n\";\n}\n```\n\n### List the SPL Token accounts owned by a wallet\n\n```php\n$tokens = $connection-\u003egetTokenAccountsByOwner(\n    $payer-\u003egetPublicKey(),\n    ['programId' =\u003e 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'],\n);\n```\n\n### Paginate a large program scan\n\nFor programs with too many accounts to fetch in a single\n`getProgramAccounts` round-trip, partition the scan by account\n`dataSize`. The generator emits a `ProgramAccount` per row across one\nRPC call per bucket:\n\n```php\n$rows = $connection-\u003eprogramAccountsPaged(\n    programId: SplTokenProgram::TOKEN_PROGRAM_ID,\n    dataSizes: [82, 165],   // mints + token accounts\n);\n\nforeach ($rows as $row) {\n    echo $row-\u003epubkey-\u003etoBase58(), \"\\n\";\n}\n```\n\n### Scan a program for accounts matching a filter\n\n```php\nuse SanderMuller\\SolanaPhpSdk\\DataObjects\\GpaFilter;\nuse SanderMuller\\SolanaPhpSdk\\Programs\\SplTokenProgram;\n\n// All SPL Token accounts holding a specific mint (offset 0..32 = mint pubkey).\n$rows = $connection-\u003eprogramAccounts(\n    programId: SplTokenProgram::TOKEN_PROGRAM_ID,\n    filters: [\n        GpaFilter::dataSize(165),                 // SPL Token Account = 165 bytes\n        GpaFilter::memcmp(0, $mintPublicKey),     // mint at offset 0\n    ],\n);\n\nforeach ($rows as $row) {\n    echo $row-\u003epubkey-\u003etoBase58(), ' ', $row-\u003eaccount-\u003elamports, \"\\n\";\n    // $row-\u003eaccount-\u003edata is a base64-decoded Buffer\n}\n```\n\n`GpaFilter::memcmp()` accepts a `PublicKey`, `Buffer`, or raw base58\nstring — the most common drift point across Solana SDKs (offset / encoding\nswap) is locked in the builder.\n\n### Look up recent signatures for an address\n\n```php\n$txs = $connection-\u003egetSignaturesForAddress($payer-\u003egetPublicKey(), ['limit' =\u003e 25]);\nforeach ($txs as $tx) {\n    echo $tx['signature'], ' ', $tx['blockTime'], \"\\n\";\n}\n```\n\n### Watch a signature land (WebSocket)\n\n```php\nuse SanderMuller\\SolanaPhpSdk\\Services\\SolanaPubSubClient;\nuse SanderMuller\\SolanaPhpSdk\\Enum\\Network;\n\n$pubsub = new SolanaPubSubClient(network: Network::MAINNET);\n\n$subId = $pubsub-\u003esignatureSubscribe(\n    $signature,\n    ['commitment' =\u003e 'finalized'],\n);\n\nforeach ($pubsub-\u003elisten(maxEvents: 1) as $event) {\n    // $event['result']['value']['err'] === null when the tx succeeded\n}\n\n$pubsub-\u003eunsubscribe($subId);\n$pubsub-\u003eclose();\n```\n\n#### Survive socket drops\n\nLong-running consumers (indexers, queue workers) can opt in to\ntransparent reconnect — the client respawns the socket, replays every\nactive subscription, and resumes yielding notifications without the\ncaller noticing the gap:\n\n```php\n$pubsub-\u003eenableAutoReconnect(maxRetries: 0, baseDelayMs: 500);  // 0 = forever\n```\n\nBackoff is exponential with full jitter (`baseDelayMs * 2^attempt`,\nrandomly shrunk to avoid thundering-herd on a cluster outage).\nSubscription ids change across the reconnect — the new id ships inside\neach notification's `subscription` field, so don't key per-id state on\nthe integer if you enable this.\n\n### Add a memo to a transaction\n\n```php\nuse SanderMuller\\SolanaPhpSdk\\Programs\\MemoProgram;\n\n$tx-\u003eaddInstructions(\n    MemoProgram::build('order-id=42', signers: [$payer-\u003egetPublicKey()]),\n);\n```\n\nThe Memo program writes raw UTF-8 bytes into the transaction log so\noff-chain indexers can correlate transactions with application data.\n`signers` is optional — list any public keys that must be verified as\ntransaction signers (Memo v2 behaviour).\n\n### Pay the priority fee the network is currently asking\n\n```php\nuse SanderMuller\\SolanaPhpSdk\\Util\\PriorityFee;\n\n[$limit, $price] = PriorityFee::buildInstructions(\n    connection:        $connection,\n    computeUnitLimit:  200_000,\n    writableAccounts:  [$payer-\u003egetPublicKey()],   // tighter estimate\n    percentile:        0.75,                       // beat 75% of recent traffic\n);\n\n$tx = new Transaction(feePayer: $payer-\u003egetPublicKey());\n$tx-\u003eaddInstructions($limit, $price, /* your real instructions */);\n```\n\nSamples `getRecentPrioritizationFees` and returns the requested\npercentile in micro-lamports per compute unit. Returns `0` when the\nnetwork is idle — no fee is necessary in that regime.\n\n### Token-2022 mint extensions\n\n```php\nuse SanderMuller\\SolanaPhpSdk\\Programs\\Token2022Program;\n\n$program = new Token2022Program();\n\n// 1. Allocate the mint account (system program), then queue extensions\n//    BEFORE InitializeMint — Token-2022 ordering rule.\n$tx = TransactionBuilder::new()\n    -\u003efeePayer($payer-\u003egetPublicKey())\n    -\u003erecentBlockhash($connection-\u003elatestBlockhash())\n    -\u003eaddInstructions(\n        // Allocate + assign owner via SystemProgram::createAccount (omitted)\n        $program-\u003ecreateInitializeNonTransferableMintInstruction($mint-\u003egetPublicKey()),\n        $program-\u003ecreateInitializeMintCloseAuthorityInstruction($mint-\u003egetPublicKey(), $payer-\u003egetPublicKey()),\n        $program-\u003ecreateInitializeTransferFeeConfigInstruction(\n            mint: $mint-\u003egetPublicKey(),\n            transferFeeConfigAuthority: $payer-\u003egetPublicKey(),\n            withdrawWithheldAuthority: $payer-\u003egetPublicKey(),\n            transferFeeBasisPoints: 250,            // 2.5%\n            maximumFee: 1_000_000,\n        ),\n        // Finally initialize the mint\n        $program-\u003ecreateInitializeMint2Instruction(\n            mint: $mint-\u003egetPublicKey(),\n            decimals: 6,\n            mintAuthority: $payer-\u003egetPublicKey(),\n            freezeAuthority: null,\n        ),\n    )\n    -\u003eaddSigner($payer)\n    -\u003eaddSigner($mint)\n    -\u003ebuild();\n```\n\nMemo-transfer (require a Memo instruction in every incoming transfer):\n\n```php\n// Enable both initializes the extension and turns the requirement on.\n$tx-\u003eaddInstructions(\n    $program-\u003ecreateMemoTransferToggleInstruction($tokenAccount, $owner-\u003egetPublicKey(), enable: true),\n);\n\n// Later — turn it off:\n$program-\u003ecreateMemoTransferToggleInstruction($tokenAccount, $owner-\u003egetPublicKey(), enable: false);\n```\n\n### Send a Token-2022 transfer\n\n```php\nuse SanderMuller\\SolanaPhpSdk\\Programs\\Token2022Program;\n\n$program = new Token2022Program();\n\n$ix = $program-\u003ecreateTransferCheckedInstruction(\n    source:      $sourceAta,\n    mint:        $mint,\n    destination: $destAta,\n    owner:       $owner-\u003egetPublicKey(),\n    amount:      1_000_000,\n    decimals:    6,\n);\n```\n\nToken-2022 inherits every core discriminator from legacy SPL Token — the\nprogram-id is the only difference. The ATA address differs between the\ntwo programs even for the same `(owner, mint)` pair, so derive it\nthrough `$program-\u003egetAssociatedTokenAddressSync()` rather than reusing\na legacy-Token ATA.\n\n### Build a v0 transaction with an Address Lookup Table\n\n```php\nuse SanderMuller\\SolanaPhpSdk\\Programs\\AddressLookupTableProgram;\n\n// 1. Create the lookup table (one-time, returns a PDA address + bump)\n$slot = $connection-\u003egetSlot();\n$create = AddressLookupTableProgram::createLookupTable($authority, $payer, $slot);\n\n// 2. Extend it with up to 256 addresses (across calls)\n$extend = AddressLookupTableProgram::extendLookupTable(\n    lookupTable: $create['lookupTableAddress'],\n    authority:   $authority,\n    payer:       $payer,\n    addresses:   [$mint, $programId, /* ... */],\n);\n```\n\nUse the resulting `lookupTableAddress` as an `AddressLookupTableAccount`\nwhen compiling a `MessageV0`.\n\n### Verify an Ed25519 signature\n\n```php\nuse SanderMuller\\SolanaPhpSdk\\PublicKey;\n\n$ok = PublicKey::verify($pubkeyBytes, $messageBytes, $signatureBytes);\n```\n\n### Call an Anchor program from its IDL\n\n```php\nuse SanderMuller\\SolanaPhpSdk\\Anchor\\AnchorIdl;\n\n$idl = AnchorIdl::fromFile(__DIR__ . '/my_program.json');\n\n$ix = $idl-\u003einstruction('initialize')-\u003ebuild(\n    accounts: [\n        'state' =\u003e $statePubkey,\n        'user'  =\u003e $payer-\u003egetPublicKey(),\n        // `systemProgram` etc. are auto-filled from the IDL's fixed `address` field\n    ],\n    args: [\n        'amount' =\u003e 1_000_000,\n        'label'  =\u003e 'hello',\n    ],\n);\n\n$tx = (new Transaction(feePayer: $payer-\u003egetPublicKey()))-\u003eaddInstructions($ix);\n$connection-\u003esendTransaction($tx, signers: [$payer]);\n```\n\nSupported IDL types: every primitive in the Anchor 0.30 spec (`u8..u64`,\n`i8..i64`, `f32`/`f64`, `bool`, `string`, `bytes`, `pubkey`) plus `vec\u003cT\u003e`,\n`array\u003cT, N\u003e`, and `option\u003cT\u003e`. User-defined struct types raise a clear\nvalidation error so you pre-encode them.\n\n---\n\n## Using with Laravel\n\nThis SDK works inside a Laravel app out of the box via auto-discovery —\n`Connection` resolves straight from the container. For the full\nLaravel-flavoured experience (facade, queue job, env-driven config, artisan\ncommands), install the wrapper instead:\n\n```bash\ncomposer require sandermuller/laravel-solana-sdk\n```\n\n| What the wrapper adds | Why it's separate from this SDK |\n|---|---|\n| `Solana` facade with 62 typed `@method` declarations | Laravel-only contract; framework-agnostic SDK shouldn't ship Facade subclasses |\n| `ConfirmTransactionJob` (Queueable, Dispatchable) | Laravel Queue contracts; events (`TransactionConfirmed` / `TransactionExpired`) stay here in the SDK |\n| Env-driven config + `php artisan vendor:publish` | Laravel config publishing |\n| 7 artisan commands: `solana:balance`, `solana:account`, `solana:transaction`, `solana:fees`, `solana:health`, `solana:airdrop`, `solana:tokens` | Laravel Console contracts |\n| Pre-wired `Solana::fake()` for tests | One-liner over this SDK's `InMemoryRpcStub` |\n\nRepo: \u003chttps://github.com/SanderMuller/laravel-solana-sdk\u003e. The wrapper requires\nthis SDK as a transitive dep — install just the wrapper, not both.\n\n---\n\n## Outside Laravel\n\nThe SDK doesn't require a Laravel app. Use the bootstrap helper:\n\n```php\nrequire __DIR__ . '/vendor/autoload.php';\n\nuse SanderMuller\\SolanaPhpSdk\\Bootstrap;\nuse SanderMuller\\SolanaPhpSdk\\Connection;\n\n$container  = Bootstrap::createContainer(__DIR__ . '/config/solana-php-sdk.php');\n$connection = $container-\u003eget(Connection::class);\n\necho $connection-\u003egetSlot();\n```\n\nThe config file lives at `config/solana-php-sdk.php` once you publish it\n(`php artisan vendor:publish --tag=solana-php-sdk-config` inside Laravel, or\ncopy the package's default) and defines the network and SPL Token program\nid.\n\n---\n\n## Reference — every typed RPC method on `Connection`\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eClick to expand\u003c/strong\u003e\u003c/summary\u003e\n\n**Cluster / node info** — `getSlot`, `getTransactionCount`,\n`getFirstAvailableBlock`, `getGenesisHash`, `getHealth`, `getVersion`,\n`getIdentity`, `getClusterNodes`, `getEpochInfo`, `getEpochSchedule`,\n`getHighestSnapshotSlot`, `minimumLedgerSlot`, `getMaxRetransmitSlot`,\n`getMaxShredInsertSlot`, `getSlotLeader`, `getSlotLeaders`.\n\n**Blocks / slots** — `getBlockHeight`, `getBlock`, `getBlocks`,\n`getBlocksWithLimit`, `getBlockTime`, `getBlockCommitment`,\n`getBlockProduction`, `getLeaderSchedule`, `isBlockhashValid`,\n`getLatestBlockhash`, `getFeeForMessage`, `getRecentPrioritizationFees`,\n`getRecentPerformanceSamples`.\n\n**Accounts** — `getAccountInfo`, `getBalance`, `getMultipleAccounts`,\n`getProgramAccounts`, `getMinimumBalanceForRentExemption`,\n`getLargestAccounts`.\n\n**Transactions / signatures** — `getTransaction`, `sendTransaction`,\n`sendRawTransaction`, `sendVersionedTransaction`, `simulateTransaction`,\n`getSignaturesForAddress`, `getSignatureStatuses`, `confirmTransaction`,\n`requestAirdrop`.\n\n**Tokens** — `getTokenAccountBalance`, `getTokenSupply`,\n`getTokenAccountsByOwner`, `getTokenAccountsByDelegate`,\n`getTokenLargestAccounts`.\n\n**Stake / vote / supply** — `getSupply`, `getVoteAccounts`,\n`getInflationGovernor`, `getInflationRate`, `getInflationReward`,\n`getStakeMinimumDelegation`, `getStakeActivation`.\n\nAny method not yet wrapped is reachable via\n`SolanaRpcClient::call($method, $params)`.\n\n\u003c/details\u003e\n\n## Reference — PubSub subscriptions\n\n`SolanaPubSubClient` covers every documented WebSocket method:\n`accountSubscribe`, `signatureSubscribe`, `slotSubscribe`, `rootSubscribe`,\n`logsSubscribe`, `programSubscribe`, `voteSubscribe`, `blockSubscribe`. Use\nthe generic `subscribe(method, params)` for anything else.\n\nThe constructor accepts a `clientFactory: Closure(): WebSocket\\Client` so\ntests can inject a scripted-reply double instead of opening a real socket.\n\n---\n\n## Troubleshooting\n\n**`Account not found` when reading a fresh wallet.** Solana doesn't allocate\nstorage until the first deposit. Airdrop / transfer something in first, or\ncatch `AccountNotFoundException`.\n\n**`Transaction simulation failed: Blockhash not found`.** The blockhash you\nattached has expired (~60–90 s). Call `getLatestBlockhash()` again and rebuild\nthe transaction.\n\n**`Transaction signature verification failed`.** You signed with the wrong\nkeypair, or modified the transaction after signing. Re-sign after every\nmutation. Confirm `feePayer` matches your first signer.\n\n**`SolanaPubSubClient` blocks forever in `listen()`.** Pass `maxEvents:` to\nstop after N events, or wrap the loop in a deadline and call `close()`.\n\n**`PublicKey::verify()` returning false even with the right key.** Solana\nsignatures are over the *raw message bytes*, not a hash. Pass the exact bytes\nyou signed, with no JSON-encoding or string interpolation in between.\n\n**Airdrop ignored.** Devnet rate-limits per IP and per recipient. Wait a few\nminutes or use a faucet website.\n\n---\n\n## Examples\n\nRunnable scripts under `examples/`:\n\n- `examples/quickstart-transfer.php` — airdrop, balance, transfer, confirm.\n- `examples/anchor-call.php` — load an IDL, build an instruction, simulate.\n- `examples/pubsub-watch-signature.php` — subscribe and block until a tx finalises.\n\nEach script is standalone — `composer install` then `php examples/quickstart-transfer.php`.\n\n---\n\n## Comparison to other Solana clients\n\n| Capability | this SDK | `attestto/solana-php-sdk` | Solnet (.NET) | `@solana/kit` (TS) |\n|---|---|---|---|---|\n| Typed RPC method coverage | ~90 % | basic | full | full |\n| WebSocket PubSub | ✅ full | ❌ | ✅ | ✅ |\n| Anchor IDL runtime builder | ✅ | ❌ | partial | ✅ |\n| V0 / ALT transactions | ✅ | ❌ | ✅ | ✅ |\n| SNS (name service) | ✅ | ❌ | community | community |\n| Static analysis posture | PHPStan max + bleeding edge + strict | none | — | — |\n\n### Compared to the canonical Solana SDKs across all languages\n\nThis SDK is built against the recurring pain points operators hit in\n`@solana/web3.js`, `@solana/kit`, `solana-py` / `solders`, `solana-go`,\nand `solana-sdk` / `anchor-client`. The matrix below is the short\nversion; see the rationale beneath it.\n\n| Capability | this SDK | `@solana/kit` (TS) | `@solana/web3.js` (TS) | `solana-py` + `solders` | `solana-go` | `solana-sdk` + `anchor-client` (Rust) |\n|---|---|---|---|---|---|---|\n| Pluggable RPC transports (fallback / round-robin / retry) | ✅ | ✅ | ❌ | ❌ | ❌ | partial |\n| Structured `SendTransactionError` (decoded `InstructionError`, logs, units) | ✅ | ✅ | partial | ❌ | partial | ✅ |\n| Auto compute-unit limit + priority-fee injection on build | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |\n| Sanitize-safe transaction builder | ✅ | ✅ (type-level) | ❌ | ❌ | ❌ | ✅ |\n| KMS / HSM / hardware-wallet signer abstraction | ✅ | ✅ | community | ❌ | community | ✅ |\n| Queue-based confirmation + lifecycle events | ✅ ([via wrapper](https://github.com/SanderMuller/laravel-solana-sdk)) | ❌ | ❌ | ❌ | ❌ | ❌ |\n| In-process test fakes + assertion helpers | ✅ (Pest macros in SDK; `Solana::fake()` in wrapper) | partial | ❌ | partial | ❌ | partial |\n| Token-2022 extensions | ✅ | partial | partial | partial | partial | ✅ |\n\n#### What we built against\n\n- **Blockhash expiry is typed, not opaque.** `sendAndConfirmTransaction`\n  tracks `lastValidBlockHeight` and trips `BlockhashExpiredException`\n  instead of swallowing the failure or silently re-signing — the #1\n  cross-SDK footgun in `web3.js` issues #1987 / #2361.\n- **Structured errors instead of raw RPC strings.** `sendTransaction` /\n  `simulateTransaction` failures raise a `SendTransactionError`\n  carrying a decoded `TransactionError`\n  (`InstructionError(2, Custom(6000))`, `BlockhashNotFound`,\n  `InsufficientFundsForRent { account_index }`, …), full program logs,\n  and `unitsConsumed`.\n- **Pluggable RPC transports.** Add a Helius primary + Triton backup +\n  public fallback in a config block — the SDK round-robins or fails\n  over on transient errors. Per-endpoint exponential-backoff retry\n  decorator included.\n- **Auto compute-budget + priority-fee on build.**\n  `TransactionBuilder::withAutoComputeBudget()` simulates, reads\n  `unitsConsumed`, scales by a safety buffer, and injects both\n  `setComputeUnitLimit` + `setComputeUnitPrice` before signing. No\n  other SDK ships this by default — every other ecosystem leaves it\n  as a Helius / Quicknode blog-post recipe.\n- **Laravel-native async story** (in [`sandermuller/laravel-solana-sdk`](https://github.com/SanderMuller/laravel-solana-sdk)).\n  `ConfirmTransactionJob::dispatch($sig, $lvbh)` hands the long-tail\n  confirmation phase to a queue worker, then fires `TransactionConfirmed` /\n  `TransactionExpired` events (defined here in the SDK). Inherits retries +\n  DLQ from your queue backend — Kit's async-iterator API cannot match this\n  on PHP-FPM.\n- **`InMemoryRpcStub` + Pest expectations.** `InMemoryRpcStub::script([...])`\n  feeds a deterministic RPC reply queue. Pest macros\n  (`toBeConfirmed`, `toHaveCustomCode`, `toBeInstructionError`) ship in this\n  SDK — usable from any test runner. The wrapper adds `Solana::fake()` as\n  a Laravel-flavoured one-liner over the same stub.\n  Other SDKs stand up a local validator just to write a unit test.\n- **Sanitize-safe builder.** Catches duplicate-account flag conflicts,\n  missing signers, and orphaned `isSigner` flags locally before the\n  RPC round-trip — so you never see \"Transaction failed to sanitize\n  accounts offsets correctly\", the single most-reported cross-SDK\n  error.\n- **PDA + ATA one-liners.** `Pda::find($programId, $seeds)` and\n  `Ata::derive($owner, $mint)` (with a `Token2022Program::TOKEN_PROGRAM_ID`\n  argument or the `Ata::derive2022()` shortcut). Most SDKs hardcode\n  the legacy SPL Token program in the ATA helper.\n\n---\n\n## Roadmap\n\nTracked separately as GitHub issues; the high-level next steps are:\n\n- **In-process test validator** (litesvm-style) — drive transactions\n  against a deterministic SVM execution layer without an RPC round-trip.\n  Out of scope for this release; the Rust `litesvm` crate is the\n  reference. Track issues tagged `harness`.\n- **Token-2022 advanced extensions** — confidential transfer (ZK\n  proofs), transfer hook (CPI to a host program), metadata pointer.\n  Initialize-only versions of transfer-fee, mint-close-authority,\n  permanent-delegate, non-transferable, immutable-owner, memo-transfer\n  already ship.\n- **Concrete KMS adapters** — AWS KMS, GCP KMS, HashiCorp Vault, Ledger.\n  The `MessageSigner` interface is stable; hosts implement against their\n  preferred backend (see README sketches above).\n\n## Where to ask for help\n\n- **Solana JSON-RPC reference:** https://solana.com/docs/rpc\n- **Anchor / IDL docs:** https://www.anchor-lang.com/docs/basics/idl\n- **Faucet (devnet SOL):** https://faucet.solana.com\n- **Explorer:** https://explorer.solana.com\n- **Issues / PRs:** GitHub\n\n---\n\n## Development\n\n```bash\ncomposer install\ncomposer test           # PHPUnit\ncomposer phpstan        # level max + bleeding edge\ncomposer rector         # apply automated refactors\ncomposer rector-dry-run\ncomposer format         # apply Pint\ncomposer qa             # rector + pint + phpstan\n```\n\n## Contributing\n\nIssues and PRs welcome. Please run `composer qa` before opening a PR.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsandermuller%2Fsolana-php-sdk","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsandermuller%2Fsolana-php-sdk","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsandermuller%2Fsolana-php-sdk/lists"}