bitcore-node
Version:
A blockchain indexing node with extended capabilities using bitcore
513 lines (468 loc) • 17.6 kB
text/typescript
import { CryptoRpc } from 'crypto-rpc';
import { ObjectId } from 'mongodb';
import request from 'request';
import { Readable } from 'stream';
import util from 'util';
import { AccountTxRequest, AccountTxResponse } from 'xrpl/dist/npm/models';
import { Ledger } from 'xrpl/dist/npm/models/ledger';
import {
CheckCreate,
Payment,
TransactionMetadata
} from 'xrpl/dist/npm/models/transactions';
import { Node } from 'xrpl/dist/npm/models/transactions/metadata';
import Config from '../../../config';
import logger from '../../../logger';
import { CacheStorage } from '../../../models/cache';
import { ICoin } from '../../../models/coin';
import { WalletAddressStorage } from '../../../models/walletAddress';
import { InternalStateProvider } from '../../../providers/chain-state/internal/internal';
import { Storage } from '../../../services/storage';
import { IBlock } from '../../../types/Block';
import { ChainNetwork } from '../../../types/ChainNetwork';
import {
BroadcastTransactionParams,
GetBalanceForAddressParams,
GetBlockBeforeTimeParams,
GetEstimateSmartFeeParams,
GetWalletBalanceParams,
IChainStateService,
StreamAddressUtxosParams,
StreamTransactionParams,
StreamTransactionsParams
} from '../../../types/namespaces/ChainStateProvider';
import { GetBlockParams } from '../../../types/namespaces/ChainStateProvider';
import { XrpBlockStorage } from '../models/block';
import { AccountTransaction, BlockTransaction, IXrpTransaction, RpcTransaction } from '../types';
import { RippleDbWalletTransactions } from './wallet-tx-transform';
export class RippleStateProvider extends InternalStateProvider implements IChainStateService {
config: any;
static clients: { [network: string]: CryptoRpc } = {};
constructor(public chain: string = 'XRP') {
super(chain, RippleDbWalletTransactions);
this.config = Config.chains[this.chain];
}
async getClient(network: string) {
if (!RippleStateProvider.clients[network]) {
const networkConfig = this.config[network];
const provider = networkConfig.provider;
RippleStateProvider.clients[network] = new CryptoRpc({
chain: this.chain,
host: provider.host,
rpcPort: provider.port,
protocol: provider.protocol
}).get(this.chain);
await RippleStateProvider.clients[network].rpc.connect();
}
try {
if (RippleStateProvider.clients[network].rpc.isConnected()) {
await RippleStateProvider.clients[network].getBlock();
} else {
await RippleStateProvider.clients[network].rpc.connect();
}
} catch (e) {
await RippleStateProvider.clients[network].rpc.connect();
}
return RippleStateProvider.clients[network];
}
async getAccountNonce(network: string, address: string) {
const client = await this.getClient(network);
try {
const info = await client.getAccountInfo({ address });
return info?.account_data?.Sequence;
} catch (err) {
throw err;
}
}
async getAccountFlags(network: string, address: string) {
const client = await this.getClient(network);
try {
const info = await client.getAccountInfo({ address });
return info?.account_flags;
} catch (err) {
throw err;
}
}
async getBalanceForAddress(params: GetBalanceForAddressParams) {
const { chain, network, address } = params;
const lowerAddress = address.toLowerCase();
const cacheKey = `getBalanceForAddress-${chain}-${network}-${lowerAddress}`;
return CacheStorage.getGlobalOrRefresh(
cacheKey,
async () => {
const client = await this.getClient(network);
try {
const balance = await client.getBalance({ address });
const confirmed = Math.round(Number(balance) * 1e6);
return { confirmed, unconfirmed: 0, balance: confirmed };
} catch (e: any) {
if (e?.data?.error_code === 19) {
// Error code for when we have derived an address,
// but the account has not yet been funded
return {
confirmed: 0,
unconfirmed: 0,
balance: 0
};
}
logger.error(`Error getting XRP balance for ${address} on ${network}: ${JSON.stringify(e.data) || e.stack || e.message || e}`);
throw e;
}
},
CacheStorage.Times.Minute
);
}
async getBlock(params: GetBlockParams) {
const client = await this.getClient(params.network);
const isHash = params.blockId && params.blockId.length == 64;
const query = isHash ? { hash: params.blockId } : { index: Number(params.blockId) };
const { ledger } = await client.getBlock(query);
return this.transformLedger(ledger, params.network);
}
async getBlockBeforeTime(params: GetBlockBeforeTimeParams) {
const { chain, network, time = Date.now() } = params;
const date = new Date(Math.min(new Date(time).getTime(), new Date().getTime())); // Date is at the most right now. This prevents excessive loop iterations below.
if (date.toString() == 'Invalid Date') {
throw new Error('Invalid time value');
}
const [block] = await XrpBlockStorage.collection
.find({
chain,
network,
timeNormalized: { $lte: date }
})
.limit(1)
.sort({ timeNormalized: -1 })
.toArray();
if (!block) {
return null;
}
let ledger = await this.getDataHostLedger(block.height, network);
if (!ledger) {
return null;
}
// Check if our DB has gaps. `block` might not be the latest block before `date`
let workingIdx = Number(ledger.ledger_index) + 1; // +1 to check if the next block is < date
ledger = await this.getDataHostLedger(workingIdx, network);
while (ledger && new Date(ledger.close_time_human) < date) {
// a gap exists
workingIdx = Number(ledger.ledger_index) + 1;
const timeGap = date.getTime() - new Date(ledger.close_time_human).getTime();
if (timeGap > 1000 * 60 * 2) { // if more than a 2 min gap...
workingIdx += Math.floor(timeGap / 10000); // ...jump forward assuming a block every 10 seconds
}
ledger = await this.getDataHostLedger(workingIdx, network);
}
// the timeGap above might have overshot
while (!ledger || new Date(ledger.close_time_human) > date) {
// walk it back
workingIdx--;
ledger = await this.getDataHostLedger(workingIdx, network);
}
return this.transformLedger(ledger, network);
}
async getDataHostLedger(index, network) {
const ledger = await util.promisify(request.post).call(request, {
url: this.config[network].provider.dataHost,
json: true,
body: {
method: 'ledger',
params: [
{
ledger_index: index,
transactions: true,
expand: false
}
]
}
});
if (ledger?.body?.result?.status !== 'success') {
return null;
}
return ledger.body.result.ledger;
}
async getFee(params: GetEstimateSmartFeeParams) {
const { chain, network, target } = params;
const cacheKey = `getFee-${chain}-${network}-${target}`;
return CacheStorage.getGlobalOrRefresh(
cacheKey,
async () => {
const client = await this.getClient(network);
const fee = await client.estimateFee();
return { feerate: parseFloat(fee), blocks: target };
},
CacheStorage.Times.Minute
);
}
async broadcastTransaction(params: BroadcastTransactionParams) {
const client = await this.getClient(params.network);
const rawTxs = typeof params.rawTx === 'string' ? [params.rawTx] : params.rawTx;
const txids = new Array<string>();
for (const tx of rawTxs) {
const hash = (await client.sendRawTransaction({ rawTx: tx }));
txids.push(hash);
}
return txids.length === 1 ? txids[0] : txids;
}
async getWalletBalance(params: GetWalletBalanceParams) {
const { chain, network } = params;
const addresses = await this.getWalletAddresses(params.wallet._id!);
const balances = await Promise.all(
addresses.map(a => this.getBalanceForAddress({ address: a.address, chain, network, args: {} }))
);
return balances.reduce(
(total, current) => {
total.balance += current.balance;
total.confirmed += current.confirmed;
total.unconfirmed += current.unconfirmed;
return total;
},
{ confirmed: 0, unconfirmed: 0, balance: 0 }
);
}
streamTxs<T>(txs: Array<T>, stream: Readable) {
for (let tx of txs) {
stream.push(tx);
}
}
async getAddressTransactions(params: StreamAddressUtxosParams) {
const { startTx, limitArg } = params.args;
const client = await this.getClient(params.network);
const serverInfo = await client.getServerInfo();
const ledgers = serverInfo.complete_ledgers.split('-');
const minLedgerIndex = Number(ledgers[0]);
let allTxs: AccountTxResponse['result']['transactions'] = [];
let limit = Number(limitArg) || 100;
const options = {
ledger_index_min: minLedgerIndex,
limit,
binary: false
} as AccountTxRequest;
if (startTx) {
const tx = await client.getTransaction({ txid: startTx });
options.ledger_index_min = Math.max(Number(tx?.ledger_index), minLedgerIndex);
options.forward = true;
}
let txs: AccountTxResponse['result'] = await client.getTransactions({ address: params.address, options });
if (startTx) {
const startTxIdx = txs.transactions.findIndex(tx => tx.tx?.hash === startTx);
if (startTxIdx > -1) {
txs.transactions = txs.transactions.slice(startTxIdx + 1);
}
}
allTxs.push(...txs.transactions);
while (txs.marker) {
txs = await client.getTransactions({ address: params.address, options: {
marker: txs.marker,
limit,
binary: false
}});
allTxs.push(...txs.transactions);
}
return allTxs;
}
async streamAddressTransactions(params: StreamAddressUtxosParams) {
const readable = new Readable({ objectMode: true });
const txs = await this.getAddressTransactions(params);
const transformed = txs.map(tx => this.transformAccountTx(tx, params.network));
this.streamTxs(transformed, readable);
readable.push(null);
Storage.stream(readable, params.req!, params.res!);
}
async streamTransactions(params: StreamTransactionsParams) {
const client = await this.getClient(params.network);
let { blockHash } = params.args;
const { ledger } = await client.getBlock({ hash: blockHash, transactions: true });
const readable = new Readable({ objectMode: true });
const txs = ledger.transactions || [];
this.streamTxs(txs, readable);
readable.push(null);
Storage.stream(readable, params.req, params.res);
}
async getTransaction(params: StreamTransactionParams) {
const client = await this.getClient(params.network);
try {
const tx = await client.getTransaction({ txid: params.txId });
return tx;
} catch (e) {
return undefined;
}
}
async getLocalTip(params: ChainNetwork) {
return XrpBlockStorage.getLocalTip(params);
}
async getCoinsForTx() {
return {
inputs: [],
outputs: []
};
}
transformLedger(ledger: Ledger, network: string): IBlock {
const txs = ledger.transactions || [];
return {
chain: this.chain,
network,
hash: ledger.ledger_hash,
height: Number(ledger.ledger_index),
previousBlockHash: ledger.parent_hash,
processed: ledger.closed,
time: new Date(ledger.close_time_human),
timeNormalized: new Date(ledger.close_time_human),
reward: 0,
size: txs.length,
transactionCount: txs.length,
nextBlockHash: ''
};
}
transform(tx: BlockTransaction | RpcTransaction, network: string, block?: IBlock): IXrpTransaction {
const date = block?.time ? new Date(block?.time) : this.getDateFromRippleTime((tx as RpcTransaction).date) || '';
const metaData: TransactionMetadata = (tx as BlockTransaction).metaData || (tx as RpcTransaction).meta;
const value = metaData.delivered_amount ??
metaData.DeliveredAmount ??
(tx as Payment).Amount ??
0;
return {
network,
chain: this.chain,
txid: tx.hash,
from: tx.Account,
blockHash: block?.hash || '',
blockHeight: block?.height || (tx as RpcTransaction).ledger_index,
blockTime: date,
blockTimeNormalized: date,
value: Number(value),
fee: Number(tx.Fee ?? 0),
nonce: Number(tx.Sequence),
destinationTag: (tx as Payment).DestinationTag,
to: (tx as Payment).Destination,
currency: undefined, // TODO
invoiceID: (tx as CheckCreate).InvoiceID,
wallets: []
} as IXrpTransaction;
}
transformAccountTx(tx: AccountTransaction, network: string): IXrpTransaction {
const date = this.getDateFromRippleTime(tx.tx?.date) || '';
const value = Number((tx.meta as TransactionMetadata).DeliveredAmount ?? (tx.meta as TransactionMetadata).delivered_amount ?? 0);
return {
network,
chain: this.chain,
txid: tx.tx?.hash,
from: tx.tx?.Account,
blockHash: (tx as any).ledger_hash || '', // TODO: ledger_hash is not a property of AccountTransaction
blockHeight: tx.tx?.ledger_index,
blockTime: date,
blockTimeNormalized: date,
value: Number(value),
fee: Number(tx.tx?.Fee ?? 0),
nonce: Number(tx.tx?.Sequence),
destinationTag: (tx.tx as Payment).DestinationTag,
to: (tx.tx as Payment).Destination,
currency: undefined, // TODO
invoiceID: (tx.tx as CheckCreate).InvoiceID,
wallets: []
} as IXrpTransaction;
}
transformToCoins(tx: BlockTransaction | AccountTransaction | RpcTransaction, network: string, block?: IBlock): Array<Partial<ICoin>> {
const coins: Partial<ICoin>[] = [];
const mintTxid = (tx as BlockTransaction).hash || (tx as AccountTransaction).tx?.hash || (tx as RpcTransaction).hash;
const metaData = (tx as BlockTransaction).metaData || (tx as AccountTransaction).meta as TransactionMetadata || (tx as RpcTransaction).meta as TransactionMetadata;
if (!metaData.AffectedNodes?.length) {
return coins;
}
const nodes: Node[] = metaData.AffectedNodes || [];
for (const node of nodes) {
if ('ModifiedNode' in node && node.ModifiedNode.FinalFields) {
const address = node.ModifiedNode.FinalFields.Account;
const value = Number(node.ModifiedNode.FinalFields.Balance) - Number(node.ModifiedNode.PreviousFields?.Balance);
const coin = {
chain: this.chain,
network,
address,
value,
coinbase: false,
mintHeight: block?.height,
mintIndex: nodes.indexOf(node),
mintTxid,
wallets: []
} as Partial<ICoin>;
coins.push(coin);
} else if ('CreatedNode' in node) {
const address = node.CreatedNode.NewFields.Account;
const value = Number(node.CreatedNode.NewFields.Balance);
const coin = {
chain: this.chain,
network,
address,
value,
coinbase: false,
mintHeight: block?.height,
mintIndex: nodes.indexOf(node),
mintTxid,
wallets: []
} as Partial<ICoin>;
coins.push(coin);
} else if ('DeletedNode' in node) {
const address = node.DeletedNode.FinalFields.Account;
const value = -1 * Number(node.DeletedNode.FinalFields.Balance);
const coin = {
chain: this.chain,
network,
address,
value,
coinbase: false,
mintHeight: block?.height,
mintIndex: nodes.indexOf(node),
mintTxid,
wallets: []
} as Partial<ICoin>;
coins.push(coin);
}
}
return coins;
}
async tag(
chain: string,
network: string,
tx: IXrpTransaction,
outputs: Array<ICoin> | any
): Promise<{ transaction: IXrpTransaction; coins: Array<ICoin> }> {
const address = tx.from;
let involvedAddress = [address];
const transaction = { ...tx, wallets: new Array<ObjectId>() };
let coins = new Array<ICoin>();
if (Array.isArray(outputs)) {
coins = outputs.map(c => {
return { ...c, wallets: new Array<ObjectId>() };
});
involvedAddress.push(...coins.map(c => c.address));
}
const walletAddresses = await WalletAddressStorage.collection
.find({ chain, network, address: { $in: involvedAddress } })
.toArray();
if ('chain' in tx) {
transaction.wallets = walletAddresses.map(wa => wa.wallet);
}
if (coins && coins.length) {
for (const coin of coins) {
const coinWalletAddresses = walletAddresses.filter(wa => coin.address && wa.address === coin.address);
if (coinWalletAddresses && coinWalletAddresses.length) {
coin.wallets = coinWalletAddresses.map(wa => wa.wallet);
}
}
}
return { transaction, coins };
}
async getReserve(network: string) {
const client = await this.getClient(network);
const info = await client.getServerInfo();
return info.validated_ledger.reserve_base_xrp * 1e6;
}
getDateFromRippleTime(rippleTime?: number) {
if (rippleTime == null) {
return null;
}
// the ripple epoch is 2000-01-01
return new Date(new Date('2000-01-01').getTime() + rippleTime * 1000);
}
}
export const XRP = new RippleStateProvider();