npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@yellow-org/clearnet-sdk

v0.9.0

Published

TypeScript SDK for Clearnet integration.

Readme

Clearnet TypeScript SDK

TypeScript SDK for Clearnet integration. This package exposes a public gateway client (balances, transfer history, EIP-712 transfers and operation recovery) and EVM, Solana, XRPL, and Bitcoin vault depositors. EVM supports native ETH and ERC-20 deposits. Solana supports native SOL and SPL token deposits. XRPL supports native XRP and issued-currency deposits. Bitcoin supports native BTC deposits. Deposits credit a destination account. EVM, Solana, XRPL, and Bitcoin can also carry an optional ADR-015 opaque reference.

The package is ESM-first. EVM callers use viem clients and primitives. Solana and XRPL callers provide SDK-owned signer adapters around their wallet or local keypair.

Install

npm install @yellow-org/clearnet-sdk viem xrpl @scure/btc-signer

For local development in this repository:

cd sdk/ts
npm ci

Solana signer adapters use the @solana/web3.js v1 transaction API. Import Solana client helpers from the SDK's solana namespace so transactions and RPC clients share the same constructors. Mixing transactions with a separately installed client can break methods such as simulateTransaction.

Gateway Client

GatewayClient is the TypeScript counterpart of the Go pkg/client used by the yellow CLI. It reads balances and recent transfers, and prepares, signs and submits EIP-712 transfers through a public gateway. It depends only on viem; import it from the /gateway entry point to keep chain depositors out of browser bundles.

import { GatewayClient, toBaseUnits } from "@yellow-org/clearnet-sdk/gateway";

const gateway = new GatewayClient({
  baseUrl: "https://gateway.example.org",
  networkId: "YTES",          // the gateway must serve this network
  // fingerprint: "…",        // optional: pin the identity `yellow network show` prints
});

const balance = await gateway.balance(address);              // live, includes the nonce
const page = await gateway.transfers(address, { limit: 20 }); // recent cache, cursor paging

const intent = await gateway.prepareTransfer({
  from: address,
  recipient: "0x…",                                          // or yellow://ynet/user/0x…
  assets: [{ asset: "USDC", amount: "125.5" }, { asset: "YELLOW", amount: "2" }],
  maxFee: "0.5",                                              // explicit YELLOW ceiling
});

// Any EIP-712 signer works: wagmi signTypedDataAsync, a viem WalletClient, or a local account.
const signed = await gateway.signTransfer(intent, (typedData) => walletClient.signTypedData({ account, ...typedData }));
const result = await gateway.submitTransfer(signed, { firstSubmission: true }); // sealed or pending

Prepared intents retain a catalogueFingerprint covering the signing domain, asset decimals and backing-asset identities. Both signing and submission check fresh metadata against it, including when the client has no configured trust pin. Persist that field with signed intents. Missing or changed catalogue metadata requires preparation and review again; this local guard does not change the EIP-712 schema or revoke signatures already issued. The Go client uses the same fingerprint through client.NetworkFingerprint.

Pool spotPrice is a base-unit reserve ratio multiplied by 10^18, not a human price string. The optional transferPrice is the effective integer YELLOW base-unit ratio used for transfer fees (EMA after bootstrap). Older gateways without transferPrice need an explicit user-approved fee cap.

Wallets that enforce "domain chain ID equals active chain" (MetaMask) need the Yellow chain active before signing. Its chain ID is networkChainId(networkId). prepareTransfer resolves a pasted user URI to its account address before signing. For an externally prepared Go intent that retains a canonical yellow:// recipient, transferTypedData instead signs the first 20 bytes of keccak256(uri) in the EIP-712 to slot, exactly like the Go SDK, while the routed request keeps the original URI. If your app signs one of these raw URI intents, show the full URI to the user before requesting the wallet signature: the wallet displays the derived address, not the human-readable URI.

Submission never retries automatically: a timeout can happen after the network included the transfer. Persist the signed transfer before posting and follow the CLI's recovery rules, which are exported as pure functions:

import { applySubmission, markSubmitting, newOperationRecord, observeOperation, retryDecision } from "@yellow-org/clearnet-sdk/gateway";

let record = newOperationRecord(signed, gateway.baseUrl);   // save it
const submitting = markSubmitting(record);                  // save it BEFORE the POST
try {
  record = applySubmission(record, submitting, {
    result: await gateway.submitTransfer(signed, { firstSubmission: record.submittedAt === undefined }),
  });
} catch (error) {
  record = applySubmission(record, submitting, { error });  // rejected, unresolved, or restored
}
// Later: observeOperation(record, await gateway.operation(record.id)).
// Replay record.signed only when retryDecision(record, status).allowed.

