it-healer/laravel-bitcoin
Laravel Bitcoin by it-healer provides a framework-native way to bring Bitcoin functionality into Laravel applications. It offers simple configuration and integration points for PHP developers building Bitcoin-related features, making it a practical starting point for Laravel projects.
What it is. A thin Laravel wrapper around Bitcoin Core's JSON-RPC interface, plus wallet helpers (address generation, balance, sends, transaction lookup) and BIP39 mnemonic support. If you're building anything that needs to talk to a node — payments, watch-only addresses, confirmation monitoring — this saves you from hand-rolling JSON-RPC.
Install and wire up.
composer require it-healer/laravel-bitcoin
php artisan vendor:publish --tag=bitcoin-config # publishes config/bitcoin.php
php artisan config:clear
The service provider is auto-discovered; no manual registration needed. Point the config at your node:
BITCOIN_RPC_HOST=127.0.0.1
BITCOIN_RPC_PORT=8332
BITCOIN_RPC_USER=rpcuser
BITCOIN_RPC_PASSWORD=secret
BITCOIN_NETWORK=mainnet # testnet | regtest in dev
First thing to check. Your node must be running with server=1 in bitcoin.conf and an RPC user/password set. Before touching wallet code, prove connectivity with a trivial call and log the result:
use ItHealer\LaravelBitcoin\Facades\Bitcoin; // verify exact namespace against README
$info = Bitcoin::getBlockchainInfo(); // method names: confirm against README
logger()->info('node ok', ['chain' => $info['chain'], 'blocks' => $info['blocks']]);
Where to look first.
config/bitcoin.php — network selection, RPC credentials, default wallet name, timeouts.vendor/it-healer/laravel-bitcoin/tests — the canonical example set for real call shapes and return formats.First real use case. "Generate a deposit address for a user and check its balance."
$address = Bitcoin::getNewAddress($label = "user:{$user->id}");
$user->forceFill(['btc_deposit_address' => $address])->save();
$balance = Bitcoin::getReceivedByAddress($address); // in BTC — verify unit
Dev workflow. Run Bitcoin Core in regtest locally (bitcoind -regtest -daemon), generate blocks to fund yourself, and exercise sends/confirmations cheaply. Never develop against mainnet.
Wrap the package in one domain service. Don't scatter facade calls across controllers, jobs, and models. A single BitcoinService (or WalletService) gives you one seam to mock in tests and one place to change if the underlying API shifts — which it did in v2.0.3.
final class WalletService
{
public function __construct(private BitcoinRpcContract $rpc) {}
public function depositAddress(User $user): string
{
return $this->rpc->getNewAddress("user:{$user->id}");
}
public function confirmedBalance(string $address, int $minConf = 3): string
{
// Prefer listunspent filtered by confirmations over a raw balance
return $this->rpc->getReceivedByAddress($address, $minConf);
}
}
Bind an interface in a service provider so tests can swap in a fake. This also future-proofs you against a facade rename.
Treat confirmation count as the source of truth. A send is not "done" when the RPC returns a txid; it's done at N confirmations. Persist txid, status, confirmations, last_checked_at, and reconcile from a scheduled job:
// app/Console/Kernel or a scheduled command
$pending->each(function (Payment $p) {
$tx = Bitcoin::getTransaction($p->txid, $includeWatchOnly = true);
$p->update([
'confirmations' => $tx['confirmations'] ?? 0,
'status' => ($tx['confirmations'] ?? 0) >= 3 ? 'confirmed' : 'pending',
'last_checked_at' => now(),
]);
});
Poll on a schedule (e.g. every minute), not per-request. Keep the poll bounded and idempotent.
Idempotency for sends. Sending funds is the one operation you cannot undo. Gate it:
status = 'sending') before calling the RPC, then 'sent' after. If the process dies mid-call you need to know which side you're on.sendToAddress with an explicit subtractfeefromamount=false and a fixed fee/conf target, so retries don't double-spend.Handle amounts as integers (satoshis) at your boundaries. RPC returns and accepts BTC as decimal floats. Convert once:
$sats = (int) round((float) $btcAmount * 1e8); // display only
$btc = number_format($sats / 1e8, 8, '.', ''); // send as string
Never do arithmetic on BTC floats.
Multi-wallet / named-wallet setups. Bitcoin Core supports named wallets. If you run more than one, the wallet name is part of the RPC path. Store the wallet name in config, verify it's loaded (listwallets) at boot, and fail fast if not — a missing wallet produces confusing "wallet not found" errors at the worst moment.
BIP39 onboarding. For generating user wallets from a mnemonic, wrap the cryptographic calls in a dedicated service, normalize input (collapse whitespace, lowercase, trim) before validating, and never log the phrase:
$phrase = trim(preg_replace('/\s+/u', ' ', mb_strtolower($rawInput)));
if (! $bip39->validateMnemonic($phrase)) {
throw new InvalidMnemonicException(); // static message, no phrase interpolated
}
Store the mnemonic encrypted ('encrypted' cast or Crypt::encryptString). Persist the derivation path, not derived private keys — re-derive deterministically when needed.
Testing. Fake the RPC layer for feature tests; run against regtest for integration tests. Assert on fixed vectors (known txids, known addresses) rather than "a string was returned," so a dependency swap like v2.0.3 is caught the moment your suite runs.
Queue anything slow or stateful. Address generation, seed derivation, and reconciliation belong in jobs. Emit events (PaymentConfirmed, MnemonicGenerated) and let listeners handle notifications and audit logs — keeps HTTP requests lean and secrets out of request logs.
v2.0.3 is a patch that removes a library. "Remove old library" is technically a breaking change shipped as 2.0.3. Before adopting:
composer why <removed-package> # anything else depend on it?
grep -rn "OldVendor\\\\" app/ vendor/it-healer/ # did you reach past the facade?
composer outdated
php artisan test # especially your fixed-vector tests
Pin the exact version ("it-healer/laravel-bitcoin": "2.0.3") until your suite is green; don't float ^2.0 on a package with a one-line changelog and patch-level dependency removals.
Node configuration is the #1 source of "it doesn't work". Checklist: server=1, rpcuser/rpcpassword (or rpcauth), correct rpcport per network (8332 mainnet / 18332 testnet / 18443 regtest), rpcallowip if the app is on another host, and TLS/SSH tunnel if crossing a network boundary. Wrap the initial health check at boot so misconfiguration fails loudly early.
Network mismatch is the most expensive mistake. A testnet-configured app pointed at a mainnet node (or vice versa) will silently transact with real funds. Log the reported chain at startup and refuse to boot if it doesn't match config. Add it to your deploy checklist.
Units. Confirm whether each method returns BTC or satoshis. This is not consistent across RPC methods and is the most common off-by-1e8 bug. Assert with a known fixture once, then trust the conversion helper everywhere.
Config cache. php artisan config:cache bakes config/bitcoin.php into the framework cache — an .env change to RPC credentials won't take effect until you re-cache. Deploy pipelines that cache config must re-run config:cache after env changes. Locally, config:clear after publishing.
RPC credentials and secrets. The RPC password is a full wallet-authority credential. Keep it in .env/secrets manager, out of config/bitcoin.php committed to git, and out of logs. Sanitize request/response logging (Sentry, Telescope, HTTP client logging) — a captured RPC payload may include a signed transaction or a mnemonic.
Fees, replaceability, and mempool state. sendToAddress returns fast but a low-fee tx can sit unconfirmed for hours. Set an explicit fee target or fee_rate, and understand whether your sends are RBF-enabled (replaceable=true) so you have a recovery path. Don't hardcode a fee — estimate, and fall back to a configured floor if the estimator is unavailable.
Timeouts and retries. The default HTTP timeout may be too short under chain load. Set a sane timeout in config. But never retry a sendToAddress blindly — retries cause double-sends. Retry idempotent reads (getBlockchainInfo, getTransaction), never value-moving calls.
BIP39 determinism. If you use the mnemonic features, run the official BIP39 test vectors against v2.0.3 in an isolated harness and confirm a phrase generated by 2.0.2 validates and re-derives byte-identically under 2.0.3. Apply NFKD normalization to both mnemonic and passphrase (the spec requires it) — skipping it silently produces a different valid seed for non-ASCII input. And remember: the passphrase is not part of the mnemonic; losing it loses the funds.
Validation and throttling. Mnemonic validation and any address-derivation endpoint are bruteforce targets. Add throttle:5,1 and return generic errors. Wrap package exceptions in your own so validation messages never interpolate the phrase into laravel.log, Sentry, or APP_DEBUG traces.
Confirmations vs. finality. "Confirmed" is a business decision, not an RPC fact. Set a minimum confirmation threshold per use case (e.g. 3 for deposits, 6 for large ones), make it configurable, and never credit a balance on 0-conf.
Extension points. Put an interface in front of the package and bind it in a provider. That's your escape hatch for a future major-version API change, a different RPC transport, or a switch to a hosted node provider — your call sites depend on your contract, not ItHealer\LaravelBitcoin\*.
How can I help you explore Laravel packages today?