@chainberry/chainberry-wallet
v1.0.4
Published
Blockchain wallet provider library — sign, broadcast, balance, fee estimation for EVM, BTC, LTC, BCH, SOL, TRX, XRP, TON
Readme
@chainberry/chainberry-wallet
Pure TypeScript wallet provider library — no NestJS dependency. Handles EVM (ETH/POL/BNB), Solana, TRON, TON, and Bitcoin chain operations with a unified provider interface.
Setup
Add to your app's tsconfig.app.json references:
{
"path": "../../libs/chainberry-wallet/tsconfig.lib.json"
}Core concepts
IWalletChainProvider— common interface for all chains (send, estimateFee, getBalances)WalletProviderRegistry— routes(token, network)to the correct provider, no if-chainsCredentialUtils— hybrid AES-256-GCM encrypt/decrypt; takesIKeyProviderto abstract key storageIKeyProvider— abstraction over key wrapping (AWS KMS, RSA, or any other backend)
Minimal setup (no framework)
import {
CredentialUtils,
EvmChainProvider,
WalletProviderRegistry,
type IKeyProvider,
type WalletConfig,
} from "@chainberry/chainberry-wallet";
import { constants, privateDecrypt, publicEncrypt } from "crypto";
import fs from "fs";
// 1. Build a key provider (RSA example — swap for AWS KMS in production)
const privateKeyPem = fs.readFileSync("private.pem", "utf-8");
const publicKeyPem = fs.readFileSync("public.pem", "utf-8");
const rsaOpts = { padding: constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256" } as const;
const keyProvider: IKeyProvider = {
wrapKey: (aesKey) => Promise.resolve(publicEncrypt({ key: publicKeyPem, ...rsaOpts }, aesKey).toString("base64")),
unwrapKey: (wrappedKey) => Promise.resolve(privateDecrypt({ key: privateKeyPem, ...rsaOpts }, Buffer.from(wrappedKey, "base64"))),
};
// 2. Build credential utils
const credentialUtils = new CredentialUtils(keyProvider);
// 3. Config (all RPC URL fields optional — fall back to public endpoints)
const config: WalletConfig = {
WALLET_ENV: "testnet",
ETH_RPC_URL: "https://my-rpc.example.com",
};
// 4. Create providers and registry
const registry = new WalletProviderRegistry([
new EvmChainProvider(config, credentialUtils),
]);Encrypt a private key (wallet creation)
CredentialUtils.hybridEncrypt wraps a raw private key into the WalletData format expected by providers:
const kmsKeyId = "my-kms-key-id"; // or any string identifier for the key pair
const encrypted = await credentialUtils.hybridEncrypt(rawPrivateKeyHex, kmsKeyId);
// encrypted = { ciphertext, iv, tag, wrappedKey, kmsKeyId }
// Store encrypted in DB as WalletData:
const walletData = {
address: "0xYourAddress",
encryptedSecret: encrypted.ciphertext,
iv: encrypted.iv,
tag: encrypted.tag,
wrappedKey: encrypted.wrappedKey,
kmsKeyId: encrypted.kmsKeyId,
};Send a transaction
const provider = registry.getProviderForSend("ETH", "ETH");
if (!provider) throw new Error("No provider for ETH on ETH");
// walletData comes from your database (encrypted at creation time)
const walletData = {
address: "0xSenderAddress",
encryptedSecret: "<base64 ciphertext>",
iv: "<base64>",
tag: "<base64>",
wrappedKey: "<base64>",
kmsKeyId: "my-kms-key-id",
};
const txHash = await provider.send({
token: "ETH",
network: "ETH",
toAddress: "0xRecipientAddress",
amount: "0.05", // in ETH (human-readable)
wallet: walletData,
});
console.log("tx:", txHash);
// "0xabc123..."
// Same pattern for USDC on Polygon:
const usdcHash = await provider.send({
token: "USDC",
network: "POL",
toAddress: "0xRecipientAddress",
amount: "10.00",
wallet: walletData,
});Estimate fee before sending
const fee = await provider.estimateFee({
token: "ETH",
network: "ETH",
toAddress: "0xRecipientAddress",
amount: "0.05",
wallet: walletData,
});
console.log(`Fee: ${fee.estimatedFee} ${fee.feeToken} (${fee.estimatedFeeInBaseUnit} ${fee.baseUnit})`);
// "Fee: 0.000021 ETH (21000000000000 wei)"Get balances
const evmProvider = registry.getProviderByNetwork("ETH")!;
const balances = await evmProvider.getBalances({
network: "ETH",
address: "0xYourAddress",
tokenFilter: undefined, // omit to get all tokens (ETH + USDC + USDT)
});
// [
// { token: "ETH", balance: "1.234", balanceInBaseUnit: "1234000000000000000", baseUnit: "wei", decimals: "18" },
// { token: "USDC", balance: "50.0", balanceInBaseUnit: "50000000", baseUnit: "tokenUnits", decimals: "6" },
// { token: "USDT", balance: "0.0", balanceInBaseUnit: "0", baseUnit: "tokenUnits", decimals: "6" },
// ]All providers and supported networks
import {
BtcChainProvider,
EvmChainProvider,
SolChainProvider,
TonChainProvider,
TrxChainProvider,
} from "@chainberry/chainberry-wallet";
const registry = new WalletProviderRegistry([
new EvmChainProvider(config, credentialUtils), // ETH, POL, BNB + USDC/USDT
new SolChainProvider(config, credentialUtils), // SOL + USDC/USDT
new TrxChainProvider(config, credentialUtils), // TRX + USDT
new TonChainProvider(config, credentialUtils), // TON
new BtcChainProvider(config, credentialUtils), // BTC
]);
registry.getAllNetworks();
// ["ETH", "POL", "BNB", "SOL", "SOL Devnet", "TRX", "TON", "BTC"]WalletConfig defaults
All RPC URL fields are optional. Omitting them uses public endpoints (suitable for testnet/development):
| Key | Default |
|---|---|
| WALLET_ENV | "testnet" |
| ETH_RPC_URL | https://ethereum-rpc.publicnode.com |
| POL_RPC_URL | https://polygon-mainnet.gateway.tenderly.co |
| BNB_RPC_URL | https://bsc.drpc.org |
| SOL_RPC_URL | https://api.mainnet-beta.solana.com |
| TRX_RPC_URL | https://api.trongrid.io |
| TON_RPC_URL | https://toncenter.com/api/v2 |
| TATUM_API_KEY | (required for BTC) |
AWS KMS key provider
import { KMSClient, EncryptCommand, DecryptCommand } from "@aws-sdk/client-kms";
import type { IKeyProvider } from "@chainberry/chainberry-wallet";
const kms = new KMSClient({ region: "eu-central-1" });
const kmsKeyProvider: IKeyProvider = {
async wrapKey(aesKey, keyId) {
const { CiphertextBlob } = await kms.send(new EncryptCommand({ KeyId: keyId, Plaintext: aesKey }));
return Buffer.from(CiphertextBlob!).toString("base64");
},
async unwrapKey(wrappedKey, keyId) {
const { Plaintext } = await kms.send(new DecryptCommand({
KeyId: keyId,
CiphertextBlob: Buffer.from(wrappedKey, "base64"),
}));
return Buffer.from(Plaintext!);
},
};NestJS integration
In NestJS wrap each provider with @Injectable() and resolve config from ConfigService. See the vault adapter pattern for the full reference implementation already wired in this monorepo.
React Native — private key in device secure store
Required polyfills
CredentialUtils uses Node's built-in crypto module (createCipheriv, randomBytes, etc.) and Buffer — neither exists in React Native's JS engine out of the box.
Expo Go not supported —
react-native-quick-cryptocontains native code. You must use a development build (npx expo run:ios/npx expo run:androidor EAS Build).
npx expo install react-native-quick-crypto bufferapp.json — register the config plugin so Expo links the native module:
{
"expo": {
"plugins": ["react-native-quick-crypto"]
}
}metro.config.js — create this file in your project root if it doesn't exist:
const { getDefaultConfig } = require('expo/metro-config');
const config = getDefaultConfig(__dirname);
config.resolver.extraNodeModules = {
...config.resolver.extraNodeModules,
crypto: require.resolve('react-native-quick-crypto'),
buffer: require.resolve('buffer'),
};
module.exports = config;App entry point (index.js / App.tsx, before any other import):
import { install } from 'react-native-quick-crypto';
install(); // replaces global crypto with native implementation
import { Buffer } from 'buffer';
global.Buffer = global.Buffer ?? Buffer;Without these steps CredentialUtils will throw Cannot find module 'crypto' at runtime.
npx expo install react-native-keychainImplement IKeyProvider so the ephemeral AES-256 key never leaves hardware-backed storage (iOS Keychain / Android Keystore). The actual private key stays AES-encrypted in WalletData; only the wrapping key touches the device secure store.
import * as Keychain from 'react-native-keychain';
import {
CredentialUtils,
EvmChainProvider,
WalletProviderRegistry,
type IKeyProvider,
type WalletConfig,
} from '@chainberry/chainberry-wallet';
const keychainKeyProvider: IKeyProvider = {
async wrapKey(aesKey: Buffer, keyId: string): Promise<string> {
await Keychain.setGenericPassword(keyId, aesKey.toString('base64'), {
service: keyId,
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});
// "wrappedKey" stored in WalletData is the service name used for retrieval
return keyId;
},
async unwrapKey(_wrappedKey: string, keyId: string): Promise<Buffer> {
const credentials = await Keychain.getGenericPassword({ service: keyId });
if (!credentials) throw new Error(`No key in keychain for service: ${keyId}`);
return Buffer.from(credentials.password, 'base64');
},
};
const credentialUtils = new CredentialUtils(keychainKeyProvider);
const config: WalletConfig = { WALLET_ENV: 'mainnet' };
const registry = new WalletProviderRegistry([
new EvmChainProvider(config, credentialUtils),
// add more providers as needed
]);Key generation
hybridEncrypt(plaintext, keyId) expects the private key as a UTF-8 string. Each chain has its own format:
| Chain | @trustwallet/wallet-core | Chain-specific lib | Expected string format |
|---|---|---|---|
| EVM (ETH/POL/BNB) | hdWallet.getKeyForCoin(CoinType.ethereum).data() | ethers.Wallet.createRandom().privateKey | hex, 0x prefix OK |
| SOL | hdWallet.getKeyForCoin(CoinType.solana).data() | Keypair.generate().secretKey | hex, 32 or 64 bytes |
| TRX | hdWallet.getKeyForCoin(CoinType.tron).data() | TronWeb.createAccount().privateKey | hex, no 0x |
| TON | hdWallet.getKeyForCoin(CoinType.ton).data() | mnemonicToPrivateKey(...) | exactly 64 hex chars (32-byte seed) |
| BTC | hdWallet.getKeyForCoin(CoinType.bitcoin).data() | ECPair.makeRandom() | hex string |
TrustWallet Core (used by the vault, covers all chains) returns raw bytes — convert uniformly:
import { initWasm } from '@trustwallet/wallet-core';
const { HDWallet, CoinType } = await initWasm();
const hdWallet = HDWallet.create(128, ''); // 128-bit entropy = 12-word mnemonic
// EVM
const evmKey = hdWallet.getKeyForCoin(CoinType.ethereum);
const rawPrivateKeyHex = '0x' + Buffer.from(evmKey.data()).toString('hex');
const address = hdWallet.getAddressForCoin(CoinType.ethereum);For EVM-only apps, ethers.js is a lighter alternative:
import { Wallet } from 'ethers';
const wallet = Wallet.createRandom();
const rawPrivateKeyHex = wallet.privateKey; // "0x3a4b..." — 66 chars
const address = wallet.address;Encrypt on creation, send on use
// On wallet creation — encrypt private key; AES wrapping key goes to device keychain
const keyId = `wallet-${address}`; // unique per wallet — use address or a UUID, NOT userId (one user can have many wallets)
const encrypted = await credentialUtils.hybridEncrypt(rawPrivateKeyHex, keyId);
// Store { ciphertext, iv, tag, wrappedKey, kmsKeyId } in AsyncStorage or your DB
// wrappedKey here is just the keyId string (the keychain service name)
// On send — keychain is read automatically via unwrapKey inside the provider
const txHash = await registry.getProviderForSend('ETH', 'ETH')!.send({
token: 'ETH', network: 'ETH',
toAddress: '0xRecipient', amount: '0.05',
wallet: { address, ...encrypted },
});Biometric protection (optional)
Add accessControl to require Face ID / fingerprint before the AES key can be retrieved:
// wrapKey — add accessControl when storing
await Keychain.setGenericPassword(keyId, aesKey.toString('base64'), {
service: keyId,
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY_OR_DEVICE_PASSCODE,
});
// unwrapKey — prompt appears automatically on retrieval
const credentials = await Keychain.getGenericPassword({
service: keyId,
authenticationPrompt: { title: 'Authenticate to sign transaction' },
});Custom key provider (testing)
Any object satisfying IKeyProvider works:
import type { IKeyProvider } from "@chainberry/chainberry-wallet";
export const inMemoryKeyProvider: IKeyProvider = {
wrapKey: async (aesKey) => aesKey.toString("hex"),
unwrapKey: async (wrappedKey) => Buffer.from(wrappedKey, "hex"),
};Logger
All providers accept an optional ILogger as their third constructor argument. Any object with debug / warn / error / log methods works (NestJS Logger, winston, pino, etc.):
import { Logger } from "@nestjs/common";
const provider = new EvmChainProvider(config, credentialUtils, new Logger("EVM"));