laravel-solana-sdk maintained by sandermuller
Laravel Solana SDK
Laravel wrapper around sandermuller/solana-php-sdk — service
provider, facades, env-driven config, and artisan commands so you can
call Solana RPC from a Laravel app without wiring containers yourself.
use SanderMuller\LaravelSolanaSdk\Facades\Solana;
$balance = Solana::getBalance('SomeWalletAddressBase58'); // lamports as float
$blockhash = Solana::latestBlockhash(); // typed BlockhashInfo
$status = Solana::sendAndConfirmTransaction($tx, [$payer]);
Contents
- Requirements
- Install
- Configuration
- Facades
- Multi-endpoint transport
- Confirming transactions on the queue
- PubSub / WebSocket
- Artisan commands
- Testing
- Upgrading
- Changelog
- Contributing
- Security
- Credits
- License
Requirements
- PHP
^8.3 - Laravel
^11.0 || ^12.0 || ^13.0 ext-sodium(transitively required by the core SDK)
Install
composer require sandermuller/laravel-solana-sdk
The service provider auto-discovers. Publish the config if you want to customise defaults:
php artisan vendor:publish --tag=solana-sdk-config
Configuration
config/solana-sdk.php exposes four knobs:
return [
'network' => env('SOLANA_NETWORK', 'mainnet'),
'token_program_id' => env('SOLANA_TOKEN_PROGRAM_ID', 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'),
'transport' => [
'mode' => env('SOLANA_TRANSPORT_MODE', 'fallback'),
'urls' => array_values(array_filter([
env('SOLANA_RPC_URL'),
env('SOLANA_RPC_URL_FALLBACK'),
])),
'headers' => [],
'timeout' => (float) env('SOLANA_RPC_TIMEOUT', 30.0),
'retry' => [
'max_attempts' => (int) env('SOLANA_RPC_RETRY_ATTEMPTS', 3),
'base_delay_ms' => (int) env('SOLANA_RPC_RETRY_BASE_MS', 100),
'max_delay_ms' => (int) env('SOLANA_RPC_RETRY_MAX_MS', 2_000),
],
],
'commands' => [
'enabled' => env('SOLANA_COMMANDS_ENABLED', true),
],
];
SOLANA_NETWORK accepts mainnet, mainnet-beta, testnet, devnet
(also main, test, dev). The RPC endpoint is derived from the
Network enum in the core SDK.
Facades
use SanderMuller\LaravelSolanaSdk\Facades\Solana;
use SanderMuller\LaravelSolanaSdk\Facades\SolanaRpc;
$balance = Solana::getBalance('SomeWalletAddressBase58');
$info = Solana::accountInfo('SomeWalletAddressBase58'); // typed AccountInfo
$blockhash = Solana::latestBlockhash(); // typed BlockhashInfo
// Send + poll until confirmed in a single call:
$status = Solana::sendAndConfirmTransaction($tx, [$payer]);
// Low-level JSON-RPC escape hatch
$result = SolanaRpc::call('getSlot');
Solana proxies the typed Connection API (~60 RPC methods covering
~80 % of the Solana JSON-RPC spec — accounts, blocks, slots, signatures,
tokens, supply, stake, vote, inflation). SolanaRpc proxies the raw
SolanaRpcClient for calls the typed facade does not yet cover.
Both bindings are also reachable via plain DI:
use SanderMuller\SolanaPhpSdk\Connection;
class WalletController
{
public function show(Connection $solana, string $address): array
{
return ['balance' => $solana->getBalance($address)];
}
}
Programs, builders, signers
The core SDK ships first-class program builders (SystemProgram,
SplTokenProgram, Token2022Program, MemoProgram, StakeProgram,
VoteProgram, AddressLookupTableProgram, ComputeBudgetProgram,
MetaplexProgram, AnchorIdl, …), a sanitize-safe
TransactionBuilder, a Util\PriorityFee helper, and a
Contracts\MessageSigner interface (with Signing\InMemoryMessageSigner
as the local adapter). Use them directly — no wrapping required:
use SanderMuller\SolanaPhpSdk\TransactionBuilder;
use SanderMuller\SolanaPhpSdk\Programs\SystemProgram;
use SanderMuller\LaravelSolanaSdk\Facades\Solana;
$blockhash = Solana::latestBlockhash();
$tx = TransactionBuilder::make()
->feePayer($payer->publicKey)
->recentBlockhash($blockhash)
->addInstruction(SystemProgram::transfer($payer->publicKey, $to, $lamports))
->addSigner($payer)
->build();
$status = Solana::sendAndConfirmTransaction($tx, [$payer]);
Multi-endpoint transport
Set SOLANA_RPC_URL (and optionally SOLANA_RPC_URL_FALLBACK) to route
through your own RPC provider with automatic retry + fallback. Leave
both unset to keep the public-endpoint default.
SOLANA_RPC_URL=https://mainnet.helius-rpc.com/?api-key=…
SOLANA_RPC_URL_FALLBACK=https://api.mainnet-beta.solana.com
SOLANA_TRANSPORT_MODE=fallback # or round_robin
SOLANA_RPC_TIMEOUT=30
SOLANA_RPC_RETRY_ATTEMPTS=3
For more elaborate setups (custom auth headers, multiple fallbacks)
publish the config and edit transport directly — the wrapper hands
the array straight to SanderMuller\SolanaPhpSdk\Rpc\TransportFactory.
Confirming transactions on the queue
The core SDK ships SanderMuller\SolanaPhpSdk\Queue\ConfirmTransactionJob
out of the box. Dispatch it after sendTransaction so the long-tail
confirmation phase becomes a background job that fires
TransactionConfirmed / TransactionExpired events:
use SanderMuller\LaravelSolanaSdk\Facades\Solana;
use SanderMuller\SolanaPhpSdk\Queue\ConfirmTransactionJob;
$blockhash = Solana::latestBlockhash();
$signature = Solana::sendTransaction($tx, [$payer]);
ConfirmTransactionJob::dispatch(
signature: $signature,
lastValidBlockHeight: $blockhash->lastValidBlockHeight,
context: ['order_id' => $order->id],
);
Listen for SanderMuller\SolanaPhpSdk\Events\TransactionConfirmed and
TransactionExpired in EventServiceProvider.
PubSub / WebSocket
SolanaPubSubClient is bound transient against the configured network,
so you can typehint it directly:
use SanderMuller\SolanaPhpSdk\Services\SolanaPubSubClient;
class WatchSignatures
{
public function handle(SolanaPubSubClient $pubsub, string $signature): void
{
$pubsub->enableAutoReconnect();
$pubsub->signatureSubscribe($signature);
foreach ($pubsub->listen() as $event) {
// …
}
}
}
Artisan commands
| Command | Purpose |
|---|---|
solana:balance {address} |
Print SOL balance + lamports |
solana:airdrop {address} {sol=1} |
Request devnet/testnet airdrop |
solana:account {address} |
Dump raw account info JSON |
solana:tx {signature} |
Look up a transaction by signature |
solana:health |
RPC health + version |
solana:tokens {owner} |
List SPL token accounts owned by an address |
solana:fees {addresses?*} |
Recent prioritization fee samples |
Disable all bundled commands with SOLANA_COMMANDS_ENABLED=false (they
hit live RPC; production may want them off).
Testing
composer test
Stubbing RPC in tests
Call Solana::fake() to swap the bound SolanaRpcClient for the core
SDK's InMemoryRpcStub so the SDK never hits the network:
use SanderMuller\LaravelSolanaSdk\Facades\Solana;
it('reads the balance', function (): void {
Solana::fake()->script([
'getBalance' => ['value' => 5_000_000],
]);
expect(Solana::getBalance($address))->toBe(5_000_000.0);
expect(Solana::fakedStub()?->methodCalls())->toContain('getBalance');
});
Wire the core Pest expectations (toBeConfirmed, toHaveCustomCode,
toBeInstructionError) once from tests/Pest.php:
use SanderMuller\SolanaPhpSdk\Testing\PestExpectations;
PestExpectations::register();
The wrapper's own test suite runs against Orchestra Testbench with the package provider auto-registered. Network-dependent tests are not shipped — facade unit tests just verify resolution + container shape.
Upgrading
See UPGRADING.md.
Changelog
See CHANGELOG.md. Updated automatically on release publish.
Contributing
PRs welcome. Run the local gauntlet before opening one:
vendor/bin/pest # tests
vendor/bin/pint --test # style
vendor/bin/phpstan # static analysis
vendor/bin/rector --dry-run
The package is intentionally a thin wrapper — net-new RPC methods,
program builders, and DTOs belong in
sandermuller/solana-php-sdk. The wrapper only adds Laravel
glue (@method lines on the Solana facade, env-driven config keys,
container bindings, solana:* commands). See
CLAUDE.md for the full scope rules.
Security
See SECURITY.md.
Credits
License
The MIT License (MIT). See LICENSE.