Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Bech32 Laravel Package

bitwasp/bech32

A pure PHP implementation of bech32 and native SegWit address encoding. Use it standalone to encode and decode bech32 strings, including native SegWit addresses, in legacy codebases that have not yet upgraded to bitwasp/bitcoin.

View on GitHub
Deep Wiki
Context7

Getting Started

bitwasp/bech32 is not a Laravel package — it has no service provider, no config file, no facade, and no published assets. It is a set of global functions (encode, decode, convertBits, encodeSegwit, decodeSegwit) plus one exception class, autoloaded via Composer's files directive. Treat it as a low-level utility library you call directly, not something you "integrate."

Install and verify (2 minutes):

composer require bitwasp/bech32
php -r "var_dump(function_exists('BitWasp\\Bech32\\encodeSegwit'));"  # true

Or in php artisan tinker:

>>> \BitWasp\Bech32\encodeSegwit('bc', 0, hex2bin('751e76e8199196d454941c45d1b3a323f1433bd6'));
=> "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"

That string is the canonical BIP173 P2WPKH test vector — if it matches, your install is correct.

Where to look first: don't hunt for docs; the README is four paragraphs. Go straight to the source (src/bech32.php and src/segwit.php, roughly 200 lines total) and read all of it. The whole API surface is:

BitWasp\Bech32\encode(string $hrp, array $data5bit): string
BitWasp\Bech32\decode(string $bech32): array               // [$hrp, $data5bit]
BitWasp\Bech32\convertBits(array $data, int $from, int $to, bool $pad): array
BitWasp\Bech32\encodeSegwit(string $hrp, int $version, string $program8bit): string
BitWasp\Bech32\decodeSegwit(string $hrp, string $bech32): array  // [$version, $program8bit]
BitWasp\Bech32\Exception\Bech32Exception

First real use case: validating a user-submitted BTC withdrawal address in a form request, before you persist it. Decode it, check the HRP matches your network, check the witness version is one you support, and only then save. Second use case: turning a hash160 from a bitwasp/bitcoin HD key derivation into a deposit address via encodeSegwit('bc', 0, $hash160).

Implementation Patterns

Wrap it — never call the functions from controllers

Global functions can't be mocked, injected, or swapped. One thin class pays for itself the first time you move to a bech32m-capable library:

namespace App\Support\Bech32;

use BitWasp\Bech32\Exception\Bech32Exception;
use function BitWasp\Bech32\decodeSegwit;
use function BitWasp\Bech32\encodeSegwit;

final class SegwitAddress
{
    public function __construct(private readonly string $hrp = 'bc') {}

    public function encode(int $version, string $program): string
    {
        return encodeSegwit($this->hrp, $version, $program);
    }

    /** @return array{version:int, program:string} */
    public function decode(string $address): array
    {
        [$version, $program] = decodeSegwit($this->hrp, $address);
        return ['version' => $version, 'program' => $program];
    }

    public function isValid(string $address): bool
    {
        try {
            $this->decode($address);
            return true;
        } catch (Bech32Exception) {
            return false;
        }
    }
}

Bind the HRP per environment in a service provider — bc on mainnet, tb on testnet, bcrt for regtest — so a staging deployment can't accidentally mint mainnet addresses.

Validation as a Laravel rule object

final class SegwitAddressRule implements ValidationRule
{
    public function __construct(private readonly string $hrp = 'bc') {}

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (!is_string($value) || $value === '') {
            $fail('The :attribute must be a bech32 address.');
            return;
        }
        // bech32 forbids mixed case; the spec says "either all lower or all upper"
        if (strtolower($value) !== $value && strtoupper($value) !== $value) {
            $fail('Addresses may not mix upper and lower case.');
            return;
        }
        try {
            [$version, $program] = decodeSegwit($this->hrp, $value);
        } catch (Bech32Exception) {
            $fail('The :attribute is not a valid :hrp bech32 address.');
            return;
        }
        if ($version !== 0 || !in_array(strlen($program), [20, 32], true)) {
            $fail('Only P2WPKH and P2WSH addresses are supported.');
        }
    }
}

Run str_replace([':hrp'], [$this->hrp], ...) on the message or just hardcode the network name to keep messages user-friendly.

Deriving addresses from a stored xpub

bitwasp/bech32 only encodes; key derivation belongs to bitwasp/bitcoin (or simplito/elliptic-php). The typical job pipeline:

