@chainberry/berry-signer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # BerrySigner
2
+
3
+ Pure signing library for multiple blockchain networks. No network calls — takes an unsigned transaction prepared elsewhere and signs it locally with a private key. Designed to run on the frontend (mobile wallet, browser extension) while the unsigned transaction is prepared on the backend.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ npm install berry-signer
9
+ ```
10
+
11
+ ## Supported Chains
12
+
13
+ | Chain | Function |
14
+ |-------|----------|
15
+ | EVM (ETH, BNB, POL, AVAX, ARB, OP, BASE, SONIC) | `signEvmTransaction` |
16
+ | Bitcoin | `signBtcTransaction` |
17
+ | Litecoin | `signLtcTransaction` |
18
+ | Solana | `signSolTransaction` |
19
+ | TRON | `signTrxTransaction` |
20
+ | XRP Ledger | `signXrpTransaction` |
21
+ | TON | `signTonTransaction` |
22
+
23
+ ## Usage
24
+
25
+ Each function takes a private key and an unsigned transaction prepared by the backend, and returns a signed transaction ready for broadcast.
26
+
27
+ ### EVM (ETH, BNB, POL, AVAX, ARB, OP, BASE, SONIC)
28
+
29
+ ```ts
30
+ import { signEvmTransaction } from "berry-signer";
31
+
32
+ const signedTx = await signEvmTransaction(privateKeyHex, unsignedTx);
33
+ // signedTx is a hex string ready to broadcast via eth_sendRawTransaction
34
+ ```
35
+
36
+ ### Bitcoin
37
+
38
+ ```ts
39
+ import { signBtcTransaction } from "berry-signer";
40
+
41
+ const signedTxHex = signBtcTransaction(privateKeyHex, unsignedPsbtBase64, isMainnet);
42
+ // signedTxHex is a raw transaction hex ready for broadcast
43
+ ```
44
+
45
+ ### Litecoin
46
+
47
+ ```ts
48
+ import { signLtcTransaction } from "berry-signer";
49
+
50
+ const signedTxHex = signLtcTransaction(privateKeyHex, unsignedPsbtBase64, isMainnet);
51
+ ```
52
+
53
+ ### Solana
54
+
55
+ ```ts
56
+ import { signSolTransaction } from "berry-signer";
57
+
58
+ const signedTxBase64 = signSolTransaction(privateKey, unsignedTxBase64);
59
+ // signedTxBase64 is a base64-encoded signed transaction
60
+ ```
61
+
62
+ ### TRON
63
+
64
+ ```ts
65
+ import { signTrxTransaction } from "berry-signer";
66
+
67
+ const signedTxJson = await signTrxTransaction(privateKey, unsignedTx, rpcUrl);
68
+ // signedTxJson is a JSON string of the signed transaction
69
+ ```
70
+
71
+ ### XRP Ledger
72
+
73
+ ```ts
74
+ import { signXrpTransaction } from "berry-signer";
75
+
76
+ const signedTxBlob = signXrpTransaction(privateKeyHex, preparedTx);
77
+ // signedTxBlob is a hex-encoded signed transaction blob
78
+ ```
79
+
80
+ ### TON
81
+
82
+ ```ts
83
+ import { signTonTransaction } from "berry-signer";
84
+
85
+ const { bocBase64, txHash } = signTonTransaction(privateKeyHex, {
86
+ toAddress,
87
+ amount, // in TON (e.g. "1.5")
88
+ seqno, // wallet seqno fetched from the network
89
+ memoId, // optional memo
90
+ });
91
+ // bocBase64 is the signed BOC ready for broadcast
92
+ // txHash is the pre-computed transaction hash (needed for TON broadcast fallback)
93
+ ```
94
+
95
+ ## Design
96
+
97
+ The split between signing and broadcasting allows the private key to stay on the client:
98
+
99
+ ```
100
+ Backend Frontend (BerrySigner) Backend
101
+ │ │ │
102
+ │── prepare unsigned tx ──────────────>│ │
103
+ │ │── sign with key │
104
+ │<─────────────────────── signed tx ───│ │
105
+ │ │
106
+ │── broadcast to network ──────────────────────────────────────>│
107
+ ```
108
+
109
+ ## Private Key Format
110
+
111
+ All functions accept the private key as a hex string (with or without `0x` prefix), except:
112
+ - **Solana**: accepts hex or base58 encoded private key
113
+ - **TRON**: accepts hex string
114
+
115
+ ## License
116
+
117
+ MIT
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@chainberry/berry-signer",
3
+ "version": "1.0.0",
4
+ "description": "Pure signing library for multiple blockchain networks — EVM, BTC, LTC, SOL, TRX, XRP, TON",
5
+ "keywords": ["blockchain", "signing", "wallet", "ethereum", "bitcoin", "solana", "tron", "ton", "xrp", "litecoin"],
6
+ "license": "MIT",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ "./package.json": "./package.json",
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "peerDependencies": {
19
+ "bitcoinjs-lib": ">=6.0.0",
20
+ "ecpair": ">=2.0.0",
21
+ "tiny-secp256k1": ">=2.0.0",
22
+ "ethers": ">=6.0.0",
23
+ "@solana/web3.js": ">=1.0.0",
24
+ "tronweb": ">=5.0.0",
25
+ "xrpl": ">=2.0.0",
26
+ "@ton/core": ">=0.50.0",
27
+ "@ton/crypto": ">=3.0.0",
28
+ "@ton/ton": ">=13.0.0"
29
+ },
30
+ "peerDependenciesMeta": {
31
+ "bitcoinjs-lib": { "optional": true },
32
+ "ecpair": { "optional": true },
33
+ "tiny-secp256k1": { "optional": true },
34
+ "ethers": { "optional": true },
35
+ "@solana/web3.js": { "optional": true },
36
+ "tronweb": { "optional": true },
37
+ "xrpl": { "optional": true },
38
+ "@ton/core": { "optional": true },
39
+ "@ton/crypto": { "optional": true },
40
+ "@ton/ton": { "optional": true }
41
+ },
42
+ "dependencies": {
43
+ "tslib": "2.8.1"
44
+ }
45
+ }
@@ -0,0 +1,22 @@
1
+ import * as bitcoin from "bitcoinjs-lib";
2
+ import ECPairFactory from "ecpair";
3
+ import * as ecc from "tiny-secp256k1";
4
+ import { parsePrivateKey } from "../utxo/parse-private-key";
5
+
6
+ /**
7
+ * Pure signing — no network calls. `unsignedPsbtBase64` comes from
8
+ * wallet-broadcast's prepareBtcTransaction (a read-only call that fetches
9
+ * UTXOs/fee rate). Signs every input, finalizes, and returns the raw tx hex
10
+ * ready for broadcast.
11
+ */
12
+ export function signBtcTransaction(privateKeyHex: string, unsignedPsbtBase64: string, isMainnet: boolean): string {
13
+ const network = isMainnet ? bitcoin.networks.bitcoin : bitcoin.networks.testnet;
14
+ const ECPair = ECPairFactory(ecc);
15
+ const keyPair = ECPair.fromPrivateKey(parsePrivateKey(privateKeyHex), { network });
16
+
17
+ const psbt = bitcoin.Psbt.fromBase64(unsignedPsbtBase64, { network });
18
+ for (let i = 0; i < psbt.inputCount; i++) psbt.signInput(i, keyPair);
19
+ psbt.finalizeAllInputs();
20
+
21
+ return psbt.extractTransaction().toHex();
22
+ }
@@ -0,0 +1,43 @@
1
+ import { ethers } from "ethers";
2
+
3
+ export type EvmUnsignedTx = {
4
+ to: string;
5
+ chainId: number;
6
+ nonce: number;
7
+ gasLimit: string;
8
+ value?: string;
9
+ data?: string;
10
+ gasPrice?: string;
11
+ maxFeePerGas?: string;
12
+ maxPriorityFeePerGas?: string;
13
+ };
14
+
15
+ function formatPrivateKey(key: string): string {
16
+ return key.startsWith("0x") ? key : `0x${key}`;
17
+ }
18
+
19
+ /**
20
+ * Pure signing — no network calls, no key storage/decryption.
21
+ * Caller (frontend or backend) supplies the raw private key directly.
22
+ * `unsignedTx` must already carry nonce/gas/chainId, since those require
23
+ * a prior read-only RPC call (see wallet-broadcast's prepare helpers).
24
+ */
25
+ export function signEvmTransaction(privateKeyHex: string, unsignedTx: EvmUnsignedTx): Promise<string> {
26
+ const wallet = new ethers.Wallet(formatPrivateKey(privateKeyHex));
27
+ const txRequest: ethers.TransactionRequest = {
28
+ to: unsignedTx.to,
29
+ chainId: unsignedTx.chainId,
30
+ nonce: unsignedTx.nonce,
31
+ gasLimit: BigInt(unsignedTx.gasLimit),
32
+ value: unsignedTx.value !== undefined ? BigInt(unsignedTx.value) : undefined,
33
+ data: unsignedTx.data,
34
+ };
35
+ if (unsignedTx.maxFeePerGas !== undefined) {
36
+ txRequest.maxFeePerGas = BigInt(unsignedTx.maxFeePerGas);
37
+ txRequest.maxPriorityFeePerGas =
38
+ unsignedTx.maxPriorityFeePerGas !== undefined ? BigInt(unsignedTx.maxPriorityFeePerGas) : undefined;
39
+ } else if (unsignedTx.gasPrice !== undefined) {
40
+ txRequest.gasPrice = BigInt(unsignedTx.gasPrice);
41
+ }
42
+ return wallet.signTransaction(txRequest);
43
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from "./evm/evm-sign";
2
+ export * from "./xrp/xrp-sign";
3
+ export * from "./sol/sol-sign";
4
+ export * from "./trx/trx-sign";
5
+ export * from "./btc/btc-sign";
6
+ export * from "./ltc/ltc-sign";
7
+ export * from "./ton/ton-sign";
@@ -0,0 +1,53 @@
1
+ import * as bitcoin from "bitcoinjs-lib";
2
+ import ECPairFactory from "ecpair";
3
+ import * as ecc from "tiny-secp256k1";
4
+ import { parsePrivateKey } from "../utxo/parse-private-key";
5
+
6
+ const LITECOIN_MAINNET: bitcoin.Network = {
7
+ messagePrefix: "\x19Litecoin Signed Message:\n",
8
+ bech32: "ltc",
9
+ bip32: { public: 0x019da462, private: 0x019d9cfe },
10
+ pubKeyHash: 0x30,
11
+ scriptHash: 0x32,
12
+ wif: 0xb0,
13
+ };
14
+ const LITECOIN_TESTNET: bitcoin.Network = {
15
+ messagePrefix: "\x19Litecoin Signed Message:\n",
16
+ bech32: "tltc",
17
+ bip32: { public: 0x0436f6e1, private: 0x0436ef7d },
18
+ pubKeyHash: 0x6f,
19
+ scriptHash: 0xc4,
20
+ wif: 0xef,
21
+ };
22
+
23
+ /**
24
+ * Pure signing — no network calls. `unsignedPsbtBase64` comes from
25
+ * wallet-broadcast's prepareLtcTransaction (a read-only call that fetches
26
+ * UTXOs/fee rate). Signs every input, finalizes, and returns the raw tx hex
27
+ * ready for broadcast.
28
+ */
29
+ export function signLtcTransaction(privateKeyHex: string, unsignedPsbtBase64: string, isMainnet: boolean): string {
30
+ const network = isMainnet ? LITECOIN_MAINNET : LITECOIN_TESTNET;
31
+ const ECPair = ECPairFactory(ecc);
32
+ const keyPair = ECPair.fromPrivateKey(parsePrivateKey(privateKeyHex), { network });
33
+
34
+ const psbt = bitcoin.Psbt.fromBase64(unsignedPsbtBase64, { network });
35
+ for (let i = 0; i < psbt.inputCount; i++) psbt.signInput(i, keyPair);
36
+ psbt.finalizeAllInputs();
37
+
38
+ return psbt.extractTransaction().toHex();
39
+ }
40
+
41
+ /**
42
+ * Derives the P2WPKH address for a raw private key. WalletCore (TrustWallet) has no
43
+ * native Litecoin testnet support, so this is the one place that still needs bitcoinjs-lib
44
+ * for address derivation — kept here instead of duplicated at call sites.
45
+ */
46
+ export function getLtcAddressFromPrivateKey(privateKeyHex: string, isMainnet: boolean): string {
47
+ const network = isMainnet ? LITECOIN_MAINNET : LITECOIN_TESTNET;
48
+ const ECPair = ECPairFactory(ecc);
49
+ const keyPair = ECPair.fromPrivateKey(parsePrivateKey(privateKeyHex), { network });
50
+ const address = bitcoin.payments.p2wpkh({ pubkey: keyPair.publicKey, network }).address;
51
+ if (!address) throw new Error("Failed to derive LTC address");
52
+ return address;
53
+ }
@@ -0,0 +1,31 @@
1
+ import bs58 from "bs58";
2
+ import { Keypair, Transaction } from "@solana/web3.js";
3
+
4
+ function getSolKeypairFromPrivateKey(privateKey: string): Keypair {
5
+ const cleanedKey = privateKey.startsWith("0x") ? privateKey.slice(2) : privateKey;
6
+ let secretKey: Uint8Array;
7
+ if (/^[0-9a-fA-F]+$/.test(cleanedKey)) {
8
+ secretKey = Uint8Array.from(Buffer.from(cleanedKey, "hex"));
9
+ } else {
10
+ try {
11
+ secretKey = bs58.decode(cleanedKey);
12
+ } catch (error) {
13
+ throw new Error(`Invalid private key encoding: not valid hex or base58 (${(error as Error).message})`);
14
+ }
15
+ }
16
+ if (secretKey.length === 64) return Keypair.fromSecretKey(secretKey);
17
+ if (secretKey.length === 32) return Keypair.fromSeed(secretKey);
18
+ throw new Error(`Invalid private key length: ${secretKey.length} bytes. Expected 32 or 64 bytes.`);
19
+ }
20
+
21
+ /**
22
+ * Pure signing — no network calls. `unsignedTxBase64` must already carry
23
+ * recentBlockhash/feePayer/instructions (see wallet-broadcast's
24
+ * prepareSolTransaction, a read-only RPC call).
25
+ */
26
+ export function signSolTransaction(privateKey: string, unsignedTxBase64: string): string {
27
+ const keypair = getSolKeypairFromPrivateKey(privateKey);
28
+ const transaction = Transaction.from(Buffer.from(unsignedTxBase64, "base64"));
29
+ transaction.sign(keypair);
30
+ return transaction.serialize().toString("base64");
31
+ }
@@ -0,0 +1,38 @@
1
+ import { external, storeMessage } from "@ton/core";
2
+ import { keyPairFromSeed } from "@ton/crypto";
3
+ import { Address, beginCell, Cell, internal, toNano, WalletContractV4 } from "@ton/ton";
4
+
5
+ function getKeyPairFromPrivateKey(privateKeyHex: string): { publicKey: Buffer; secretKey: Buffer } {
6
+ const raw = privateKeyHex.replace(/^0x/i, "");
7
+ if (raw.length !== 64) throw new Error(`Invalid TON private key length: ${raw.length} hex chars. Expected 64 (32 bytes).`);
8
+ return keyPairFromSeed(Buffer.from(raw, "hex"));
9
+ }
10
+
11
+ export type SignedTonTx = { bocBase64: string; txHash: string };
12
+
13
+ /**
14
+ * Pure signing — no network calls. `seqno` comes from wallet-broadcast's
15
+ * fetchTonSeqno (a read-only RPC call).
16
+ */
17
+ export function signTonTransaction(
18
+ privateKeyHex: string,
19
+ params: { toAddress: string; amount: string; seqno: number; memoId?: string }
20
+ ): SignedTonTx {
21
+ const keyPair = getKeyPairFromPrivateKey(privateKeyHex);
22
+ const wallet = WalletContractV4.create({ workchain: 0, publicKey: keyPair.publicKey });
23
+ const walletAddress = wallet.address;
24
+
25
+ const recipientAddress = Address.parse(params.toAddress.trim());
26
+ const amountInNano = toNano(params.amount);
27
+ let body: Cell | undefined;
28
+ if (params.memoId) {
29
+ body = beginCell().storeUint(0, 32).storeStringTail(params.memoId).endCell();
30
+ }
31
+
32
+ const internalMessage = internal({ to: recipientAddress, value: amountInNano, body, bounce: false });
33
+ const transferBody = wallet.createTransfer({ secretKey: keyPair.secretKey, messages: [internalMessage], seqno: params.seqno });
34
+ const extMessage = external({ to: walletAddress, init: params.seqno === 0 ? wallet.init : undefined, body: transferBody });
35
+ const bocBytes = beginCell().store(storeMessage(extMessage)).endCell().toBoc();
36
+
37
+ return { bocBase64: Buffer.from(bocBytes).toString("base64"), txHash: transferBody.hash().toString("hex") };
38
+ }
@@ -0,0 +1,17 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
2
+ let TronWeb = require("tronweb");
3
+ if (TronWeb.TronWeb) TronWeb = TronWeb.TronWeb;
4
+ else if (TronWeb.default) TronWeb = TronWeb.default;
5
+
6
+ /**
7
+ * Pure signing — TronWeb's signing call itself makes no network request, but its
8
+ * constructor still requires node URLs (an SDK quirk, not an actual dependency for
9
+ * signing). `rpcUrl` can be any reachable TRON node; it is never called during sign.
10
+ * `unsignedTx` comes from wallet-broadcast's prepareTrxTransaction (a read-only call).
11
+ */
12
+ export async function signTrxTransaction(privateKey: string, unsignedTx: unknown, rpcUrl: string): Promise<string> {
13
+ const cleanPrivateKey = privateKey.startsWith("0x") ? privateKey.slice(2) : privateKey;
14
+ const tronWeb = new TronWeb({ fullNode: rpcUrl, solidityNode: rpcUrl, eventServer: rpcUrl, privateKey: cleanPrivateKey });
15
+ const signedTx = await tronWeb.trx.sign(unsignedTx, cleanPrivateKey);
16
+ return JSON.stringify(signedTx);
17
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Decodes a raw UTXO-chain private key from either a 32-byte hex string or the
3
+ * legacy hex-of-UTF8-bytes-of-comma-separated-decimal format used by older wallets.
4
+ */
5
+ export function parsePrivateKey(decryptedKey: string, coinLabel = "UTXO"): Buffer {
6
+ const raw = Buffer.from(decryptedKey.replace(/^0x/, ""), "hex");
7
+ if (raw.length === 32) return raw;
8
+ const asString = raw.toString("utf8");
9
+ const parts = asString.split(",").map(Number);
10
+ if (parts.length === 32 && parts.every((b) => Number.isInteger(b) && b >= 0 && b <= 255)) {
11
+ return Buffer.from(parts);
12
+ }
13
+ throw new Error(`Invalid ${coinLabel} private key: expected 32 bytes, got ${raw.length}`);
14
+ }
@@ -0,0 +1,21 @@
1
+ import * as ecc from "tiny-secp256k1";
2
+ import { Payment, Wallet as XrplWallet } from "xrpl";
3
+
4
+ function buildXrplWallet(privateKeyHex: string): XrplWallet {
5
+ const raw = privateKeyHex.replace(/^0x/i, "");
6
+ const pubKeyBuf = ecc.pointFromScalar(Buffer.from(raw, "hex"), true);
7
+ if (!pubKeyBuf) throw new Error("Failed to derive XRP public key from private key");
8
+ const pubKeyHex = Buffer.from(pubKeyBuf).toString("hex").toUpperCase();
9
+ const prefixedPriv = raw.startsWith("00") ? raw : `00${raw}`;
10
+ return new XrplWallet(pubKeyHex, prefixedPriv);
11
+ }
12
+
13
+ /**
14
+ * Pure signing — no network calls. `preparedTx` must already carry
15
+ * Sequence/Fee/LastLedgerSequence (xrpl's autofill, a read-only RPC call —
16
+ * see wallet-broadcast's prepareXrpTransaction).
17
+ */
18
+ export function signXrpTransaction(privateKeyHex: string, preparedTx: Payment): string {
19
+ const wallet = buildXrplWallet(privateKeyHex);
20
+ return wallet.sign(preparedTx).tx_blob;
21
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "declarationDir": "dist",
6
+ "module": "CommonJS",
7
+ "moduleResolution": "Node10"
8
+ },
9
+ "include": ["src"],
10
+ "references": []
11
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "baseUrl": "../../",
5
+ "rootDir": "src",
6
+ "outDir": "dist",
7
+ "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
8
+ "emitDeclarationOnly": false,
9
+ "module": "CommonJS",
10
+ "moduleResolution": "Node10",
11
+ "types": ["node"]
12
+ },
13
+ "include": ["src/**/*.ts"],
14
+ "references": []
15
+ }