import * as _ from 'lodash' import {EventEmitter} from 'events' import {Connection, errors, validate} from './common' import { connect, disconnect, isConnected, getServerInfo, getFee, getLedgerVersion, formatLedgerClose } from './server/server' import getTransaction from './ledger/transaction' import getTransactions from './ledger/transactions' import getTrustlines from './ledger/trustlines' import getBalances from './ledger/balances' import getBalanceSheet from './ledger/balance-sheet' import getPaths from './ledger/pathfind' import getOrders from './ledger/orders' import getOrderbook from './ledger/orderbook' import getSettings from './ledger/settings' import getAccountInfo from './ledger/accountinfo' import getPaymentChannel from './ledger/payment-channel' import preparePayment from './transaction/payment' import prepareTrustline from './transaction/trustline' import prepareOrder from './transaction/order' import prepareOrderCancellation from './transaction/ordercancellation' import prepareEscrowCreation from './transaction/escrow-creation' import prepareEscrowExecution from './transaction/escrow-execution' import prepareEscrowCancellation from './transaction/escrow-cancellation' import preparePaymentChannelCreate from './transaction/payment-channel-create' import preparePaymentChannelFund from './transaction/payment-channel-fund' import preparePaymentChannelClaim from './transaction/payment-channel-claim' import prepareSettings from './transaction/settings' import sign from './transaction/sign' import combine from './transaction/combine' import submit from './transaction/submit' import {generateAddressAPI} from './offline/generate-address' import computeLedgerHash from './offline/ledgerhash' import signPaymentChannelClaim from './offline/sign-payment-channel-claim' import verifyPaymentChannelClaim from './offline/verify-payment-channel-claim' import getLedger from './ledger/ledger' import { AccountOffersRequest, AccountOffersResponse, AccountInfoRequest, AccountInfoResponse, AccountLinesRequest, AccountLinesResponse, BookOffersRequest, BookOffersResponse, GatewayBalancesRequest, GatewayBalancesResponse } from './common/types/commands' import RangeSet from './common/rangeset' import * as ledgerUtils from './ledger/utils' import * as schemaValidator from './common/schema-validator' import {clamp} from './ledger/utils' export type APIOptions = { server?: string, feeCushion?: number, trace?: boolean, proxy?: string, timeout?: number } /** * Get the response key / property name that contains the listed data for a * command. This varies from command to command, but we need to know it to * properly count across many requests. */ function getCollectKeyFromCommand(command: string): string|undefined { switch (command) { case 'account_offers': case 'book_offers': return 'offers' case 'account_lines': return 'lines' default: return undefined } } // prevent access to non-validated ledger versions export class RestrictedConnection extends Connection { request(request: any, timeout?: number) { const ledger_index = request.ledger_index if (ledger_index !== undefined && ledger_index !== 'validated') { if (!_.isNumber(ledger_index) || ledger_index > this._ledgerVersion) { return Promise.reject(new errors.LedgerVersionError( `ledgerVersion ${ledger_index} is greater than server\'s ` + `most recent validated ledger: ${this._ledgerVersion}`)) } } return super.request(request, timeout) } } class RippleAPI extends EventEmitter { _feeCushion: number connection: RestrictedConnection // these are exposed only for use by unit tests; they are not part of the API. static _PRIVATE = { validate: validate, RangeSet, ledgerUtils, schemaValidator } constructor(options: APIOptions = {}) { super() validate.apiOptions(options) this._feeCushion = options.feeCushion || 1.2 const serverURL = options.server if (serverURL !== undefined) { this.connection = new RestrictedConnection(serverURL, options) this.connection.on('ledgerClosed', message => { this.emit('ledger', formatLedgerClose(message)) }) this.connection.on('error', (errorCode, errorMessage, data) => { this.emit('error', errorCode, errorMessage, data) }) this.connection.on('connected', () => { this.emit('connected') }) this.connection.on('disconnected', code => { this.emit('disconnected', code) }) } else { // use null object pattern to provide better error message if user // tries to call a method that requires a connection this.connection = new RestrictedConnection(null, options) } } /** * Makes a simple request to the API with the given command and any * additional request body parameters. * * NOTE: This command is under development and should not yet be relied * on by external consumers. */ async _request(command: 'account_info', params: AccountInfoRequest): Promise async _request(command: 'account_lines', params: AccountLinesRequest): Promise async _request(command: 'account_offers', params: AccountOffersRequest): Promise async _request(command: 'book_offers', params: BookOffersRequest): Promise async _request(command: 'gateway_balances', params: GatewayBalancesRequest): Promise async _request(command: string, params: any = {}) { return this.connection.request({ ...params, command }) } /** * Makes multiple paged requests to the API to return a given number of * resources. __requestAll() will make multiple requests until the `limit` * number of resources is reached (if no `limit` is provided, a single request * will be made). * * If the command is unknown, an additional `collect` property is required to * know which response key contains the array of resources. * * NOTE: This command is under development and should not yet be relied * on by external consumers. */ async _requestAll(command: 'account_offers', params: AccountOffersRequest): Promise async _requestAll(command: 'book_offers', params: BookOffersRequest): Promise async _requestAll(command: 'account_lines', params: AccountLinesRequest): Promise async _requestAll( command: string, params: any = {}, options: {collect?: string} = {}): Promise { // The data under collection is keyed based on the command. Fail if command // not recognized and collection key not provided. const collectKey = options.collect || getCollectKeyFromCommand(command) if (!collectKey) { throw new errors.ValidationError(`no collect key for command ${command}`) } // If limit is not provided, fetches all data over multiple requests. // NOTE: This may return much more than needed. Set limit when possible. const countTo: number = (params.limit !== undefined) ? params.limit : Infinity let count: number = 0 let marker: string = params.marker let lastBatchLength: number const results = [] do { const countRemaining = clamp(countTo - count, 10, 400) const repeatProps = { ...params, limit: countRemaining, marker } // NOTE: We have to generalize the `this._request()` function signature // here until we add support for unknown commands (since command is some // unknown string). const singleResult = await (this._request)(command, repeatProps) const collectedData = singleResult[collectKey] marker = singleResult.marker results.push(singleResult) // Make sure we handle when no data (not even an empty array) is returned. const isExpectedFormat = Array.isArray(collectedData) if (isExpectedFormat) { count += collectedData.length lastBatchLength = collectedData.length } else { lastBatchLength = 0 } } while(!!marker && count < countTo && lastBatchLength !== 0) return results } connect = connect disconnect = disconnect isConnected = isConnected getServerInfo = getServerInfo getFee = getFee getLedgerVersion = getLedgerVersion getTransaction = getTransaction getTransactions = getTransactions getTrustlines = getTrustlines getBalances = getBalances getBalanceSheet = getBalanceSheet getPaths = getPaths getOrders = getOrders getOrderbook = getOrderbook getSettings = getSettings getAccountInfo = getAccountInfo getPaymentChannel = getPaymentChannel getLedger = getLedger preparePayment = preparePayment prepareTrustline = prepareTrustline prepareOrder = prepareOrder prepareOrderCancellation = prepareOrderCancellation prepareEscrowCreation = prepareEscrowCreation prepareEscrowExecution = prepareEscrowExecution prepareEscrowCancellation = prepareEscrowCancellation preparePaymentChannelCreate = preparePaymentChannelCreate preparePaymentChannelFund = preparePaymentChannelFund preparePaymentChannelClaim = preparePaymentChannelClaim prepareSettings = prepareSettings sign = sign combine = combine submit = submit generateAddress = generateAddressAPI computeLedgerHash = computeLedgerHash signPaymentChannelClaim = signPaymentChannelClaim verifyPaymentChannelClaim = verifyPaymentChannelClaim errors = errors } export { RippleAPI }