# PHP SDK

The PHP SDK (version 0.2.0) is a **single file**, `Rarebl.php`, for **PHP 8.1 or later** with `ext-gmp` (and `ext-sodium` and `ext-openssl` for the envelope helper). All classes are in the namespace `Rarebl`: the client is `Rarebl\Rarebl`, signing is `Rarebl\Signer`, and there are `Keys`, `Address`, `Webhook` and `Envelope`. It builds, verifies and signs transactions **on your machine**; your private key never leaves the process. secp256k1 runs on GMP in pure PHP, with Keccak-256 and the address encodings included.

## Download

| File | Size | SHA-256 |
| --- | --- | --- |
| [Rarebl.php](https://docs.rarebl.com/sdk/php/Rarebl.php) | 110.7 KB | `5784caa72a1c89abd6a386911cac6008d8726757de3456e2606ff986592c142c` |

Requires: PHP 8.1 or later with ext-gmp (ext-sodium for envelopes). All checksums are in [SHA256SUMS](https://docs.rarebl.com/sdk/SHA256SUMS). Check the file before you use it:

~~~bash
curl -O https://docs.rarebl.com/sdk/php/Rarebl.php
sha256sum Rarebl.php
~~~

## Install

Copy `Rarebl.php` into your project and require it:

~~~php
<?php
require __DIR__ . '/Rarebl.php';
~~~

Registry publishing (Packagist `rarebl/sdk`) comes later; today you download the file and check its SHA-256 above. Check that GMP is loaded with `php -m | grep gmp`.

## Ten lines: send BTC

~~~php
<?php
require 'Rarebl.php';

$k = Rarebl\Keys::generate('btc', 'p2wpkh');          // local, CSPRNG: privateKey, wif, publicKey, address
echo "deposit address: {$k['address']}\n";            // fund it, then:

$client = new Rarebl\Rarebl();                        // https://api.rarebl.com
$txid = $client->transfer([
    'chain' => 'btc', 'from' => $k['address'], 'to' => 'bc1qanfrvw5lunlemvm09dthnxmwknal0fw0q7jfnm',
    'amount' => '0.001', 'key' => $k['privateKey'], 'priority' => 'standard',
]);
echo "broadcast: $txid\n";
~~~

## Send USDT on TRON

~~~php
<?php
$txid = $client->transfer([
    'chain' => 'tron', 'from' => $addr, 'to' => $recipient, 'amount' => '25',
    'token' => 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', 'key' => $tronKey, 'feeLimit' => 15000000,
]);
~~~

## `transfer()`

`$client->transfer([...])` runs **build, verify, sign locally, assemble and broadcast** and returns the txid. Amounts are **decimal strings** in whole units: never PHP floats.

## Lower level: build, sign, assemble

~~~php
<?php
$build = $client->build(['chain' => 'btc', 'from' => [$addr], 'to' => [['address' => $dest, 'amount' => '0.0015']], 'priority' => 'standard']);
$expect = ['to' => ['address' => $dest, 'amount' => '0.0015']];
Rarebl\Signer::verifyBuild($build, null, $expect);                      // recompute every digest, throws VerifyException on any mismatch
$signatures = Rarebl\Signer::signBuild($build, $key, ['expect' => $expect]);   // expect is required; verifies, then signs
$res = $client->assemble($build['unsigned'], $signatures, false);
$client->broadcast('btc', $res['signed']['hex'], 'payout-0001');
~~~

| Function | What it does |
| --- | --- |
| `Keys::generate`, `Keys::fromPrivate` | Local key generation and import. |
| `Address::derive`, `Address::validate` | Derive and validate addresses. |
| `Signer::signDigest` | One signature: DER + sighash byte, BIP340 Schnorr, or recoverable `r‖s‖v`. |
| `Signer::verifyBuild($build, $pubkeys, $expect, $nowMs, $prevTxs)` | Recompute every digest and check outputs, amounts, change and the fee. |
| `Signer::signBuild($build, $keys, $opts)` | Verify, then sign every digest. `$opts` is `['expect' => ..., 'unsafeNoExpect' => false, 'verify' => true, 'deterministic' => false, 'nowMs' => null, 'prevTxs' => null]`; it throws `InputException` without `expect['to']`. The old positional form is gone. |
| `build`, `assemble`, `broadcast`, `watch`, `utxos`, `status`, `signerKey`, `post`, `transfer` | The API calls, on `Rarebl\Rarebl`. |
| `Envelope::encrypt($signerKey, $payload)` | Builds an envelope v2 for [hosted signing](https://docs.rarebl.com/guides/hosted-signing) only, through `ext-sodium` (X25519) and `openssl_encrypt` (AES-256-GCM). `$signerKey` is the whole reply of `signerKey()`. |
| `Envelope::bindOf($chain, $to, $token, $decimals)`, `Envelope::decrypt` | `bindOf` converts recipients to the base-unit `bind` of an `op: 'send'` envelope; `decrypt` opens the `encryptedPrivateKey` of an [address/new](https://docs.rarebl.com/api/address/new) reply. |
| `Webhook::verify` | Check `X-Rarebl-Signature`. |

`$expect` pins who is paid: `to` (`['address', 'amount' | 'amountRaw']` or a list), `change` (extra change addresses; the input addresses always qualify), `token`, `decimals`, `opReturn`, `from`, `chain` and the fee caps below. `amount: "max"` must leave no change output. `'unsafeNoExpect' => true` signs without pinning the recipients (the fee caps still apply), and `'verify' => false` also needs it. `transfer()` always pins what you passed.

## Fee caps and defaults

When `expect` gives no cap, the SDK applies one. Override it through `expect`:

| Chain | Default | Override |
| --- | --- | --- |
| BTC, LTC | 1000 sat/vB | `maxFeeRate`, or an absolute `maxFee` (whole units) or `maxFeeSats` |
| BCH, DASH | 50 sat/byte | the same |
| ETH | `maxFeePerGas` at most 500 gwei, total fee at most 0.05 ETH, tip at most max(5 gwei, `maxFeePerGas` / 2) | `maxFeePerGas`, `maxPriorityFeePerGas` (wei), `maxFee`, `maxFeeRaw` |
| TRON | `feeLimit` at most 150 TRX | `maxFeeLimit` (sun) |

A UTXO transaction with a version other than 1 or 2, a non-zero locktime (`'allowLocktime' => true` allows it), a repeated outpoint or a sighash type other than SIGHASH_ALL is refused (`0x41`, with FORKID, is accepted on BCH only).

Legacy p2pkh inputs (BTC, LTC, DASH) must carry `unsigned.inputs[i].prevTx`, which `tx/build` delivers (or pass `'prevTxs' => [txid => rawHex]`): the SDK checks that it hashes to the outpoint and takes the amount from it.

## Hosted signing

For [hosted signing](https://docs.rarebl.com/guides/hosted-signing) the SDK builds an envelope v2 that names the chain, an expiry, a one-time id and what may be signed. The request needs the header `X-Rarebl-Accept-Risk: hosted-signing`.

~~~php
<?php
use Rarebl\Envelope;

$signer = $client->signerKey();                                            // ['kid', 'publicKey', 'expiresAt']
$digests = array_column($build['toSign'], 'digest');                       // after verifyBuild passed
$forSign = Envelope::encrypt($signer, ['privateKeys' => [$key], 'op' => 'sign', 'chain' => 'btc', 'digests' => $digests]);

$bind = Envelope::bindOf('btc', ['address' => $dest, 'amount' => '0.0015']);   // recipients, as base units
$forSend = Envelope::encrypt($signer, ['privateKeys' => [$key], 'op' => 'send', 'chain' => 'btc', 'bind' => $bind]);

$client->post('/open/tx/send', ['chain' => 'btc', 'from' => [$addr], 'to' => [['address' => $dest, 'amount' => '0.0015']],
                                'keys' => ['encrypted' => $forSend]], ['X-Rarebl-Accept-Risk' => 'hosted-signing']);
~~~

An envelope is valid for at most 300 seconds and opens once.

## Verify webhooks

~~~php
<?php
$raw = file_get_contents('php://input');               // the RAW body
$ok = Rarebl\Webhook::verify(getenv('RAREBL_WEBHOOK_SECRET'), $raw, $_SERVER['HTTP_X_RAREBL_SIGNATURE'] ?? '', 300);
if (!$ok) { http_response_code(401); exit; }
$event = json_decode($raw, true);                      // deduplicate on X-Rarebl-Event-Id
http_response_code(200);
~~~

See [Webhooks](https://docs.rarebl.com/guides/webhooks).

## Errors

API errors become `ApiException` (`->apiCode`, `->reason`, `->detail`, `->httpStatus`), for example `API422` and `node_rejected`. `VerifyException` is thrown before any signature is made, `KeyMismatchException` means a key does not own an input, `InputException` means a missing or invalid argument (for example signing without `expect`), and `EnvelopeException` covers the envelope helper. All extend `RareblException`.

## Security notes

- **Why local signing:** a key that never leaves your process cannot be intercepted, logged or leaked by a service you call: [Send a transaction](https://docs.rarebl.com/guides/send-a-transaction).
- **What `verify` protects against:** a server (or anything in between) returning a transaction that pays someone else, or digests that do not match. The SDK recomputes every digest and checks outputs, amounts, change and the fee before signing, and `expect` is required so that the recipient is always pinned. Turning verification off must be explicit (`'verify' => false` with `'unsafeNoExpect' => true`) and means you trust the server's digests blindly.
- **Pure-PHP crypto is not constant-time.** Run signing on a machine you control, not on a shared host.
- ECDSA signatures are RFC 6979 deterministic, low-S, and verified after signing. BIP340 (p2tr) signatures use fresh random auxiliary data; pass `'deterministic' => true` for reproducible output.
- Do not log request bodies on a server that handles keys, and keep keys out of web-accessible directories.
- `Envelope::encrypt` builds the envelope for [hosted signing](https://docs.rarebl.com/guides/hosted-signing). The SDK never sends a key by itself.

## Next

[Sign locally with the SDK](https://docs.rarebl.com/guides/sign-locally), [Send a transaction](https://docs.rarebl.com/guides/send-a-transaction), [Watch addresses, get UTXOs and balances](https://docs.rarebl.com/guides/watch-addresses).
