UNPKG

bitcore-node

Version:

A blockchain indexing node with extended capabilities using bitcore

441 lines 17.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.XRP = exports.RippleStateProvider = void 0; const crypto_rpc_1 = require("crypto-rpc"); const request_1 = __importDefault(require("request")); const stream_1 = require("stream"); const util_1 = __importDefault(require("util")); const config_1 = __importDefault(require("../../../config")); const logger_1 = __importDefault(require("../../../logger")); const cache_1 = require("../../../models/cache"); const walletAddress_1 = require("../../../models/walletAddress"); const internal_1 = require("../../../providers/chain-state/internal/internal"); const storage_1 = require("../../../services/storage"); const block_1 = require("../models/block"); const wallet_tx_transform_1 = require("./wallet-tx-transform"); class RippleStateProvider extends internal_1.InternalStateProvider { constructor(chain = 'XRP') { super(chain, wallet_tx_transform_1.RippleDbWalletTransactions); this.chain = chain; this.config = config_1.default.chains[this.chain]; } async getClient(network) { if (!RippleStateProvider.clients[network]) { const networkConfig = this.config[network]; const provider = networkConfig.provider; RippleStateProvider.clients[network] = new crypto_rpc_1.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, address) { 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, address) { const client = await this.getClient(network); try { const info = await client.getAccountInfo({ address }); return info?.account_flags; } catch (err) { throw err; } } async getBalanceForAddress(params) { const { chain, network, address } = params; const lowerAddress = address.toLowerCase(); const cacheKey = `getBalanceForAddress-${chain}-${network}-${lowerAddress}`; return cache_1.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) { 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_1.default.error(`Error getting XRP balance for ${address} on ${network}: ${JSON.stringify(e.data) || e.stack || e.message || e}`); throw e; } }, cache_1.CacheStorage.Times.Minute); } async getBlock(params) { 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) { 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 block_1.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_1.default.promisify(request_1.default.post).call(request_1.default, { 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) { const { chain, network, target } = params; const cacheKey = `getFee-${chain}-${network}-${target}`; return cache_1.CacheStorage.getGlobalOrRefresh(cacheKey, async () => { const client = await this.getClient(network); const fee = await client.estimateFee(); return { feerate: parseFloat(fee), blocks: target }; }, cache_1.CacheStorage.Times.Minute); } async broadcastTransaction(params) { const client = await this.getClient(params.network); const rawTxs = typeof params.rawTx === 'string' ? [params.rawTx] : params.rawTx; const txids = new Array(); 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) { 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(txs, stream) { for (let tx of txs) { stream.push(tx); } } async getAddressTransactions(params) { 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 = []; let limit = Number(limitArg) || 100; const options = { ledger_index_min: minLedgerIndex, limit, binary: false }; 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 = 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) { const readable = new stream_1.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_1.Storage.stream(readable, params.req, params.res); } async streamTransactions(params) { const client = await this.getClient(params.network); let { blockHash } = params.args; const { ledger } = await client.getBlock({ hash: blockHash, transactions: true }); const readable = new stream_1.Readable({ objectMode: true }); const txs = ledger.transactions || []; this.streamTxs(txs, readable); readable.push(null); storage_1.Storage.stream(readable, params.req, params.res); } async getTransaction(params) { 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) { return block_1.XrpBlockStorage.getLocalTip(params); } async getCoinsForTx() { return { inputs: [], outputs: [] }; } transformLedger(ledger, network) { 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, network, block) { const date = block?.time ? new Date(block?.time) : this.getDateFromRippleTime(tx.date) || ''; const metaData = tx.metaData || tx.meta; const value = metaData.delivered_amount ?? metaData.DeliveredAmount ?? tx.Amount ?? 0; return { network, chain: this.chain, txid: tx.hash, from: tx.Account, blockHash: block?.hash || '', blockHeight: block?.height || tx.ledger_index, blockTime: date, blockTimeNormalized: date, value: Number(value), fee: Number(tx.Fee ?? 0), nonce: Number(tx.Sequence), destinationTag: tx.DestinationTag, to: tx.Destination, currency: undefined, // TODO invoiceID: tx.InvoiceID, wallets: [] }; } transformAccountTx(tx, network) { const date = this.getDateFromRippleTime(tx.tx?.date) || ''; const value = Number(tx.meta.DeliveredAmount ?? tx.meta.delivered_amount ?? 0); return { network, chain: this.chain, txid: tx.tx?.hash, from: tx.tx?.Account, blockHash: tx.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.DestinationTag, to: tx.tx.Destination, currency: undefined, // TODO invoiceID: tx.tx.InvoiceID, wallets: [] }; } transformToCoins(tx, network, block) { const coins = []; const mintTxid = tx.hash || tx.tx?.hash || tx.hash; const metaData = tx.metaData || tx.meta || tx.meta; if (!metaData.AffectedNodes?.length) { return coins; } const nodes = 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: [] }; 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: [] }; 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: [] }; coins.push(coin); } } return coins; } async tag(chain, network, tx, outputs) { const address = tx.from; let involvedAddress = [address]; const transaction = { ...tx, wallets: new Array() }; let coins = new Array(); if (Array.isArray(outputs)) { coins = outputs.map(c => { return { ...c, wallets: new Array() }; }); involvedAddress.push(...coins.map(c => c.address)); } const walletAddresses = await walletAddress_1.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) { const client = await this.getClient(network); const info = await client.getServerInfo(); return info.validated_ledger.reserve_base_xrp * 1e6; } getDateFromRippleTime(rippleTime) { if (rippleTime == null) { return null; } // the ripple epoch is 2000-01-01 return new Date(new Date('2000-01-01').getTime() + rippleTime * 1000); } } exports.RippleStateProvider = RippleStateProvider; RippleStateProvider.clients = {}; exports.XRP = new RippleStateProvider(); //# sourceMappingURL=csp.js.map