Set firstSubmission only when the authorization has never been submitted. Omitting it conservatively keeps POST errors unknown, including a stale-nonce 409 after an earlier execution. definiteRejection therefore applies only to recognized rejections of a known first submission. A preflight submitted: false describes this call only. Pass an abort signal as { signal }; the existing submitTransfer(signed, signal) form also remains supported.

Before signing another transfer, check unexplainedRejectedOperation(records, networkId, sender, prepared.nonce) as well as blockingOperation and staleNonceOperation. An advanced nonce after a locally rejected authorization needs reconciliation unless a local seal at that same nonce explains it. Call await reconcileRejectedOperation(record, originalGatewayClient) for that record, and persist the returned record under the account lock before signing. If it becomes sealed, stop the replacement send and review the actual payment. Otherwise nonceConsumedBy retains a different outgoing transfer sealed at the exact sender nonce, so unexplainedRejectedOperation no longer blocks it. The rejection and signed authorization remain intact. The web wallet performs this automatically during preparation and again under its signing lock.

gateway.sealedTransferAtNonce(sender, nonce) searches up to 1,000 recent activity entries. It relies on the trusted gateway's verified history index, without verifying validator quorum locally. Missing, expired, unavailable or inconsistent evidence keeps new sends blocked; ask the original gateway operator to restore its verified index. An advanced nonce alone never clears the guard.

Amounts are exact decimal strings (toBaseUnits, fromBaseUnits, formatBaseUnits); floats are never used. Errors are GatewayError (with a kind), APIError (REST envelope) and SubmissionError (always carries the operation ID; outcomeUnknown is never permission to re-sign).

Bitcoin Quick Start

Bitcoin deposits use BitcoinVaultDepositor. Native BTC uses BITCOIN_NATIVE_ASSET, which is an empty string, and deposit amounts are positive decimal BTC strings. The depositor spends from a P2WPKH address derived from the signer public key and pays the one generic P2WSH deposit address derived from the configured vault keys (ADR-023): every deposit pays the same address, and destination.account is instead carried as a raw 20-byte address in a zero-value OP_RETURN marker output alongside it. The SDK signs digest bytes through a caller-provided BitcoinSigner so local keys, HSMs, or wallet adapters can live outside the core depositor.

import {
  BITCOIN_NATIVE_ASSET,
  BitcoinCoreRpcClient,
  BitcoinVaultDepositor,
} from "@yellow-org/clearnet-sdk";
import {
  pubECDSA,
  randomPrivateKeyBytes,
  signECDSA,
} from "@scure/btc-signer/utils.js";
import type { BitcoinSigner } from "@yellow-org/clearnet-sdk";

const privateKey = randomPrivateKeyBytes();
const signer: BitcoinSigner = {
  algorithm: "secp256k1",
  getPublicKeyCompressed() {
    return pubECDSA(privateKey, true);
  },
  signDigest32(digest) {
    return signECDSA(digest, privateKey);
  },
};

const rpc = new BitcoinCoreRpcClient({
  url: "http://127.0.0.1:18443",
  username: "sdk",
  password: "sdk",
  wallet: "sdk",
});

const vaultPubkeys = [
  pubECDSA(randomPrivateKeyBytes(), true),
  pubECDSA(randomPrivateKeyBytes(), true),
  pubECDSA(randomPrivateKeyBytes(), true),
];

const depositor = new BitcoinVaultDepositor({
  network: "regtest",
  rpc,
  signer,
  vaultPubkeys,
  threshold: 2,
  minFundingConfirmations: 1,
  fallbackFeeRateSatPerVByte: 5n,
});

console.log(await depositor.depositorAddress()); // fund this P2WPKH address
console.log(depositor.depositAddress()); // the same generic address for every deposit

const result = await depositor.submitDeposit({
  destination: { account: "000000000000000000000000000000000000a1a1" },
  asset: BITCOIN_NATIVE_ASSET,
  amount: "0.0002",
});

console.log(result.txHash); // display txid
console.log(result.depositId); // "<txid>:0"
console.log(await depositor.chainDepositStatus(result.txHash, result.depositId, 1));

BitcoinCoreRpcClient sends Basic Auth only when both username and password are supplied. Browser demos should use a same-origin proxy, omit credentials in the client config, and inject Bitcoin Core credentials server-side.

EVM Quick Start

Native ETH deposits use EVM_NATIVE_ASSET, which is an empty string. Amounts are positive decimal strings. Native ETH uses 18 decimals by default, or the nativeDecimals override in depositor config; ERC-20 decimals are read from the token contract and cached.

import {
  ClearnetSdkError,
  EVM_NATIVE_ASSET,
  EvmVaultDepositor,
} from "@yellow-org/clearnet-sdk";
import {
  createPublicClient,
  createWalletClient,
  custom,
  http,
} from "viem";
import type { Address, EIP1193Provider } from "viem";

declare const window: Window & { ethereum?: EIP1193Provider };

