# JavaScript SDK

The JavaScript SDK is an ES module for **Node 18 or later and browsers**, shipped as a single file, `rarebl.mjs`. It builds, verifies and signs transactions **on your machine**; your private key never leaves the process. Version 0.2.0. It is the reference implementation: the Python and PHP SDKs must produce byte-identical ECDSA signatures (and Schnorr signatures when `deterministic` is set).

Crypto comes from the pinned libraries `@noble/curves`, `@noble/hashes` and `@scure/base`, bundled into the single file, so you do not install anything.

## Download

| File | Size | SHA-256 |
| --- | --- | --- |
| [rarebl-sdk-0.2.0.tgz](https://docs.rarebl.com/sdk/js/rarebl-sdk-0.2.0.tgz) | 103.7 KB | `78c11fb5a1c378f55e240556c472b837ed0fbef35b940c63d167ed846e43f5fa` |
| [rarebl.mjs](https://docs.rarebl.com/sdk/js/rarebl.mjs) | 197.7 KB | `7c5bd8fc5e412ae8596cedab97634915fd94d91bf2fb8b863c1656e36b070aea` |

Requires: Node 18 or later, or a browser (ESM). 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/js/rarebl.mjs
sha256sum rarebl.mjs
~~~

## Install

Download the file, check it against the SHA-256 above, and import it:

~~~js
import { Rarebl, keys } from './rarebl.mjs';
~~~

A tarball with the source and tests (`rarebl-sdk-0.2.0.tgz`) is next to it. The npm package `@rarebl/sdk` will follow. In a browser use `<script type="module">`. On Node 18 add `globalThis.crypto ??= (await import('node:crypto')).webcrypto;` first (Node 20 and later have it).

## Ten lines: send BTC

~~~js
import { Rarebl, keys } from './rarebl.mjs';

const me = keys.fromPrivate('btc', process.env.BTC_WIF, 'p2wpkh');   // or keys.generate('btc')
const rarebl = new Rarebl();                                          // https://api.rarebl.com

await rarebl.watch('btc', me.address);                                // BTC, LTC, BCH: start watching, wait until active

const txid = await rarebl.transfer({
  chain: 'btc', from: me.address, to: 'bc1qanfrvw5lunlemvm09dthnxmwknal0fw0q7jfnm', amount: '0.0015',
  key: process.env.BTC_WIF, priority: 'standard',                     // or feeRate: 12 (sat/vB)
});
console.log(txid);
~~~

## Send USDT on TRON

~~~js
const txid = await rarebl.transfer({
  chain: 'tron', from: 'T...sender', to: 'T...recipient', amount: '25',
  token: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',   // USDT-TRC20; decimals are known to the SDK, other tokens need decimals: n
  key: process.env.TRON_KEY,                       // 64 hex characters
});
~~~

ETH works the same (`chain: 'eth'`, `token: '0xdac17f958d2ee523a2206206994597c13d831ec7'` for USDT). The sender needs TRX or ETH for fees.

## `transfer()`

`transfer` builds the transaction on the API, **recomputes every digest and checks that it pays exactly what you asked** (recipient, amount, change back to you, fee cap), signs on your machine, then assembles and broadcasts. It throws `VerifyError` before signing if anything differs. Amounts are decimal strings in whole units: do not pass numbers for money.

## Lower level: build, describe, sign, assemble

~~~js
import { describeBuild, signBuild } from './rarebl.mjs';

const reply = await rarebl.build({ chain: 'btc', from: [addr], to: [{ address, amount: '0.001' }], priority: 'fast' });
const summary = describeBuild(reply, { expect: { to: [{ address, amount: '0.001' }] } });  // what is really being signed
const signatures = signBuild(reply, [key], { expect: { to: [{ address, amount: '0.001' }] } });  // expect is required; verify is on by default
const { broadcast } = await rarebl.assemble(reply.unsigned, signatures, { broadcast: true });
~~~

| Function | What it does |
| --- | --- |
| `keys.generate`, `keys.fromPrivate` | Local key generation (CSPRNG) and import (hex or WIF). |
| `address.derive`, `address.validate` | Derive and validate addresses. |
| `signDigest` | One signature: DER + sighash byte, BIP340 Schnorr, or recoverable `r‖s‖v`. |
| `verifyBuild` | Recompute every digest and check outputs, amounts, change and fee. |
| `describeBuild` | A readable summary of what a build reply would sign. |
| `signBuild(reply, keys, { expect, unsafeNoExpect?, verify?, deterministic?, prevTxs?, now? })` | Verify, then sign every digest. `expect` is required and must contain `to`; `verify` is on by default. |
| `Rarebl` client | `build`, `assemble`, `broadcast`, `decode`, `estimate`, `status`, `watch`, `utxos`, `nonce`, `resources`, `marketPrice`, `transfer`. |
| `envelope.encrypt(signerKey, { privateKeys, op, chain, digests?, bind?, exp?, jti? })` | Builds an envelope v2 for [hosted signing](https://docs.rarebl.com/guides/hosted-signing); not needed for local signing. `signerKey` is the whole reply of `rarebl.signerKey()`. |
| `envelope.bindOf`, `envelope.seal`, `envelope.decrypt` | `bindOf(chain, to, { token?, decimals? })` 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`. |

`signBuild` refuses to run without `expect` (it must contain `to`): without it nothing pins who is paid. Pass `unsafeNoExpect: true` to sign without pinning the recipients; the default fee caps still apply, and `verify: false` also needs it. `transfer()` always pins what you passed. The `expect` fields are `to`, `change`, `token`, `decimals`, `opReturn`, `from`, `chain` and the fee caps below.

TypeScript types are in the tarball (`src/index.d.ts`).

## 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` |
| 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).

## 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`.

~~~js
import { envelope } from './rarebl.mjs';

const signer = await rarebl.signerKey();                                       // {kid, publicKey, expiresAt}
const digests = reply.toSign.map((t) => t.digest);                             // after describeBuild passed
const forSign = await envelope.encrypt(signer, { privateKeys: [key], op: 'sign', chain: 'btc', digests });

const bind = envelope.bindOf('btc', [{ address: to, amount: '0.0015' }]);      // recipients, as base units
const forSend = await envelope.encrypt(signer, { privateKeys: [key], op: 'send', chain: 'btc', bind });
// POST /open/tx/sign or /open/tx/send with { ..., keys: { encrypted } } and the header above
~~~

An envelope is valid for at most 300 seconds and opens once. The signer refuses digests or outputs that are not the ones it names.

## Verify webhooks

~~~js
import { webhook } from './rarebl.mjs';

app.post('/hook', express.raw({ type: '*/*' }), (req, res) => {          // the RAW body, not parsed JSON
  if (!webhook.verify(process.env.RAREBL_SIGNING_SECRET, req.body, req.get('X-Rarebl-Signature'))) return res.sendStatus(400);
  const event = JSON.parse(req.body);                                     // deduplicate on X-Rarebl-Event-Id
  res.sendStatus(200);
});
~~~

The check is `HMAC-SHA256(secret, t + "." + body)` in constant time, with a 5 minute tolerance by default. See [Webhooks](https://docs.rarebl.com/guides/webhooks).

## Errors

API errors are typed (`ValidationError`, `ConflictError`, `UnprocessableError`, `RateLimitError` and others) and carry `code`, `reason` and `detail`, for example `API422` with reason `node_rejected`. A verification failure is a `VerifyError`, thrown before any signature is made.

## Security notes

- **Why local signing:** a private key that is never sent cannot be leaked, logged or subpoenaed from a server. The SDK never sends a key by itself; the only code that puts key material on the wire is `envelope.encrypt`, which you must call explicitly. See [Send a transaction](https://docs.rarebl.com/guides/send-a-transaction).
- **What `verify` protects against:** `signBuild` and `transfer` rebuild every digest (legacy, BIP143, BIP341 with SIGHASH_DEFAULT, BCH FORKID, EIP-1559 RLP and keccak, TRON `sha256(raw_data_hex)` plus a decode of TRX and TRC-20 transfers) from the unsigned transaction, and check outputs, amounts, change and fee against what *you* asked for. A compromised or buggy server therefore cannot make you sign a different payment. `expect` is mandatory in `signBuild` and `transfer` builds it from its arguments. `unsafeNoExpect: true` checks only internal consistency, and `verify: false` signs whatever digests the server sends: do not use either.
- **Legacy inputs:** the signature of a legacy p2pkh input (BTC, LTC, DASH) does not commit to its amount. The SDK therefore requires `unsigned.inputs[i].prevTx` (delivered by `tx/build`) or `prevTxs`, checks that it hashes to the outpoint and reads the amount from it; a reply without it, or with a different amount, is refused.
- **Known limits:** token decimals are never taken from the server: unknown tokens need an explicit `decimals`. TRON contracts other than TRX and TRC-20 `transfer` are refused.
- ECDSA signatures use RFC 6979 deterministic nonces and low-S, and are verified before they are returned. BIP340 (p2tr) signatures use fresh random auxiliary data; pass `deterministic: true` for reproducible output.
- **In a browser** the key lives in page memory: never load third-party scripts on a page that handles keys, and prefer a server you control for custody.
- **Hosted signing.** `envelope.encrypt` encrypts keys to the signer's X25519 key for `tx/sign` and `tx/send`; the key then leaves your machine, which local signing avoids: [Hosted signing](https://docs.rarebl.com/guides/hosted-signing).
- Keep keys in environment variables or a secret store, not in source. Test with small amounts first.

MIT licensed.

## 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).
