@chainberry/berry-signer
v1.0.5
Published
Pure signing library for multiple blockchain networks — EVM, BTC, LTC, SOL, TRX, XRP, TON
Maintainers
Readme
BerrySigner
Pure signing library for multiple blockchain networks. No network calls — takes an unsigned transaction and a private key, returns a signed transaction ready for broadcast.
Can run anywhere: browser, mobile wallet, browser extension, Node.js backend, or React Native.
Installation
npm install @chainberry/berry-signerInstall only the peer dependencies you need (one per chain):
# EVM (ETH, BNB, AVAX, ARB, OP, BASE, SONIC, POL, ...)
npm install ethers
# Bitcoin
npm install bitcoinjs-lib ecpair tiny-secp256k1
# Litecoin
npm install bitcoinjs-lib ecpair tiny-secp256k1
# Solana
npm install @solana/web3.js
# TRON
npm install tronweb
# XRP Ledger
npm install xrpl tiny-secp256k1
# TON
npm install @ton/core @ton/crypto @ton/tonSupported Chains
| Chain | Function | Returns |
|-------|----------|---------|
| EVM (ETH, BNB, POL, AVAX, ARB, OP, BASE, SONIC, …) | signEvmTransaction | Signed tx hex string |
| Bitcoin | signBtcTransaction | Raw tx hex string |
| Litecoin | signLtcTransaction | Raw tx hex string |
| Solana | signSolTransaction | Signed tx base64 string |
| TRON | signTrxTransaction | Signed tx JSON string |
| XRP Ledger | signXrpTransaction | Signed tx blob hex string |
| TON | signTonTransaction | { bocBase64, txHash } |
Private Key Format
All functions accept the private key as a hex string, with or without 0x prefix — unless noted otherwise.
| Chain | Accepted formats |
|-------|-----------------|
| EVM | 0x... or plain hex |
| Bitcoin / Litecoin | Plain hex (32 bytes) |
| Solana | Hex (32 or 64 bytes) or base58 |
| TRON | Plain hex (no 0x) |
| XRP | Plain hex (32 bytes) |
| TON | Hex (32 bytes), with or without 0x |
API Reference
EVM — signEvmTransaction
import { signEvmTransaction, EvmUnsignedTx } from "@chainberry/berry-signer";
const signedTxHex: string = await signEvmTransaction(privateKeyHex, unsignedTx);EvmUnsignedTx type:
type EvmUnsignedTx = {
to: string; // recipient address
chainId: number; // e.g. 1 (ETH), 56 (BNB), 42161 (ARB)
nonce: number; // from eth_getTransactionCount
gasLimit: string; // estimated gas, as decimal string
value?: string; // amount in wei, as decimal string (omit for token transfers)
data?: string; // hex-encoded calldata (required for ERC-20 transfers)
// EIP-1559 (preferred — use these if the network supports them):
maxFeePerGas?: string; // decimal string, in wei
maxPriorityFeePerGas?: string; // decimal string, in wei
// Legacy (fallback — use if maxFeePerGas is not available):
gasPrice?: string; // decimal string, in wei
};Returns a serialized signed transaction hex string, ready for eth_sendRawTransaction.
Note: All numeric fields (
gasLimit,value,gasPrice, etc.) are strings, not BigInt — this keeps the type JSON-serializable and avoids precision loss across network boundaries.
Bitcoin — signBtcTransaction
import { signBtcTransaction } from "@chainberry/berry-signer";
const signedTxHex: string = signBtcTransaction(privateKeyHex, unsignedPsbtBase64, isMainnet);| Param | Type | Description |
|-------|------|-------------|
| privateKeyHex | string | 32-byte private key as hex |
| unsignedPsbtBase64 | string | PSBT (Partially Signed Bitcoin Transaction) in base64 — build with UTXOs and outputs |
| isMainnet | boolean | true for mainnet, false for testnet |
Returns the raw transaction hex ready for broadcast.
Litecoin — signLtcTransaction
import { signLtcTransaction } from "@chainberry/berry-signer";
const signedTxHex: string = signLtcTransaction(privateKeyHex, unsignedPsbtBase64, isMainnet);Same signature as signBtcTransaction. Uses Litecoin-specific network params (bech32 prefix ltc / tltc).
Solana — signSolTransaction
import { signSolTransaction } from "@chainberry/berry-signer";
const signedTxBase64: string = signSolTransaction(privateKey, unsignedTxBase64);| Param | Type | Description |
|-------|------|-------------|
| privateKey | string | 32-byte seed (hex or base58) or 64-byte keypair (hex or base58) |
| unsignedTxBase64 | string | Serialized Transaction object in base64 — must already have recentBlockhash set |
Returns a base64-encoded signed transaction, ready for sendRawTransaction.
Note: Solana is the only chain that accepts base58 private keys (Phantom wallet export format). Both 32-byte seeds and 64-byte keypairs are accepted.
TRON — signTrxTransaction
import { signTrxTransaction } from "@chainberry/berry-signer";
const signedTxJson: string = await signTrxTransaction(privateKey, unsignedTx, rpcUrl);| Param | Type | Description |
|-------|------|-------------|
| privateKey | string | 32-byte private key as hex (no 0x) |
| unsignedTx | unknown | Raw transaction object from transactionBuilder — TronWeb format |
| rpcUrl | string | Any reachable TRON node URL (e.g. https://api.trongrid.io) |
Returns a JSON string of the signed transaction object.
Note:
rpcUrlis required by the TronWeb SDK constructor but no network call is made during signing. You can pass any reachable TRON node — it will not be contacted.
Expiration: TRON transactions expire 60 seconds after creation by default. If you're signing client-side and broadcasting server-side with any delay, extend the expiration before signing using
tronWeb.transactionBuilder.extendExpiration(tx, 600)(10 minutes).
XRP Ledger — signXrpTransaction
import { signXrpTransaction } from "@chainberry/berry-signer";
import type { Payment } from "xrpl";
const signedTxBlob: string = signXrpTransaction(privateKeyHex, preparedTx);| Param | Type | Description |
|-------|------|-------------|
| privateKeyHex | string | 32-byte private key as hex |
| preparedTx | Payment | XRPL Payment object with Sequence, Fee, and LastLedgerSequence already set |
Returns a hex-encoded signed transaction blob ready for submit.
Note: The
Paymenttype is from thexrplpackage. You must fill inSequence,Fee, andLastLedgerSequencebefore signing (via xrpl'sautofillor manually from network data).
TON — signTonTransaction
import { signTonTransaction, SignedTonTx } from "@chainberry/berry-signer";
const result: SignedTonTx = signTonTransaction(privateKeyHex, {
toAddress,
amount, // in TON (e.g. "1.5"), NOT in nanoton
seqno, // fetch from network first; use 0 for uninitialized wallets
memoId, // optional: memo string (for exchange deposits, CEX tagging)
});
// result.bocBase64 — signed BOC, ready for broadcast
// result.txHash — pre-computed tx hash (useful as broadcast fallback identifier)SignedTonTx type:
type SignedTonTx = {
bocBase64: string; // base64-encoded signed bag-of-cells
txHash: string; // hex-encoded transaction hash
};Uninitialized wallet: If
seqno === 0, the wallet init cell is automatically included in the BOC. This is required for a wallet that has never sent a transaction (newly funded address).
txHashusage: TON'ssendBocAPI does not return a transaction hash.txHashis pre-computed from the cell hash before broadcast and can be used to look up the transaction on-chain.
Full Examples
EVM — Native transfer
import { ethers } from "ethers";
import { signEvmTransaction } from "@chainberry/berry-signer";
const provider = new ethers.JsonRpcProvider("https://arb1.arbitrum.io/rpc");
// Fetch network data (read-only — no private key needed yet)
const [feeData, nonce] = await Promise.all([
provider.getFeeData(),
provider.getTransactionCount(fromAddress, "latest"),
]);
const unsignedTx = {
to: toAddress,
chainId: 42161, // Arbitrum One
nonce,
gasLimit: "21000",
maxFeePerGas: feeData.maxFeePerGas!.toString(),
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas!.toString(),
value: ethers.parseEther("0.01").toString(),
};
// Sign locally — private key never leaves this environment
const signedTx = await signEvmTransaction(privateKeyHex, unsignedTx);
// Send signed tx to server for broadcast (or broadcast directly)
await fetch("/api/broadcast", {
method: "POST",
body: JSON.stringify({ signedTx, network: "arbitrum" }),
});EVM — ERC-20 token transfer
import { ethers } from "ethers";
import { signEvmTransaction } from "@chainberry/berry-signer";
const provider = new ethers.JsonRpcProvider("https://arb1.arbitrum.io/rpc");
const usdcAddress = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"; // USDC on Arbitrum
const erc20Interface = new ethers.Interface(["function transfer(address,uint256)"]);
// Build calldata for ERC-20 transfer
const data = erc20Interface.encodeFunctionData("transfer", [
toAddress,
ethers.parseUnits("10.0", 6), // USDC has 6 decimals
]);
const [feeData, nonce, gasLimit] = await Promise.all([
provider.getFeeData(),
provider.getTransactionCount(fromAddress, "latest"),
provider.estimateGas({ from: fromAddress, to: usdcAddress, data }),
]);
const unsignedTx = {
to: usdcAddress, // send to the token contract, not the recipient
chainId: 42161,
nonce,
gasLimit: gasLimit.toString(),
maxFeePerGas: feeData.maxFeePerGas!.toString(),
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas!.toString(),
value: "0", // no ETH value for token transfers
data, // encoded transfer(address,uint256)
};
const signedTx = await signEvmTransaction(privateKeyHex, unsignedTx);TON — Native transfer
Step 1 — Fetch seqno from network:
const response = await fetch("https://toncenter.com/api/v2/runGetMethod", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ address: walletAddress, method: "seqno", stack: [] }),
});
const data = await response.json();
// exit_code !== 0 means wallet is not yet deployed — use seqno 0
const seqno = data.result?.exit_code === 0
? parseInt(data.result.stack[0][1], 16)
: 0;Step 2 — Sign locally:
import { signTonTransaction } from "@chainberry/berry-signer";
const { bocBase64, txHash } = signTonTransaction(privateKeyHex, {
toAddress: "UQA...", // recipient TON address
amount: "1.5", // in TON, not nanoton
seqno, // 0 = wallet not yet deployed, init is included automatically
memoId: "12345", // optional: memo for exchange deposits
});Step 3 — Broadcast:
await fetch("https://toncenter.com/api/v2/sendBoc", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ boc: bocBase64 }),
});
// txHash can be used to track the transaction on-chain
console.log("Track tx:", `https://tonscan.org/tx/${txHash}`);Bitcoin — Native transfer
import { signBtcTransaction } from "@chainberry/berry-signer";
// unsignedPsbtBase64 is built by your server using UTXOs + fee rate
// (fetched from mempool.space or similar indexer)
const signedTxHex = signBtcTransaction(privateKeyHex, unsignedPsbtBase64, true);
// Broadcast via your server or directly to a BTC node
await fetch("/api/broadcast", {
method: "POST",
body: JSON.stringify({ signedTx: signedTxHex, network: "bitcoin" }),
});Solana — Native transfer
import { signSolTransaction } from "@chainberry/berry-signer";
// unsignedTxBase64 is built by your server with recentBlockhash + instructions
const signedTxBase64 = signSolTransaction(privateKey, unsignedTxBase64);
await fetch("/api/broadcast", {
method: "POST",
body: JSON.stringify({ signedTx: signedTxBase64, network: "solana" }),
});Design
The private key never leaves the signing environment. The client (or secure signing service) prepares and signs the transaction, then sends only the signed tx for broadcast.
Signing environment Broadcast environment
│ │
│── fetch nonce / seqno / fee from RPC ───────>│ (or fetched directly by signer)
│ │
│── build unsigned tx │
│── sign with private key │
│ │
│── send signed tx only ──────────────────────>│── broadcast to network
│ │
│ (private key never crosses this boundary) │This separation makes BerrySigner useful in trust-minimized architectures — hardware wallets, MPC signers, browser extensions, or any environment where the signing key cannot be exposed to network-connected servers.
Chain IDs (EVM)
| Network | Chain ID | |---------|----------| | Ethereum | 1 | | BNB Smart Chain | 56 | | Polygon | 137 | | Avalanche C-Chain | 43114 | | Arbitrum One | 42161 | | Optimism | 10 | | Base | 8453 | | Sonic | 146 |
License
MIT