const chainId = 31_337;
const rpcUrl = "http://127.0.0.1:8545";
// Replace with the Custody contract deployed on the configured chain.
const custodyAddress = "0x0000000000000000000000000000000000001000" as Address;

if (window.ethereum === undefined) {
  throw new Error("No EIP-1193 wallet found");
}

const accounts = (await window.ethereum.request({
  method: "eth_requestAccounts",
})) as Address[];
const walletAccount = accounts[0];
if (walletAccount === undefined) {
  throw new Error("Wallet did not return an account");
}

const publicClient = createPublicClient({
  transport: http(rpcUrl),
});
const walletClient = createWalletClient({
  account: walletAccount,
  transport: custom(window.ethereum),
});

const depositor = new EvmVaultDepositor({
  publicClient,
  walletClient,
  walletAccount,
  custodyAddress,
  chainId,
});

try {
  const result = await depositor.submitDeposit(
    {
      destination: { account: walletAccount },
      asset: EVM_NATIVE_ASSET,
      amount: "0.01",
    },
    {
      onSubmitted(submitted) {
        console.log("deposit submitted", submitted.txHash, submitted.depositId);
      },
    },
  );

  console.log("deposit mined", result.txHash, result.depositId);
  console.log(
    "status",
    await depositor.chainDepositStatus(result.txHash, result.depositId, 1),
  );
} catch (error) {
  if (error instanceof ClearnetSdkError) {
    console.error(error.code, error.txHash, error.depositId, error.step);
  }
  throw error;
}

The SDK checks the configured public RPC chain and wallet chain before signing or submitting a deposit. If either chain does not match chainId, it throws CHAIN_MISMATCH.

EVM nonces

Every EVM deposit consumes the next 2D nonce for (depositor, nonceKey) — the same key << 64 | sequence layout as the ERC-4337 EntryPoint. nonceKey defaults to 0n; pass EvmSubmitDepositInput.nonceKey to use another key. Each key has its own consecutive sequence, so one depositor address can keep several independent deposit streams.

