@custodify/sdk
v0.1.0
Published
TypeScript gRPC client SDK for Custodify services
Readme
@custodify/sdk
TypeScript gRPC client SDK for Custodify blockchain services — scanner, balance, fee, and transaction.
Built on nice-grpc for native async/await and AsyncIterable streaming.
Table of Contents
Installation
npm install @custodify/sdkQuick Start
import { CustodifyClient } from '@custodify/sdk';
const client = new CustodifyClient('grpc.custodify.io:443', {
credentials: ChannelCredentials.createSsl(),
});
// Unary call
const info = await client.scanner.getScannerInfo({ blockchain: 'tron' });
console.log('Latest block:', info.currentBlock);
// Server-streaming
for await (const transfer of client.scanner.streamTransfers({ blockchain: 'tron' })) {
console.log(transfer.txHash, transfer.amount, transfer.toAddress);
}
client.close();Services
Scanner
Streams live or historical token/native transfers from the blockchain.
// Stream live transfers (runs indefinitely until cancelled)
for await (const transfer of client.scanner.streamTransfers({ blockchain: 'eth' })) {
console.log(`[${transfer.blockchain}] ${transfer.fromAddress} → ${transfer.toAddress}`);
console.log(` amount: ${transfer.amount}, token: ${transfer.tokenAddress || 'native'}`);
}
// Stream historical transfers starting at block 20_000_000,
// then automatically continue live once caught up
for await (const transfer of client.scanner.streamHistoricalTransfers({
blockchain: 'eth',
fromBlock: 20_000_000n,
toBlock: 0n, // 0 = catch up and keep streaming
})) {
console.log(transfer.txHash, transfer.blockNumber);
}
// Get current scanner state
const info = await client.scanner.getScannerInfo({ blockchain: 'solana' });
console.log('Current block:', info.currentBlock);
console.log('Oldest stored block:', info.oldestBlock);
console.log('Last scanned block:', info.lastScannedBlock);ParsedTransfer fields:
| Field | Type | Description |
|---|---|---|
| txHash | string | Transaction hash |
| blockchain | string | "tron" | "eth" | "bsc" | "solana" | "bitcoin" |
| blockNumber | bigint | Block number |
| timestampMs | bigint | Unix timestamp in milliseconds |
| fromAddress | string | Sender address |
| toAddress | string | Recipient address |
| amount | string | Amount in smallest units (wei / sun / lamports / satoshi) |
| tokenAddress | string | Token contract / mint address. Empty = native transfer |
| status | TxStatus | TX_STATUS_SUCCESS | TX_STATUS_FAILED |
| feeAmount | string | Fee in native smallest units |
| metadata | Record<string, string> | Chain-specific extras (gas_used, energy_used, …) |
Balance
Fetch native or token balances and account info.
// Native balance
const { amountSmallest, blockNumber } = await client.balance.getBalance({
blockchain: 'tron',
address: 'TXyz...',
});
console.log('Balance (sun):', amountSmallest, 'at block', blockNumber);
// TRC-20 / ERC-20 token balance
const tokenBalance = await client.balance.getTokenBalance({
blockchain: 'eth',
ownerAddress: '0xabc...',
tokenAddress: '0xdac17f...', // USDT
});
console.log('USDT (wei):', tokenBalance.amountSmallest);
// Full account info (balance + nonce + chain-specific extras)
const account = await client.balance.getAccountInfo({
blockchain: 'eth',
address: '0xabc...',
});
console.log('ETH balance:', account.nativeBalanceSmallest);
console.log('Nonce:', account.nonceOrSequence);
console.log('Extras:', account.extras); // { gas_price, ... }Fee
Estimate transaction fees before building.
// EVM (Ethereum / BSC) fee estimate
const fee = await client.fee.estimateFee({
blockchain: 'eth',
paramsJson: Buffer.from(
JSON.stringify({ to: '0xabc...', value: '1000000000000000000' }),
),
});
console.log('Total fee (wei):', fee.feeSmallest);
console.log('Breakdown:', fee.breakdown);
// { gas_limit: '21000', gas_price: '...', priority_fee: '...' }
// Tron energy / bandwidth estimate
const tronFee = await client.fee.estimateFee({
blockchain: 'tron',
paramsJson: Buffer.from(JSON.stringify({ contract_address: 'T...', ... })),
});
console.log(tronFee.breakdown);
// { energy_cost, bandwidth_cost, energy_price, bandwidth_price }Transaction
Build, sign, and broadcast transactions. Private keys never leave the client — only unsigned bytes are returned by buildTransaction.
import { ChannelCredentials } from '@grpc/grpc-js';
// 1. Build unsigned transaction
const { unsignedBytes, encoding, hints } = await client.transaction.buildTransaction({
blockchain: 'eth',
paramsJson: Buffer.from(
JSON.stringify({
from: '0xabc...',
to: '0xdef...',
value: '1000000000000000', // 0.001 ETH in wei
}),
),
});
// encoding: "rlp" | "tron_proto" | "solana_message" | "psbt"
// 2. Sign locally (your code, your keys)
const signedBytes = myWallet.sign(unsignedBytes);
// 3. Broadcast
const receipt = await client.transaction.broadcastTransaction({
blockchain: 'eth',
signedBytes,
});
console.log('tx hash:', receipt.txHash, 'status:', receipt.status);
// Fetch existing transaction
const tx = await client.transaction.getTransaction({
blockchain: 'eth',
txHash: '0x...',
});
console.log(tx.status, tx.blockNumber);TLS / Credentials
By default the client uses insecure (plaintext) credentials, suitable for local development or internal networks.
import { ChannelCredentials } from '@grpc/grpc-js';
import { CustodifyClient } from '@custodify/sdk';
// TLS (production)
const client = new CustodifyClient('grpc.custodify.io:443', {
credentials: ChannelCredentials.createSsl(),
});
// Mutual TLS
const mtlsClient = new CustodifyClient('grpc.internal:443', {
credentials: ChannelCredentials.createSsl(
fs.readFileSync('ca.crt'),
fs.readFileSync('client.key'),
fs.readFileSync('client.crt'),
),
});
// Insecure (default, no need to pass options)
const devClient = new CustodifyClient('localhost:50051');Error Handling
nice-grpc throws ClientError with a gRPC status code on RPC failures.
import { ClientError, Status } from 'nice-grpc';
try {
const info = await client.scanner.getScannerInfo({ blockchain: 'unknown' });
} catch (err) {
if (err instanceof ClientError && err.code === Status.NOT_FOUND) {
console.error('Blockchain not found');
} else {
throw err;
}
}To cancel a streaming call use AbortController:
const ac = new AbortController();
setTimeout(() => ac.abort(), 5000); // stop after 5 s
for await (const transfer of client.scanner.streamTransfers(
{ blockchain: 'tron' },
{ signal: ac.signal },
)) {
console.log(transfer.txHash);
}Contributing
Prerequisites
- Node.js ≥ 18
- buf CLI ≥ 1.x
Setup
git clone https://github.com/custodify/custodify-node-sdk.git
cd custodify-node-sdk
npm installRegenerate proto types
Proto definitions are sourced from buf.build/custodify/custodify-scanner.
npm run generate # runs buf generateGenerated files under src/generated/ are committed to the repository so consumers don't need buf installed.
Build & lint
npm run build # tsc → dist/
npm run typecheck # type-check without emit
npm run lint # ESLint
npm run format # Prettier