$hdKey   = HierarchicalKeyFactory::fromExtended($xpub, $network);
$child   = $hdKey->derivePath("0/{$index}");
$hash160 = $child->getPublicKey()->getPubKeyHash();   // 20 bytes
$address = app(SegwitAddress::class)->encode(0, $hash160);

Persist address (lowercase, unique index, binary/case-sensitive collation) alongside index and derivation_path. Never re-derive on read — derivation is the slow part, not the encoding.

Script construction when you need the scriptPubKey

decodeSegwit gives you the witness program, not the script. For a watch-only scanner or an API response:

$script = $version === 0
    ? "\x00" . chr(strlen($program)) . $program   // OP_0 <push>
    : "\x51" . chr(strlen($program)) . $program;  // OP_1 ... (bech32m territory — see below)

Generic bech32 for non-Bitcoin payloads

encode/decode are payload-agnostic. NIP-19 Nostr identifiers (npub1…, nsec1…, note1…) are exactly bech32(hrp, convertBits($key32bytes, 8, 5, true)) — the same library covers that with zero extra dependencies. Same trick works for any 5-bit-grouped format you control internally.

Testing

Seed a PHPUnit data provider with the BIP173 vectors (valid and invalid) from the BIP text itself. That's your regression net against a package that will never ship another release. Assert on exception class, not message text.

Gotchas and Tips

No bech32m (BIP350). This is the dealbreaker for anything modern. The library predates the taproot checksum constant, so encodeSegwit('bc', 1, $xonly32) will happily produce a bech32-checksummed string that no wallet accepts, and decodeSegwit will accept invalid P2TR addresses. If you support Taproot at all, this package is the wrong tool — use bitwasp/bitcoin's current address classes or a maintained bech32m-capable library. Guard it: reject $version !== 0 at the boundary (as in the rule above) so the failure is loud rather than silent fund loss.

The 90-character limit. BIP173 caps bech32 strings at 90 chars. Verify against the source of the version you install whether decode enforces it — if it does, you cannot use the generic decode for BOLT11 Lightning invoices or long LNURL strings, which routinely exceed that. Don't assume; write a test with a real invoice before you build on it.

decode returns 5-bit integers, not bytes. Passing that array to hash() or bin2hex() produces nonsense. You need convertBits($data, 5, 8, false) (note pad = false) — or just use decodeSegwit, which does it for you. When padding into 5-bit groups use pad = true; padding rules are asymmetric and getting them backwards is the single most common bug with this API.

Case handling. Mixed-case input must be rejected. Some implementations lowercase defensively before validating, which silently accepts spec-invalid input; the guard in the rule above is deliberate. Also: always persist and compare addresses in lowercase, and make sure the MySQL column collation is utf8mb4_bin (or *_as_cs) — the default _ci collation will treat two "different" base32 strings as equal and let duplicates through your unique index.

Errors are coarse. Everything throws Bech32Exception with the same class — invalid checksum, bad HRP, wrong length all look identical. Don't try to distinguish them by matching message strings; that's unstable API. If you need granular user feedback, pre-check length and character set yourself before calling.

HRP mismatch throws. decodeSegwit('bc', $testnetAddress) throws rather than returning something usable, which is what you want — but it means you must know the network before validating. For a UI that accepts "any BTC address," try bc then tb and branch on which succeeds.

Abandonware status. 12 stars, PHP 5.6–7.2 target, Travis CI, "will eventually replace the implementation in bitwasp/bitcoin" — that replacement happened elsewhere years ago. It runs fine on PHP 8 in practice (plain scalar type hints, no deprecations), but there will be no fixes. Pin the version in composer.json, keep the BIP173 vector tests, and budget a swap if you ever need bech32m. If you already depend on bitwasp/bitcoin, you almost certainly don't need this package at all.

No extension requirements. Pure PHP — no gmp, no bcmath, no ext-secp256k1. That's genuinely its best property, and the reason it's still a sane choice for a validation-only microservice or a shared-hosting deployment where you can't install extensions.

Cache the validity bit. Address validity is a pure function of the string. Since it's trivially cheap (microseconds) don't bother caching in-request, but do validate once at write time and store a boolean if you're validating the same addresses on every page render.

No extension points. There are no hooks, no interfaces, no driver system — only functions. Everything customizable lives in your wrapper class. That's the design; don't fight it.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style