Router rule: a contract that deposits on behalf of many users should give each user its own key (for example key = uint192(uint160(user)), as integration rule 2 in custody's src/interfaces/IDeposit.sol recommends) and must take the sequence from the user's own signed input, never read it from on-chain state at execution time (otherwise, after a deep reorg, the replayed call can pick a different nonce and be credited twice). submitDeposit reads getNonce itself, so it is meant for an account that submits its own deposits, not for building such a contract's calldata.

Do not call submitDeposit concurrently for the same (depositor, nonceKey) — both calls would race the same on-chain counter and one would revert. Calling it again after a previous call has already returned successfully is safe.

If submitDeposit throws after the nonce has been read, the error carries step ("approve" or "deposit"), depositId, nonceKey, nonce, and txHash when known (see Troubleshooting). Do not blindly retry: a retry reads a fresh nonce and, if the earlier attempt actually landed on chain, creates a second real deposit. Resolve the outcome first:

  1. step is "deposit" and txHash is set: call chainDepositStatus(txHash, depositId, minConfirmations). "confirmed" or "pending" means the deposit exists; "absent" means that transaction did not produce it.
  2. No txHash (the wallet call itself failed, so viem returned no hash), or step is "approve": chainDepositStatus cannot be used. Read the vault's getNonce(depositor, nonceKey) at latest. If it is greater than the error's nonce, a deposit with that nonce has landed (this one, or another from the same depositor and key); otherwise the nonce is unused and calling submitDeposit again is safe.
  3. STALE_NONCE: the nonce this call read had already been consumed by another deposit, so this call created no deposit; call submitDeposit again, and it reads the now-current nonce.

After a successful mine, submitDeposit also checks that the receipt carries the vault's Deposited log for the returned depositId; if it does not, it throws DEPOSIT_EVENT_NOT_FOUND with txHash and depositId.

ERC-20 Deposits

For ERC-20 deposits, pass the token contract address as asset and the token amount as a decimal string.

const result = await depositor.submitDeposit({
  destination: { account: walletAccount },
  asset: "0x0000000000000000000000000000000000003000",
  amount: "25",
});

The SDK reads and caches the token's decimals(), converts the decimal amount to base units, and checks the current allowance. If it does not already cover the amount, the SDK submits an exact-amount approve(custodyAddress, amount) transaction and waits for it to mine before submitting the custody deposit(...) transaction. When the allowance already covers the amount (for example on a retry after an earlier approve landed), no approve is sent. An allowance that is nonzero but below the amount still gets a nonzero-to-nonzero approve, which tokens such as USDT reject; reset such a token's allowance to zero first. A successful submitDeposit call returns the deposit transaction's { txHash, depositId }, not the approval transaction's. If an ERC-20 approval or deposit fails, the thrown ClearnetSdkError carries step ("approve" or "deposit"), the deposit's identity fields, and txHash when known — see EVM nonces.

Solana Deposits

Solana deposits use SolanaVaultDepositor. The SDK builds the custody instruction and delegates signing/broadcast to a caller-provided SolanaSigner. The signer boundary is small so browser-wallet, Wallet Standard, and local keypair adapters can live outside the core SDK.

prepareDeposit(input) returns the same unsigned custody Transaction used by submitDeposit. It does not set a recent blockhash or sign/broadcast. Clients may use it for simulation, compute-budget selection, and fee estimation without duplicating the SDK's custody instruction encoding.

import {
  SOLANA_NATIVE_ASSET,
  SolanaVaultDepositor,
  solana,
  type SolanaSigner,
} from "@yellow-org/clearnet-sdk";
const {
  Connection,
  Keypair,
  LAMPORTS_PER_SOL,
  sendAndConfirmTransaction,
} = solana;

const rpcUrl = "http://127.0.0.1:8899";
const keypair = Keypair.generate();
const connection = new Connection(rpcUrl, "confirmed");

const airdrop = await connection.requestAirdrop(
  keypair.publicKey,
  LAMPORTS_PER_SOL,
);
await connection.confirmTransaction(airdrop, "confirmed");

const signer: SolanaSigner = {
  publicKey: keypair.publicKey.toBase58(),
  async signAndSend(transaction) {
    return sendAndConfirmTransaction(connection, transaction, [keypair], {
      commitment: "confirmed",
      preflightCommitment: "confirmed",
    });
  },
};

const depositor = new SolanaVaultDepositor({
  rpcUrl,
  signer,
  commitment: "confirmed",
});

const result = await depositor.submitDeposit({
  destination: {
    account: "00000000000000000000000000000000000000a1",
    ref: "0x3333333333333333333333333333333333333333333333333333333333333333",
  },
  asset: SOLANA_NATIVE_ASSET,
  amount: "0.1",
});

console.log(result.txHash); // Solana base58 signature
console.log(result.depositId); // "0x<sha256(signature bytes)>:0"
console.log(await depositor.chainDepositStatus(result.txHash, result.depositId, 0));

Native SOL uses SOLANA_NATIVE_ASSET, which is an empty string. For SPL deposits, pass the mint public key as asset and the token amount as a decimal string. The SDK reads and caches SPL mint decimals. It does not mint tokens or create token accounts. SPL callers must ensure the depositor ATA and vault ATA exist before submitting the deposit.

XRPL Deposits

XRPL deposits use XrplVaultDepositor. Amounts are positive decimal strings. Native XRP uses an empty asset string. Issued-currency assets use CUR.rIssuer and require decimals in depositor config.

import {
  XRPL_NATIVE_ASSET,
  XrplVaultDepositor,
} from "@yellow-org/clearnet-sdk";
import { Wallet, hashes, type SubmittableTransaction } from "xrpl";
import type {
  XrplPreparedPayment,
  XrplSigner,
} from "@yellow-org/clearnet-sdk";

const wallet = Wallet.generate();
const signer: XrplSigner = {
  classicAddress: wallet.classicAddress,
  async sign(payment: XrplPreparedPayment) {
    const signed = wallet.sign(payment as SubmittableTransaction);
    return { txBlob: signed.tx_blob, hash: hashes.hashSignedTx(signed.tx_blob) };
  },
};

const depositor = new XrplVaultDepositor({
  rpcUrl: "ws://127.0.0.1:6006",
  vaultAddress: "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
  signer,
});

try {
  const result = await depositor.submitDeposit({
    destination: {
      account: "00000000000000000000000000000000000000a1",
      ref: "0x3333333333333333333333333333333333333333333333333333333333333333",
    },
    asset: XRPL_NATIVE_ASSET,
    amount: "1",
  });

  console.log(result.txHash); // uppercase XRPL transaction hash
  console.log(result.depositId); // the lower-cased hash
  console.log(await depositor.chainDepositStatus(result.txHash, result.depositId, 0));
} finally {
  await depositor.disconnect();
}

For issued currencies, pass the asset key, decimal amount, and configured decimals:

const usdAsset = "USD.rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH";
const depositor = new XrplVaultDepositor({
  rpcUrl: "ws://127.0.0.1:6006",
  vaultAddress: "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
  signer,
  issuedAssetDecimals: { [usdAsset]: 2 },
});
const ref = await depositor.submitDeposit({
  destination: { account: "00000000000000000000000000000000000000a1" },
  asset: usdAsset,
  amount: "25.50",
});

Trustlines and balances must already exist before an issued-currency deposit. The SDK builds one XRPL Payment, adds one ynet-account memo carrying the Clearnet account/reference, asks the caller-provided signer to sign, submits the signed blob, and returns after rippled accepts the submit result as tesSUCCESS or terQUEUED. Use chainDepositStatus to observe validated-ledger finality; a just-submitted XRPL payment can return pending until it appears in a validated ledger.

prepareDeposit(input) returns the same autofilled unsigned Payment used by submitDeposit. Callers can inspect its Fee, Sequence, and LastLedgerSequence before signing; submitDeposit remains the authoritative SDK signing and submission flow.

Deposit References

Pass destination.ref to attach a 32-byte opaque sub-account reference to the deposit. Omit it when there is no sub-account reference.

const result = await depositor.submitDeposit({
  destination: {
    account: walletAccount,
    ref: "0x3333333333333333333333333333333333333333333333333333333333333333",
  },
  asset: EVM_NATIVE_ASSET,
  amount: "0.01",
});

For EVM, the reference is passed to Custody.deposit(...) as bytes32. For Solana, it is encoded into deposit_sol or deposit_spl as [u8; 32]. For XRPL, it is appended after the 20-byte Clearnet account in the ynet-account payment memo. The SDK does not interpret it. Omitted references are sent as 32 zero bytes. For Bitcoin, a non-zero reference selects the ADR-023 version 0x02 deposit marker (the OP_RETURN output carries the account and the reference); an omitted or all-zero reference selects version 0x01, which carries no reference on the wire.

Verify A Deposit

const status = await depositor.chainDepositStatus(
  result.txHash,
  result.depositId,
  1,
);

chainDepositStatus is a pure on-chain read — it does not check clearing/crediting on Clearnet, only whether the deposit transaction itself is present and final. It does not apply custody's crediting rules (for example the Bitcoin dust floor and self-deposit guard, or XRPL partial payments and asset support), so "confirmed" does not guarantee the deposit will be credited. It returns:

| Status | Meaning | |---|---| | confirmed | The transaction has a successful receipt and at least minConfirmations confirmations. | | pending | The transaction is known but is not confirmed enough yet. | | absent | The transaction is unknown or has a reverted receipt. |

minConfirmations accepts a non-negative safe integer number or a non-negative bigint. EVM treats it as an inclusive receipt confirmation count. Solana maps it onto the commitment ladder: 0 accepts confirmed; >= 1 requires finalized. XRPL validates the shape for cross-chain parity but treats XRPL finality as binary: a validated transaction is confirmed. Bitcoin treats 0 as 1, so a mempool transaction is never confirmed; it returns pending for mempool or shallow transactions and confirmed when the transaction has at least minConfirmations (at least one) confirmations.

API Reference

EvmVaultDepositor

new EvmVaultDepositor(config: EvmDepositorConfig)

Config fields:

| Field | Type | Notes | |---|---|---| | publicClient | viem PublicClient | Used for chain checks, receipt waits, and verification. | | walletClient | viem WalletClient | Used to submit approval and deposit transactions. | | walletAccount | Account \| Address | Must match walletClient.account when the wallet client has one. | | custodyAddress | Address | The deployed custody contract address. | | chainId | number | Positive safe integer; must match the public RPC and wallet chain. | | receiptTimeoutMs | number | Optional default timeout for receipt waits. | | nativeDecimals | number | Optional native ETH decimals override; defaults to 18. |

submitDeposit(input, options?)

Input fields:

| Field | Type | Notes | |---|---|---| | destination.account | Address | Clearnet account credited by the custody deposit. | | destination.ref | Hash \| undefined | Optional 32-byte opaque reference. Omitted values are sent as bytes32(0). | | asset | Address \| "" | Use EVM_NATIVE_ASSET for native ETH, or an ERC-20 token address. | | amount | string | Positive decimal amount. Native uses nativeDecimals; ERC-20 tokens use on-chain decimals(). | | nonceKey | bigint \| undefined | Optional upper 192 bits of the 2D deposit nonce, in [0, 2^192) (otherwise INVALID_INPUT); defaults to 0n. See EVM nonces. |

Options:

| Field | Notes | |---|---| | signal | Aborts the receipt wait. | | receiptTimeoutMs | Overrides the receipt wait timeout for this call. | | onSubmitted | Called once with the deposit's SubmitDepositResult (see below), after the deposit transaction mines. |

Returns:

interface SubmitDepositResult {
  txHash: string;
  depositId: string;
}

txHash is the mined deposit transaction's hash. depositId is "0x" + 64 lowercase hex characters — the ID custody signs a MintReceipt for; it is exported as depositId(chainId, vault, depositor, nonce) from @yellow-org/clearnet-sdk, alongside requireDepositId, composeNonce, and splitNonce. Pass both to chainDepositStatus.

BitcoinVaultDepositor

new BitcoinVaultDepositor(config: BitcoinDepositorConfig)

Config fields:

| Field | Type | Notes | |---|---|---| | network | "mainnet" \| "testnet" \| "signet" \| "regtest" | Selects address parameters. | | rpc | BitcoinRpc | Used for UTXO lookup, fee estimates, broadcast, and verification. | | signer | BitcoinSigner | Provides a compressed secp256k1 public key and DER signature over 32-byte digests. | | vaultPubkeys | (Uint8Array \| string)[] | Compressed secp256k1 vault public keys. The SDK sorts them before building the P2WSH script. | | threshold | number | Multisig threshold, from 1 to the number of vault keys. | | minFundingConfirmations | bigint \| number | Optional UTXO confirmation floor; defaults to 1. | | feeTargetBlocks | bigint \| number | Optional estimatesmartfee target; defaults to 6. | | fallbackFeeRateSatPerVByte | bigint \| number | Optional fallback fee rate; defaults to 5. |

Bitcoin input fields:

| Field | Type | Notes | |---|---|---| | destination.account | string | 20-byte hex Clearnet account address. Every deposit pays the one generic P2WSH address (depositAddress()); the account is instead carried in the deposit's OP_RETURN marker output. | | destination.ref | `0x${string}` \| undefined | Optional 32-byte opaque reference. A non-zero value selects the ADR-023 version 0x02 marker; an omitted or all-zero value selects version 0x01. | | asset | string | Use BITCOIN_NATIVE_ASSET, the empty string. Other asset values are rejected. | | amount | string | Positive decimal BTC amount that fits in signed 64-bit satoshis. |

For Bitcoin, SubmitDepositResult.txHash is the display txid, and depositId is "<txid>:<vout>" with the decimal output index — the ID custody signs a MintReceipt for. The SDK-built deposit output is always output 0, so submitDeposit and submitSignedDepositPsbt return bitcoinDepositId(txHash, 0); bitcoinDepositId(txid, vout) is exported from @yellow-org/clearnet-sdk. It keeps the txid's case as given; pass the lowercase hex txid Bitcoin Core reports. submitSignedDepositPsbt throws INVALID_INPUT without broadcasting unless output 0 of the signed transaction pays depositAddress() the amount in expectedOutputs[0]. submitDeposit returns after Bitcoin Core accepts the raw transaction; use chainDepositStatus to observe mempool, shallow, and confirmed states.

Bitcoin chainDepositStatus throws INVALID_DEPOSIT_ID unless depositId is exactly bitcoinDepositId(txHash, vout). It reports "absent" unless the transaction is known, output vout pays depositAddress() a positive value, and the transaction's outputs carry exactly one valid ADR-023 deposit marker. BitcoinRpc.getRawTransaction must therefore return the transaction's outputs (value in satoshis and scriptPubKey hex, in output order); BitcoinCoreRpcClient reads them from verbose getrawtransaction. The dust floor and self-deposit guard custody applies before crediting are not checked.

For PSBT wallet signing, prepareDepositPsbt returns an unsignedTxID for the unsigned transaction shape. Use the txHash returned by submitSignedDepositPsbt for verification because wallet finalization can change the final txid for nested-SegWit inputs.

BitcoinCoreRpcClient is a convenience adapter for Bitcoin Core JSON-RPC:

new BitcoinCoreRpcClient({
  url: "http://127.0.0.1:18443",
  username: "sdk",
  password: "sdk",
  wallet: "sdk",
});

Omit username and password together when using a same-origin proxy that adds Basic Auth outside the browser.

SolanaVaultDepositor

new SolanaVaultDepositor(config: SolanaDepositorConfig)

Config fields:

| Field | Type | Notes | |---|---|---| | rpcUrl | string | Used for signature-status verification and commitment waits. | | signer | SolanaSigner | Provides publicKey and signAndSend(transaction). | | programId | string | Optional Solana custody program public key; defaults to the SDK's bundled custody program ID. | | commitment | "processed" \| "confirmed" \| "finalized" | Optional; defaults to finalized. | | receiptTimeoutMs | number | Optional default timeout for commitment waits. |

Solana input fields:

| Field | Type | Notes | |---|---|---| | destination.account | string | 20-byte Clearnet account as hex, optional 0x, or URI-like value whose final path segment is that hex. | | destination.ref | `0x${string}` \| undefined | Optional 32-byte opaque reference. | | asset | string | Use SOLANA_NATIVE_ASSET, the empty string, or an SPL mint public key. | | amount | string | Positive decimal amount. Native SOL uses 9 decimals; SPL tokens use mint decimals. |

For Solana, SubmitDepositResult.txHash is the base58 signature, and depositId is "0x" + lowercase hex of sha256(the 64 signature bytes) + ":" + index, where index is the Deposited event's zero-based position among the transaction's custody Deposited and Executed event CPIs. A transaction submitDeposit builds carries one deposit, so it returns solanaDepositId(signature, 0); solanaDepositId(signature, index) is exported from @yellow-org/clearnet-sdk.

Solana chainDepositStatus throws INVALID_DEPOSIT_ID unless depositId is exactly solanaDepositId(txHash, index). It keeps the signature-status commitment mapping, and for a confirmed or finalized status also fetches the transaction: a failed transaction, or one whose custody event at index is not a Deposited event, is "absent"; a transaction the RPC does not serve yet is "pending".

const transaction = await depositor.prepareDeposit(input);
// Add a recent blockhash and any client-owned compute-budget instructions,
// then simulate or call getFeeForMessage. submitDeposit still performs the
// authoritative SDK submission flow.

XrplVaultDepositor

new XrplVaultDepositor(config: XrplDepositorConfig)

Config fields:

| Field | Type | Notes | |---|---|---| | rpcUrl | string | XRPL WebSocket URL used for autofill, submit, and verification. | | vaultAddress | string | XRPL classic address that receives deposits. | | signer | XrplSigner | Provides classicAddress and sign(payment). | | maxFeeDrops | bigint \| number | Optional positive fee ceiling checked after autofill and before signing. | | issuedAssetDecimals | Record<string, number> | Required per issued-currency asset used for deposits. Native XRP uses 6 decimals. |

XRPL input fields:

| Field | Type | Notes | |---|---|---| | destination.account | string | 20-byte Clearnet account as hex, with optional 0x. | | destination.ref | `0x${string}` \| undefined | Optional 32-byte opaque reference. | | asset | string | Empty string for native, or issued-currency CUR.rIssuer. | | amount | string | Positive decimal amount; native XRP uses 6 decimals, issued currencies use configured decimals. |

For XRPL, SubmitDepositResult.txHash is the uppercase 64-hex transaction hash, and depositId is that hash lower-cased — the ID custody signs a MintReceipt for; xrplDepositId(txHash) is exported from @yellow-org/clearnet-sdk.

XRPL chainDepositStatus throws INVALID_DEPOSIT_ID unless depositId is xrplDepositId(txHash). It reports "absent" unless the transaction is a Payment from another account to vaultAddress whose first well-formed ynet-account memo names a non-zero 20-byte account followed by a 32-byte reference. Such a transaction is "pending" until validated, then "confirmed" if its result is tesSUCCESS and "absent" otherwise. Partial payments and asset support, which custody checks before crediting, are not checked.

const payment = await depositor.prepareDeposit(input);
console.log(payment.Fee); // autofilled network fee in drops

XrplVaultDepositor owns an XRPL WebSocket client. Call await depositor.disconnect() when the depositor is no longer needed, such as when replacing the signer or shutting down a long-lived process.

chainDepositStatus(txHash, depositId, minConfirmations)

Returns Promise<"absent" | "pending" | "confirmed">. A pure on-chain read — it does not check clearing/crediting on Clearnet, and "confirmed" does not guarantee the deposit will be credited.

Local Development

Run package checks from sdk/ts:

npm run typecheck
npm test
npm run build
npm --workspace @yellow-org/evm-deposit-demo run build
npm --workspace @yellow-org/solana-deposit-demo run build
npm --workspace @yellow-org/xrpl-deposit-demo run build
npm --workspace @yellow-org/bitcoin-deposit-demo run build

npm test discovers every unit and example test under test/, excluding *.integration.test.ts. The test:core, test:evm, test:sol, test:xrpl, and test:btc scripts run focused subsets. The test:integration:* scripts select Vitest's integration mode, which discovers only integration tests.

Run the EVM integration test against local Anvil:

# From the repository root:
make devnet-evm

# From sdk/ts:
npm run test:integration:evm

# From the repository root:
make devnet-down

The integration test deploys fresh Custody and MockERC20 contracts on each run.

Run the Bitcoin integration test against local Bitcoin Core regtest:

# From the repository root:
make devnet-btc

# From sdk/ts:
npm run test:integration:btc

# From the repository root:
make devnet-down

The Bitcoin integration test creates a fresh signer set, mines regtest funds, funds the generated depositor P2WPKH address, submits a native BTC deposit, and verifies the returned transaction reference before and after mining.

Run the Solana integration test against the local validator:

# From the repository root:
make devnet-sol

# From sdk/ts:
npm run test:integration:sol

# From the repository root:
make devnet-down

The Solana devnet preloads the custody program at 98eVpih8X9CAcgU9bzNB9V7VtkRrnFZUmqzEnsq7cfmg. The integration test creates and funds local signers, creates SPL token accounts needed for the test, submits native SOL and SPL deposits, and verifies each returned transaction reference.

Run the XRPL integration test against local rippled:

# From the repository root:
make devnet-xrpl

# From sdk/ts:
npm run test:integration:xrpl

# From the repository root:
make devnet-down

The XRPL integration test uses rippled WebSocket ws://127.0.0.1:6006 for SDK calls and admin JSON-RPC http://127.0.0.1:5005 for ledger_accept. Override with XRPL_WS_URL and XRPL_ADMIN_RPC_URL if needed. It creates fresh accounts, funds them from the standalone genesis wallet, submits native XRP and issued currency deposits, and verifies each returned transaction reference.

To run the repository integration suite, including the TS EVM, Solana, XRPL, and Bitcoin integration tests:

# From the repository root:
make devnet
make integration
make devnet-down

Demo

Start the browser demo from sdk/ts:

npm run demo:evm
npm run demo:sol
npm run demo:xrpl
npm run demo:btc

The EVM demo expects:

  • an EIP-1193 wallet, such as MetaMask
  • an RPC URL and chain ID that match the wallet's selected network
  • a deployed Custody contract address
  • a funded wallet account on that network

make devnet-evm starts Anvil on http://127.0.0.1:8545 with chain ID 31337, but it does not predeploy Custody for the browser demo. See examples/evm-deposit/README.md for the browser flow, required contract setup, and troubleshooting notes.

The Solana demo discovers wallets through Wallet Standard, uses solana:signTransaction, and broadcasts the signed transaction through the configured RPC URL. The selected wallet chain must be one the wallet advertises, such as solana:localnet for a local validator. The local devnet preloads the custody program, but the wallet must be funded and SPL token accounts must already exist for SPL deposits. See examples/solana-deposit/README.md for local funding notes and troubleshooting.

The XRPL demo supports a local signer for standalone-devnet smoke tests and GemWallet for browser-wallet signing. The GemWallet path requires a custom wss:// endpoint that points at the same local chain as the demo because wallet network selection and SDK submission must agree. See examples/xrpl-deposit/README.md for the full local signer flow, GemWallet custom-network setup, funding steps, and troubleshooting notes.

The Bitcoin demo uses local generated keys and Bitcoin Core regtest through a Vite proxy at /btc-rpc. Start make devnet-btc, then npm run demo:btc. The browser never receives the Bitcoin Core username or password; the Vite proxy adds Basic Auth before forwarding to http://127.0.0.1:18443. The demo includes both a deterministic local-signer flow and an Xverse PSBT signing flow; see examples/bitcoin-deposit/README.md for the Xverse Electrs facade, local funding steps, and troubleshooting notes.

Troubleshooting

Errors thrown by the SDK use ClearnetSdkError with a stable code.

| Code | Common cause | |---|---| | INVALID_INPUT | Submit options are missing or have the wrong shape, an asset is unsupported, or Bitcoin transaction/PSBT finalization input is invalid. | | INVALID_ADDRESS | EVM address, Solana public key, Solana mint, program ID, XRPL classic address, XRPL issued-currency key, or Clearnet account is invalid. | | INVALID_AMOUNT | amount is not positive, has the wrong type/precision, or exceeds the chain limit (uint256 for EVM, uint64 for Solana/XRPL native drops, signed 64-bit satoshis for Bitcoin). | | INVALID_CONFIRMATIONS | minConfirmations is negative, fractional, or an unsafe number. | | INVALID_REFERENCE | destination.ref is not a 32-byte hex value. | | INVALID_TX_ID | txHash is not valid for the chain: EVM transaction hash, Solana 64-byte signature, XRPL 64-hex hash, or Bitcoin 64-hex txid. | | INVALID_DEPOSIT_ID | depositId is not a valid deposit ID: for EVM, exactly "0x" + 64 lowercase hex characters (rejects a txHash/logIndex shape); for Bitcoin, Solana and XRPL, not exactly the chain's deposit ID helper output for txHash. | | MISSING_WALLET_ACCOUNT | The EVM wallet account is missing/mismatched, or the Solana/XRPL signer is missing. | | CHAIN_MISMATCH | The configured chain or network does not match the RPC or wallet network, such as an EVM chain ID mismatch or unsupported Bitcoin network. | | INSUFFICIENT_FUNDS | Bitcoin only: confirmed depositor UTXOs cannot cover the deposit amount plus fee. | | TX_REVERTED | A submitted approval/deposit transaction reverted, or XRPL rejected the payment engine result. | | RECEIPT_TIMEOUT | Waiting for a receipt timed out or was aborted. | | STALE_NONCE | EVM only: the deposit reverted because the nonce read at the start of submitDeposit was already consumed by another deposit from the same (depositor, nonceKey). Call submitDeposit again — it reads the then-current nonce. | | DEPOSIT_EVENT_NOT_FOUND | EVM only: the deposit transaction mined successfully, but its receipt has no Deposited log from the configured vault whose depositor and nonce reproduce the expected depositId (for example a wrong custodyAddress or chainId). Carries txHash and depositId. | | RPC_ERROR | The public RPC or wallet provider returned an unexpected error. |

When a transaction may already have been submitted, ClearnetSdkError can include txHash (the on-chain transaction/signature hash, when known) and, for EVM, depositId, nonceKey and nonce (known before broadcast) and step ("approve" or "deposit"). Use txHash to let a user inspect the submitted transaction, and the other fields to decide whether it is safe to retry — see EVM nonces.