From 365de6d18a3d5f439c572c9287c2a8de9b43d37b Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Tue, 20 Feb 2018 11:44:36 -0800 Subject: [PATCH] Add new request interface, implement first few request typings (#843) * Add request interface & typings - src/api: add basic implementation of request/requestAll() - src/ledgers/account_info: refactor to simplify with request() - src/ledgers/balances: refactor to simplify with request() - src/ledgers/orderbook: refactor to simplify with requestAll() - src/ledgers/orders: refactor to simplify with requestAll() - src/ledgers/trustlines: refactor to simplify with requestAll() * standardize on Formatted prefix --- src/api.ts | 112 ++++++++++++++++++ src/common/types.ts | 63 ---------- src/common/types/commands/account_info.ts | 36 ++++++ src/common/types/commands/account_lines.ts | 19 +++ src/common/types/commands/account_offers.ts | 27 +++++ src/common/types/commands/book_offers.ts | 30 +++++ src/common/types/commands/gateway_balances.ts | 20 ++++ src/common/types/commands/index.ts | 5 + src/common/types/objects/accounts.ts | 17 +++ src/common/types/objects/adjustments.ts | 19 +++ src/common/types/objects/amounts.ts | 28 +++++ src/common/types/objects/index.ts | 7 ++ src/common/types/objects/memos.ts | 6 + src/common/types/objects/signers.ts | 14 +++ src/common/types/objects/transactions.ts | 22 ++++ src/common/types/objects/trustlines.ts | 42 +++++++ src/common/utils.ts | 2 +- src/ledger/accountinfo.ts | 50 +++----- src/ledger/balance-sheet.ts | 29 ++--- src/ledger/balances.ts | 7 +- src/ledger/orderbook.ts | 85 +++++++------ src/ledger/orders.ts | 58 ++++----- src/ledger/parse/account-order.ts | 3 +- src/ledger/parse/account-trustline.ts | 21 +--- src/ledger/parse/amount.ts | 2 +- src/ledger/parse/pathfind.ts | 2 +- src/ledger/parse/utils.ts | 2 +- src/ledger/pathfind-types.ts | 11 +- src/ledger/pathfind.ts | 2 +- src/ledger/transaction-types.ts | 2 +- src/ledger/trustlines-types.ts | 33 ------ src/ledger/trustlines.ts | 60 ++++------ src/ledger/types.ts | 7 +- src/ledger/utils.ts | 2 +- src/transaction/escrow-cancellation.ts | 2 +- src/transaction/escrow-creation.ts | 2 +- src/transaction/escrow-execution.ts | 2 +- src/transaction/payment.ts | 2 +- src/transaction/settings.ts | 2 +- src/transaction/trustline.ts | 8 +- src/transaction/utils.ts | 8 +- test/api-test.js | 41 ++++--- 42 files changed, 605 insertions(+), 307 deletions(-) delete mode 100644 src/common/types.ts create mode 100644 src/common/types/commands/account_info.ts create mode 100644 src/common/types/commands/account_lines.ts create mode 100644 src/common/types/commands/account_offers.ts create mode 100644 src/common/types/commands/book_offers.ts create mode 100644 src/common/types/commands/gateway_balances.ts create mode 100644 src/common/types/commands/index.ts create mode 100644 src/common/types/objects/accounts.ts create mode 100644 src/common/types/objects/adjustments.ts create mode 100644 src/common/types/objects/amounts.ts create mode 100644 src/common/types/objects/index.ts create mode 100644 src/common/types/objects/memos.ts create mode 100644 src/common/types/objects/signers.ts create mode 100644 src/common/types/objects/transactions.ts create mode 100644 src/common/types/objects/trustlines.ts delete mode 100644 src/ledger/trustlines-types.ts diff --git a/src/api.ts b/src/api.ts index 6c7ea721..58dd3f63 100644 --- a/src/api.ts +++ b/src/api.ts @@ -39,10 +39,19 @@ 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' type APIOptions = { server?: string, @@ -52,6 +61,23 @@ type APIOptions = { 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 class RestrictedConnection extends Connection { request(request: any, timeout?: number) { @@ -106,6 +132,92 @@ class RippleAPI extends EventEmitter { } } + /** + * 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 diff --git a/src/common/types.ts b/src/common/types.ts deleted file mode 100644 index 0633c441..00000000 --- a/src/common/types.ts +++ /dev/null @@ -1,63 +0,0 @@ - -export type RippledAmountIOU = { - currency: string, - value: string, - issuer?: string -} - -export type RippledAmount = string | RippledAmountIOU - - -export type Amount = { - value: string, - currency: string, - issuer?: string, - counterparty?: string -} - - -// Amount where counterparty and value are optional -export type LaxLaxAmount = { - currency: string, - value?: string, - issuer?: string, - counterparty?: string -} - -// A currency-counterparty pair, or just currency if it's XRP -export type Issue = { - currency: string, - issuer?: string, - counterparty?: string -} - -export type Adjustment = { - address: string, - amount: Amount, - tag?: number -} - -export type MaxAdjustment = { - address: string, - maxAmount: Amount, - tag?: number -} - -export type MinAdjustment = { - address: string, - minAmount: Amount, - tag?: number -} - -export type Memo = { - type?: string, - format?: string, - data?: string -} - -export type ApiMemo = { - MemoData?: string, - MemoType?: string, - MemoFormat?: string -} - diff --git a/src/common/types/commands/account_info.ts b/src/common/types/commands/account_info.ts new file mode 100644 index 00000000..17f43904 --- /dev/null +++ b/src/common/types/commands/account_info.ts @@ -0,0 +1,36 @@ +import {AccountRoot, SignerList} from '../objects' + +export interface AccountInfoRequest { + account: string, + strict?: boolean, + queue?: boolean, + ledger_hash?: string, + ledger_index?: number | ('validated' | 'closed' | 'current'), + signer_lists?: boolean +} + +export interface AccountInfoResponse { + account_data: AccountRoot, + signer_lists?: SignerList[], + ledger_current_index?: number, + ledger_index?: number, + queue_data?: QueueData, + validated?: boolean +} + +interface QueueData { + txn_count: number, + auth_change_queued?: boolean, + lowest_sequence?: number, + highest_sequence?: number, + max_spend_drops_total?: string, + transactions?: TransactionData[] +} + +interface TransactionData { + auth_change?: boolean, + fee?: string, + fee_level?: string, + max_spend_drops?: string, + seq?: number +} diff --git a/src/common/types/commands/account_lines.ts b/src/common/types/commands/account_lines.ts new file mode 100644 index 00000000..e380d151 --- /dev/null +++ b/src/common/types/commands/account_lines.ts @@ -0,0 +1,19 @@ +import {Trustline} from '../objects' + +export interface AccountLinesRequest { + account: string, + ledger_hash?: string, + ledger_index?: number | ('validated' | 'closed' | 'current'), + peer?: string, + limit?: number, + marker?: any, +} + +export interface AccountLinesResponse { + account: string, + lines: Trustline[], + ledger_current_index?: number, + ledger_index?: number, + ledger_hash?: string, + marker?: any, +} diff --git a/src/common/types/commands/account_offers.ts b/src/common/types/commands/account_offers.ts new file mode 100644 index 00000000..dc9f8af3 --- /dev/null +++ b/src/common/types/commands/account_offers.ts @@ -0,0 +1,27 @@ +import {RippledAmount} from '../objects' + +export interface AccountOffersRequest { + account: string, + ledger_hash?: string, + ledger_index?: number | ('validated' | 'closed' | 'current'), + limit?: number, + marker?: any, +} + +export interface AccountOffersResponse { + account: string, + ledger_hash?: string, + ledger_current_index?: number, + ledger_index?: number, + marker?: any, + offers?: AccountOffer[], +} + +export interface AccountOffer { + seq: number, + flags: number, + taker_gets: RippledAmount, + taker_pays: RippledAmount, + quality: string, + expiration?: number +} diff --git a/src/common/types/commands/book_offers.ts b/src/common/types/commands/book_offers.ts new file mode 100644 index 00000000..94504e63 --- /dev/null +++ b/src/common/types/commands/book_offers.ts @@ -0,0 +1,30 @@ +import { + TakerRequestAmount, + OfferCreateTransaction, + RippledAmount +} from '../objects' + +export interface BookOffersRequest { + taker?: string, + taker_gets: TakerRequestAmount, + taker_pays: TakerRequestAmount, + ledger_hash?: string, + ledger_index?: number | ('validated' | 'closed' | 'current'), + limit?: number, + marker?: any +} + +export interface BookOffersResponse { + offers: OrderBookOffer[], + ledger_hash?: string, + ledger_current_index?: number, + ledger_index?: number, + marker?: any +} + +interface OrderBookOffer extends OfferCreateTransaction { + quality?: number + owner_funds?: string, + taker_gets_funded?: RippledAmount, + taker_pays_funded?: RippledAmount +} diff --git a/src/common/types/commands/gateway_balances.ts b/src/common/types/commands/gateway_balances.ts new file mode 100644 index 00000000..27812982 --- /dev/null +++ b/src/common/types/commands/gateway_balances.ts @@ -0,0 +1,20 @@ +import {Amount} from '../objects' + + +export interface GatewayBalancesRequest { + account: string, + strict?: boolean, + hotwallet: string|Array, + ledger_hash?: string, + ledger_index?: number | ('validated' | 'closed' | 'current') +} + +export interface GatewayBalancesResponse { + account: string, + obligations?: {[currency: string]: string}, + balances?: {[address: string]: Amount[]}, + assets?: {[address: string]: Amount[]}, + ledger_hash?: string, + ledger_current_index?: number, + ledger_index?: number +} diff --git a/src/common/types/commands/index.ts b/src/common/types/commands/index.ts new file mode 100644 index 00000000..7316272e --- /dev/null +++ b/src/common/types/commands/index.ts @@ -0,0 +1,5 @@ +export * from './account_info' +export * from './account_lines' +export * from './account_offers' +export * from './book_offers' +export * from './gateway_balances' diff --git a/src/common/types/objects/accounts.ts b/src/common/types/objects/accounts.ts new file mode 100644 index 00000000..6d79951a --- /dev/null +++ b/src/common/types/objects/accounts.ts @@ -0,0 +1,17 @@ +export interface AccountRoot { + LedgerEntryType: string, + Account: string, + Flags: number, + Sequence: number, + Balance: string, + OwnerCount: number, + PreviousTxnID: string, + PreviousTxnLgrSeq: number, + AccountTxnID?: string, + RegularKey?: string, + EmailHash?: string, + MessageKey?: string + TickSize?: number, + TransferRate?: number, + Domain?: string +} diff --git a/src/common/types/objects/adjustments.ts b/src/common/types/objects/adjustments.ts new file mode 100644 index 00000000..42a17d7d --- /dev/null +++ b/src/common/types/objects/adjustments.ts @@ -0,0 +1,19 @@ +import {Amount} from './amounts' + +export type Adjustment = { + address: string, + amount: Amount, + tag?: number +} + +export type MaxAdjustment = { + address: string, + maxAmount: Amount, + tag?: number +} + +export type MinAdjustment = { + address: string, + minAmount: Amount, + tag?: number +} diff --git a/src/common/types/objects/amounts.ts b/src/common/types/objects/amounts.ts new file mode 100644 index 00000000..45f00118 --- /dev/null +++ b/src/common/types/objects/amounts.ts @@ -0,0 +1,28 @@ +export type Amount = { + value: string, + currency: string, + issuer?: string, + counterparty?: string +} + + +export type RippledAmount = string | Amount + +/** + * Specification of which currency the account taking the offer would pay/ + * receive, as an object with currency and issuer fields (omit issuer for XRP). + * Similar to currency amounts. + */ +export interface TakerRequestAmount { + currency: string + issuer?: string +} + +/** + * A currency-counterparty pair, or just currency if it's XRP. + */ +export type Issue = { + currency: string, + issuer?: string, + counterparty?: string +} diff --git a/src/common/types/objects/index.ts b/src/common/types/objects/index.ts new file mode 100644 index 00000000..b11a227b --- /dev/null +++ b/src/common/types/objects/index.ts @@ -0,0 +1,7 @@ +export * from './accounts' +export * from './adjustments' +export * from './amounts' +export * from './memos' +export * from './signers' +export * from './transactions' +export * from './trustlines' diff --git a/src/common/types/objects/memos.ts b/src/common/types/objects/memos.ts new file mode 100644 index 00000000..4464fa89 --- /dev/null +++ b/src/common/types/objects/memos.ts @@ -0,0 +1,6 @@ + +export type Memo = { + type?: string, + format?: string, + data?: string +} diff --git a/src/common/types/objects/signers.ts b/src/common/types/objects/signers.ts new file mode 100644 index 00000000..a90f737e --- /dev/null +++ b/src/common/types/objects/signers.ts @@ -0,0 +1,14 @@ +export interface SignerList { + LedgerEntryType: string, + OwnerNode: string, + SignerQuorum: number, + SignerEntries: SignerEntry[], + SignerListID: number, + PreviousTxnID: string, + PreviousTxnLgrSeq: number +} + +export interface SignerEntry { + Account: string, + SignerWeight: number +} diff --git a/src/common/types/objects/transactions.ts b/src/common/types/objects/transactions.ts new file mode 100644 index 00000000..40c54117 --- /dev/null +++ b/src/common/types/objects/transactions.ts @@ -0,0 +1,22 @@ +import {RippledAmount} from './amounts' +import {Memo} from './memos' + +export interface OfferCreateTransaction { + TransactionType: 'OfferCreate', + Account: string, + AccountTxnID?: string, + Fee: string, + Field: any, + Flags: number, + LastLedgerSequence?: number, + Sequence: number, + Signers: any[], + SigningPubKey: string, + SourceTag?: number, + TakerGets: RippledAmount, + TakerPays: RippledAmount, + TxnSignature: string, + Expiration?: number, + Memos?: Memo[], + OfferSequence?: number, +} diff --git a/src/common/types/objects/trustlines.ts b/src/common/types/objects/trustlines.ts new file mode 100644 index 00000000..f7f62228 --- /dev/null +++ b/src/common/types/objects/trustlines.ts @@ -0,0 +1,42 @@ +import {Memo} from './memos' + +export interface Trustline { + account: string, + balance: string, + currency: string, + limit: string, + limit_peer: string, + quality_in: number, + quality_out: number, + no_ripple?: boolean, + no_ripple_peer?: boolean, + freeze?: boolean, + freeze_peer?: boolean, + authorized?: boolean, + peer_authorized?: boolean, +} + +export type FormattedTrustlineSpecification = { + currency: string, + counterparty: string, + limit: string, + qualityIn?: number, + qualityOut?: number, + ripplingDisabled?: boolean, + authorized?: boolean, + frozen?: boolean, + memos?: Memo[] +} + +export type FormattedTrustline = { + specification: FormattedTrustlineSpecification, + counterparty: { + limit: string, + ripplingDisabled?: boolean, + frozen?: boolean, + authorized?: boolean + }, + state: { + balance: string + } +} diff --git a/src/common/utils.ts b/src/common/utils.ts index 3bdebc2f..089d331a 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -2,7 +2,7 @@ import * as _ from 'lodash' import BigNumber from 'bignumber.js' const {deriveKeypair} = require('ripple-keypairs') -import {Amount, RippledAmount} from './types' +import {Amount, RippledAmount} from './types/objects' function isValidSecret(secret: string): boolean { try { diff --git a/src/ledger/accountinfo.ts b/src/ledger/accountinfo.ts index 2979e6f7..a987ef94 100644 --- a/src/ledger/accountinfo.ts +++ b/src/ledger/accountinfo.ts @@ -1,31 +1,12 @@ import {validate, removeUndefined, dropsToXrp} from '../common' +import {RippleAPI} from '../api' +import {AccountInfoResponse} from '../common/types/commands/account_info' -type AccountData = { - Sequence: number, - Account: string, - Balance: string, - Flags: number, - LedgerEntryType: string, - OwnerCount: number, - PreviousTxnID: string, - AccountTxnID?: string, - PreviousTxnLgrSeq: number, - index: string -} - -type AccountDataResponse = { - account_data: AccountData, - ledger_current_index?: number, - ledger_hash?: string, - ledger_index: number, - validated: boolean -} - -type AccountInfoOptions = { +type GetAccountInfoOptions = { ledgerVersion?: number } -type AccountInfoResponse = { +type FormattedGetAccountInfoResponse = { sequence: number, xrpBalance: string, ownerCount: number, @@ -34,7 +15,9 @@ type AccountInfoResponse = { previousAffectingTransactionLedgerVersion: number } -function formatAccountInfo(response: AccountDataResponse) { +function formatAccountInfo( + response: AccountInfoResponse +): FormattedGetAccountInfoResponse { const data = response.account_data return removeUndefined({ sequence: data.Sequence, @@ -46,17 +29,16 @@ function formatAccountInfo(response: AccountDataResponse) { }) } -function getAccountInfo(address: string, options: AccountInfoOptions = {} -): Promise { +export default async function getAccountInfo( + this: RippleAPI, address: string, options: GetAccountInfoOptions = {} +): Promise { + // 1. Validate validate.getAccountInfo({address, options}) - - const request = { - command: 'account_info', + // 2. Make Request + const response = await this._request('account_info', { account: address, ledger_index: options.ledgerVersion || 'validated' - } - - return this.connection.request(request).then(formatAccountInfo) + }) + // 3. Return Formatted Response + return formatAccountInfo(response) } - -export default getAccountInfo diff --git a/src/ledger/balance-sheet.ts b/src/ledger/balance-sheet.ts index d0c25d24..9eb90527 100644 --- a/src/ledger/balance-sheet.ts +++ b/src/ledger/balance-sheet.ts @@ -1,7 +1,8 @@ import * as _ from 'lodash' -import * as utils from './utils' import {validate} from '../common' -import {Amount} from '../common/types' +import {Amount} from '../common/types/objects' +import {ensureLedgerVersion} from './utils' +import {RippleAPI} from '../api' type BalanceSheetOptions = { excludeAddresses?: Array, @@ -46,21 +47,21 @@ function formatBalanceSheet(balanceSheet): GetBalanceSheet { return result } -function getBalanceSheet(address: string, options: BalanceSheetOptions = {} +async function getBalanceSheet( + this: RippleAPI, address: string, options: BalanceSheetOptions = {} ): Promise { + // 1. Validate validate.getBalanceSheet({address, options}) - - return utils.ensureLedgerVersion.call(this, options).then(_options => { - const request = { - command: 'gateway_balances', - account: address, - strict: true, - hotwallet: _options.excludeAddresses, - ledger_index: _options.ledgerVersion - } - - return this.connection.request(request).then(formatBalanceSheet) + options = await ensureLedgerVersion.call(this, options) + // 2. Make Request + const response = await this._request('gateway_balances', { + account: address, + strict: true, + hotwallet: options.excludeAddresses, + ledger_index: options.ledgerVersion }) + // 3. Return Formatted Response + return formatBalanceSheet(response) } export default getBalanceSheet diff --git a/src/ledger/balances.ts b/src/ledger/balances.ts index a07643f3..4c55929f 100644 --- a/src/ledger/balances.ts +++ b/src/ledger/balances.ts @@ -1,7 +1,8 @@ import * as utils from './utils' import {validate} from '../common' import {Connection} from '../common' -import {TrustlinesOptions, Trustline} from './trustlines-types' +import {GetTrustlinesOptions} from './trustlines' +import {FormattedTrustline} from '../common/types/objects/trustlines' type Balance = { @@ -12,7 +13,7 @@ type Balance = { type GetBalances = Array -function getTrustlineBalanceAmount(trustline: Trustline) { +function getTrustlineBalanceAmount(trustline: FormattedTrustline) { return { currency: trustline.specification.currency, counterparty: trustline.specification.counterparty, @@ -46,7 +47,7 @@ function getLedgerVersionHelper(connection: Connection, optionValue?: number return connection.getLedgerVersion() } -function getBalances(address: string, options: TrustlinesOptions = {} +function getBalances(address: string, options: GetTrustlinesOptions = {} ): Promise { validate.getTrustlines({address, options}) diff --git a/src/ledger/orderbook.ts b/src/ledger/orderbook.ts index 9e9ed8c0..d4abafae 100644 --- a/src/ledger/orderbook.ts +++ b/src/ledger/orderbook.ts @@ -2,15 +2,20 @@ import * as _ from 'lodash' import * as utils from './utils' import parseOrderbookOrder from './parse/orderbook-order' import {validate} from '../common' -import {Connection} from '../common' -import {OrdersOptions, OrderSpecification} from './types' -import {Amount, Issue} from '../common/types' +import {OrderSpecification} from './types' +import {Amount, Issue} from '../common/types/objects' +import {RippleAPI} from '../api' +import {OfferCreateTransaction} from '../common/types/objects' + +export type OrdersOptions = { + limit?: number, + ledgerVersion?: number +} type Orderbook = { base: Issue, counter: Issue } - type OrderbookItem = { specification: OrderSpecification, properties: { @@ -31,26 +36,6 @@ type GetOrderbook = { asks: OrderbookOrders } -// account is to specify a "perspective", which affects which unfunded offers -// are returned -function getBookOffers(connection: Connection, account: string, - ledgerVersion: number|undefined, limit: number|undefined, takerGets: Issue, - takerPays: Issue -): Promise { - const orderData = utils.renameCounterpartyToIssuerInOrder({ - taker_gets: takerGets, - taker_pays: takerPays - }) - return connection.request({ - command: 'book_offers', - taker_gets: orderData.taker_gets, - taker_pays: orderData.taker_pays, - ledger_index: ledgerVersion || 'validated', - limit: limit, - taker: account - }).then(data => data.offers) -} - function isSameIssue(a: Amount, b: Amount) { return a.currency === b.currency && a.counterparty === b.counterparty } @@ -75,7 +60,8 @@ function alignOrder(base: Amount, order: OrderbookItem) { return isSameIssue(quantity, base) ? order : flipOrder(order) } -function formatBidsAndAsks(orderbook: Orderbook, offers) { +function formatBidsAndAsks( + orderbook: Orderbook, offers: OfferCreateTransaction[]) { // the "base" currency is the currency that you are buying or selling // the "counter" is the currency that the "base" is priced in // a "bid"/"ask" is an order to buy/sell the base, respectively @@ -93,17 +79,42 @@ function formatBidsAndAsks(orderbook: Orderbook, offers) { return {bids, asks} } -function getOrderbook(address: string, orderbook: Orderbook, - options: OrdersOptions = {} -): Promise { - validate.getOrderbook({address, orderbook, options}) - - const getter = _.partial(getBookOffers, this.connection, address, - options.ledgerVersion, options.limit) - const getOffers = _.partial(getter, orderbook.base, orderbook.counter) - const getReverseOffers = _.partial(getter, orderbook.counter, orderbook.base) - return Promise.all([getOffers(), getReverseOffers()]).then(data => - formatBidsAndAsks(orderbook, _.flatten(data))) +// account is to specify a "perspective", which affects which unfunded offers +// are returned +async function makeRequest( + api: RippleAPI, taker: string, options: OrdersOptions, + takerGets: Issue, takerPays: Issue +) { + const orderData = utils.renameCounterpartyToIssuerInOrder({ + taker_gets: takerGets, + taker_pays: takerPays + }) + return api._requestAll('book_offers', { + taker_gets: orderData.taker_gets, + taker_pays: orderData.taker_pays, + ledger_index: options.ledgerVersion || 'validated', + limit: options.limit, + taker +}) } -export default getOrderbook +export default async function getOrderbook( + this: RippleAPI, + address: string, + orderbook: Orderbook, + options: OrdersOptions = {} +): Promise { + // 1. Validate + validate.getOrderbook({address, orderbook, options}) + // 2. Make Request + const [directOfferResults, reverseOfferResults] = await Promise.all([ + makeRequest(this, address, options, orderbook.base, orderbook.counter), + makeRequest(this, address, options, orderbook.counter, orderbook.base) + ]) + // 3. Return Formatted Response + const directOffers = _.flatMap(directOfferResults, + directOfferResult => directOfferResult.offers) + const reverseOffers = _.flatMap(reverseOfferResults, + reverseOfferResult => reverseOfferResult.offers) + return formatBidsAndAsks(orderbook, [...directOffers, ...reverseOffers]) +} diff --git a/src/ledger/orders.ts b/src/ledger/orders.ts index f01b6af0..838c78b4 100644 --- a/src/ledger/orders.ts +++ b/src/ledger/orders.ts @@ -1,37 +1,39 @@ import * as _ from 'lodash' -import * as utils from './utils' import {validate} from '../common' -import {Connection} from '../common' import parseAccountOrder from './parse/account-order' -import {OrdersOptions, Order} from './types' +import {Order} from './types' +import {RippleAPI} from '../api' +import {AccountOffersResponse} from '../common/types/commands/account_offers' -type GetOrders = Array - -function requestAccountOffers(connection: Connection, address: string, - ledgerVersion: number, marker: string, limit: number -): Promise { - return connection.request({ - command: 'account_offers', - account: address, - marker: marker, - limit: utils.clamp(limit, 10, 400), - ledger_index: ledgerVersion - }).then(data => ({ - marker: data.marker, - results: data.offers.map(_.partial(parseAccountOrder, address)) - })) +export type GetOrdersOptions = { + limit?: number, + ledgerVersion?: number } -function getOrders(address: string, options: OrdersOptions = {} -): Promise { +function formatResponse( + address: string, responses: AccountOffersResponse[] +): Order[] { + let orders: Order[] = [] + for (const response of responses) { + const offers = response.offers.map(offer => { + return parseAccountOrder(address, offer) + }) + orders = orders.concat(offers) + } + return _.sortBy(orders, order => order.properties.sequence) +} + +export default async function getOrders( + this: RippleAPI, address: string, options: GetOrdersOptions = {} +): Promise { + // 1. Validate validate.getOrders({address, options}) - - return utils.ensureLedgerVersion.call(this, options).then(_options => { - const getter = _.partial(requestAccountOffers, this.connection, address, - _options.ledgerVersion) - return utils.getRecursive(getter, _options.limit).then(orders => - _.sortBy(orders, order => order.properties.sequence)) + // 2. Make Request + const responses = await this._requestAll('account_offers', { + account: address, + ledger_index: options.ledgerVersion || await this.getLedgerVersion(), + limit: options.limit }) + // 3. Return Formatted Response + return formatResponse(address, responses) } - -export default getOrders diff --git a/src/ledger/parse/account-order.ts b/src/ledger/parse/account-order.ts index 559a1948..85d3dbe0 100644 --- a/src/ledger/parse/account-order.ts +++ b/src/ledger/parse/account-order.ts @@ -3,6 +3,7 @@ import parseAmount from './amount' import {parseTimestamp, adjustQualityForXRP} from './utils' import {removeUndefined} from '../../common' import {orderFlags} from './flags' +import {Order} from '../types' // TODO: remove this function once rippled provides quality directly function computeQuality(takerGets, takerPays) { @@ -12,7 +13,7 @@ function computeQuality(takerGets, takerPays) { // rippled 'account_offers' returns a different format for orders than 'tx' // the flags are also different -function parseAccountOrder(address: string, order: any): Object { +function parseAccountOrder(address: string, order: any): Order { const direction = (order.flags & orderFlags.Sell) === 0 ? 'buy' : 'sell' const takerGetsAmount = parseAmount(order.taker_gets) const takerPaysAmount = parseAmount(order.taker_pays) diff --git a/src/ledger/parse/account-trustline.ts b/src/ledger/parse/account-trustline.ts index fd965acb..2328ac53 100644 --- a/src/ledger/parse/account-trustline.ts +++ b/src/ledger/parse/account-trustline.ts @@ -1,24 +1,13 @@ import {parseQuality} from './utils' import {removeUndefined} from '../../common' - -type Trustline = { - account: string, limit: number, currency: string, quality_in: number|null, - quality_out: number|null, no_ripple: boolean, freeze: boolean, - authorized: boolean, limit_peer: string, no_ripple_peer: boolean, - freeze_peer: boolean, peer_authorized: boolean, balance: any -} - -type TrustlineSpecification = {} -type TrustlineCounterParty = {} -type TrustlineState = {balance: number} -type AccountTrustline = { - specification: TrustlineSpecification, counterparty: TrustlineCounterParty, - state: TrustlineState -} +import { + Trustline, + FormattedTrustline +} from '../../common/types/objects/trustlines' // rippled 'account_lines' returns a different format for // trustlines than 'tx' -function parseAccountTrustline(trustline: Trustline): AccountTrustline { +function parseAccountTrustline(trustline: Trustline): FormattedTrustline { const specification = removeUndefined({ limit: trustline.limit, currency: trustline.currency, diff --git a/src/ledger/parse/amount.ts b/src/ledger/parse/amount.ts index 63519b00..ca6883c8 100644 --- a/src/ledger/parse/amount.ts +++ b/src/ledger/parse/amount.ts @@ -1,5 +1,5 @@ import * as common from '../../common' -import {Amount, RippledAmount} from '../../common/types' +import {Amount, RippledAmount} from '../../common/types/objects' function parseAmount(amount: RippledAmount): Amount { diff --git a/src/ledger/parse/pathfind.ts b/src/ledger/parse/pathfind.ts index ed08dbe7..5fd09c5d 100644 --- a/src/ledger/parse/pathfind.ts +++ b/src/ledger/parse/pathfind.ts @@ -1,6 +1,6 @@ import * as _ from 'lodash' import parseAmount from './amount' -import {Amount, RippledAmount} from '../../common/types' +import {Amount, RippledAmount} from '../../common/types/objects' import {Path, GetPaths, RippledPathsResponse} from '../pathfind-types' function parsePaths(paths) { diff --git a/src/ledger/parse/utils.ts b/src/ledger/parse/utils.ts index 5de5274a..f34eade3 100644 --- a/src/ledger/parse/utils.ts +++ b/src/ledger/parse/utils.ts @@ -4,7 +4,7 @@ import BigNumber from 'bignumber.js' import * as common from '../../common' import parseAmount from './amount' -import {Amount, Memo} from '../../common/types' +import {Amount, Memo} from '../../common/types/objects' function adjustQualityForXRP( quality: string, takerGetsCurrency: string, takerPaysCurrency: string diff --git a/src/ledger/pathfind-types.ts b/src/ledger/pathfind-types.ts index 2fa86f39..238029a2 100644 --- a/src/ledger/pathfind-types.ts +++ b/src/ledger/pathfind-types.ts @@ -1,7 +1,14 @@ -import {Amount, LaxLaxAmount, RippledAmount, Adjustment, MaxAdjustment, - MinAdjustment} from '../common/types' +import {Amount, RippledAmount, Adjustment, MaxAdjustment, + MinAdjustment} from '../common/types/objects' +// Amount where counterparty and value are optional +export type LaxLaxAmount = { + currency: string, + value?: string, + issuer?: string, + counterparty?: string +} export type Path = { source: Adjustment | MaxAdjustment, diff --git a/src/ledger/pathfind.ts b/src/ledger/pathfind.ts index 2a381e0c..be27a15b 100644 --- a/src/ledger/pathfind.ts +++ b/src/ledger/pathfind.ts @@ -4,7 +4,7 @@ import {getXRPBalance, renameCounterpartyToIssuer} from './utils' import {validate, toRippledAmount, errors} from '../common' import {Connection} from '../common' import parsePathfind from './parse/pathfind' -import {RippledAmount, Amount} from '../common/types' +import {RippledAmount, Amount} from '../common/types/objects' import { GetPaths, PathFind, RippledPathsResponse, PathFindRequest } from './pathfind-types' diff --git a/src/ledger/transaction-types.ts b/src/ledger/transaction-types.ts index ffceff86..cf627161 100644 --- a/src/ledger/transaction-types.ts +++ b/src/ledger/transaction-types.ts @@ -1,5 +1,5 @@ -import {Amount, Memo} from '../common/types' +import {Amount, Memo} from '../common/types/objects' type Outcome = { result: string, diff --git a/src/ledger/trustlines-types.ts b/src/ledger/trustlines-types.ts deleted file mode 100644 index b980882b..00000000 --- a/src/ledger/trustlines-types.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {Memo} from '../common/types' - -export type TrustLineSpecification = { - currency: string, - counterparty: string, - limit: string, - qualityIn?: number, - qualityOut?: number, - ripplingDisabled?: boolean, - authorized?: boolean, - frozen?: boolean, - memos?: Memo[] -} - -export type Trustline = { - specification: TrustLineSpecification, - counterparty: { - limit: string, - ripplingDisabled?: boolean, - frozen?: boolean, - authorized?: boolean - }, - state: { - balance: string - } -} - -export type TrustlinesOptions = { - counterparty?: string, - currency?: string, - limit?: number, - ledgerVersion?: number -} diff --git a/src/ledger/trustlines.ts b/src/ledger/trustlines.ts index 5f0e21d9..bdf37b72 100644 --- a/src/ledger/trustlines.ts +++ b/src/ledger/trustlines.ts @@ -1,53 +1,37 @@ import * as _ from 'lodash' -import * as utils from './utils' import {validate} from '../common' -import {Connection} from '../common' import parseAccountTrustline from './parse/account-trustline' -import {TrustlinesOptions, Trustline} from './trustlines-types' +import {RippleAPI} from '../api' +import {FormattedTrustline} from '../common/types/objects/trustlines' - -type GetTrustlinesResponse = Array -interface GetAccountLinesResponse { - marker?: any, - results: Trustline[] +export type GetTrustlinesOptions = { + counterparty?: string, + currency?: string, + limit?: number, + ledgerVersion?: number } -function currencyFilter(currency: string, trustline: Trustline) { +function currencyFilter(currency: string, trustline: FormattedTrustline) { return currency === null || trustline.specification.currency === currency } -function formatResponse(options: TrustlinesOptions, data: any) { - return { - marker: data.marker, - results: data.lines.map(parseAccountTrustline) - .filter(_.partial(currencyFilter, options.currency || null)) - } -} - -function getAccountLines(connection: Connection, address: string, - ledgerVersion: number, options: TrustlinesOptions, marker: string, - limit: number -): Promise { - const request = { - command: 'account_lines', +async function getTrustlines( + this: RippleAPI, address: string, options: GetTrustlinesOptions = {} +): Promise { + // 1. Validate + validate.getTrustlines({address, options}) + const ledgerVersion = await this.getLedgerVersion() + // 2. Make Request + const responses = await this._requestAll('account_lines', { account: address, ledger_index: ledgerVersion, - marker: marker, - limit: utils.clamp(limit, 10, 400), + limit: options.limit, peer: options.counterparty - } - - return connection.request(request).then(_.partial(formatResponse, options)) -} - -function getTrustlines(address: string, options: TrustlinesOptions = {} -): Promise { - validate.getTrustlines({address, options}) - - return this.getLedgerVersion().then(ledgerVersion => { - const getter = _.partial(getAccountLines, this.connection, address, - options.ledgerVersion || ledgerVersion, options) - return utils.getRecursive(getter, options.limit) + }) + // 3. Return Formatted Response + const trustlines = _.flatMap(responses, response => response.lines) + return trustlines.map(parseAccountTrustline).filter(trustline => { + return currencyFilter(options.currency || null, trustline) }) } diff --git a/src/ledger/types.ts b/src/ledger/types.ts index 31e8e5eb..44d2c4ee 100644 --- a/src/ledger/types.ts +++ b/src/ledger/types.ts @@ -1,10 +1,5 @@ -import {Amount} from '../common/types' - -export type OrdersOptions = { - limit?: number, - ledgerVersion?: number -} +import {Amount} from '../common/types/objects' export type OrderSpecification = { direction: string, diff --git a/src/ledger/utils.ts b/src/ledger/utils.ts index 2ec9c146..b453ea5b 100644 --- a/src/ledger/utils.ts +++ b/src/ledger/utils.ts @@ -3,7 +3,7 @@ import * as assert from 'assert' import * as common from '../common' import {Connection} from '../common' import {TransactionType} from './transaction-types' -import {Issue} from '../common/types' +import {Issue} from '../common/types/objects' type RecursiveData = { marker: string, diff --git a/src/transaction/escrow-cancellation.ts b/src/transaction/escrow-cancellation.ts index a4232b43..1b9006a1 100644 --- a/src/transaction/escrow-cancellation.ts +++ b/src/transaction/escrow-cancellation.ts @@ -2,7 +2,7 @@ import * as _ from 'lodash' import * as utils from './utils' const validate = utils.common.validate import {Instructions, Prepare} from './types' -import {Memo} from '../common/types' +import {Memo} from '../common/types/objects' type EscrowCancellation = { owner: string, diff --git a/src/transaction/escrow-creation.ts b/src/transaction/escrow-creation.ts index aa9fb4a5..dd0b8b53 100644 --- a/src/transaction/escrow-creation.ts +++ b/src/transaction/escrow-creation.ts @@ -3,7 +3,7 @@ import * as utils from './utils' import {validate, iso8601ToRippleTime, xrpToDrops} from '../common' const ValidationError = utils.common.errors.ValidationError import {Instructions, Prepare} from './types' -import {Memo} from '../common/types' +import {Memo} from '../common/types/objects' type EscrowCreation = { amount: string, diff --git a/src/transaction/escrow-execution.ts b/src/transaction/escrow-execution.ts index f3f874ff..ff010233 100644 --- a/src/transaction/escrow-execution.ts +++ b/src/transaction/escrow-execution.ts @@ -3,7 +3,7 @@ import * as utils from './utils' const validate = utils.common.validate const ValidationError = utils.common.errors.ValidationError import {Instructions, Prepare} from './types' -import {Memo} from '../common/types' +import {Memo} from '../common/types/objects' type EscrowExecution = { owner: string, diff --git a/src/transaction/payment.ts b/src/transaction/payment.ts index dbe4b797..06bfd921 100644 --- a/src/transaction/payment.ts +++ b/src/transaction/payment.ts @@ -6,7 +6,7 @@ const paymentFlags = utils.common.txFlags.Payment const ValidationError = utils.common.errors.ValidationError import {Instructions, Prepare} from './types' import {Amount, Adjustment, MaxAdjustment, - MinAdjustment, Memo} from '../common/types' + MinAdjustment, Memo} from '../common/types/objects' type Payment = { diff --git a/src/transaction/settings.ts b/src/transaction/settings.ts index eb53cce0..9e061038 100644 --- a/src/transaction/settings.ts +++ b/src/transaction/settings.ts @@ -6,7 +6,7 @@ const validate = utils.common.validate const AccountFlagIndices = utils.common.constants.AccountFlagIndices const AccountFields = utils.common.constants.AccountFields import {Instructions, Prepare} from './types' -import {Memo} from '../common/types' +import {Memo} from '../common/types/objects' type WeightedSigner = {address: string, weight: number} type SettingsSigners = { diff --git a/src/transaction/trustline.ts b/src/transaction/trustline.ts index 40a6394f..c732f899 100644 --- a/src/transaction/trustline.ts +++ b/src/transaction/trustline.ts @@ -4,14 +4,16 @@ import * as utils from './utils' const validate = utils.common.validate const trustlineFlags = utils.common.txFlags.TrustSet import {Instructions, Prepare} from './types' -import {TrustLineSpecification} from '../ledger/trustlines-types' +import { + FormattedTrustlineSpecification +} from '../common/types/objects/trustlines' function convertQuality(quality) { return (new BigNumber(quality)).shift(9).truncated().toNumber() } function createTrustlineTransaction(account: string, - trustline: TrustLineSpecification + trustline: FormattedTrustlineSpecification ): Object { const limit = { currency: trustline.currency, @@ -49,7 +51,7 @@ function createTrustlineTransaction(account: string, } function prepareTrustline(address: string, - trustline: TrustLineSpecification, instructions: Instructions = {} + trustline: FormattedTrustlineSpecification, instructions: Instructions = {} ): Promise { validate.prepareTrustline({address, trustline, instructions}) const txJSON = createTrustlineTransaction(address, trustline) diff --git a/src/transaction/utils.ts b/src/transaction/utils.ts index 2472e826..e00c008b 100644 --- a/src/transaction/utils.ts +++ b/src/transaction/utils.ts @@ -1,10 +1,16 @@ import BigNumber from 'bignumber.js' import * as common from '../common' -import {Memo, ApiMemo} from '../common/types' +import {Memo} from '../common/types/objects' const txFlags = common.txFlags import {Instructions, Prepare} from './types' import {RippleAPI} from '../api' +export type ApiMemo = { + MemoData?: string, + MemoType?: string, + MemoFormat?: string +} + function formatPrepareResponse(txJSON: any): Prepare { const instructions = { fee: common.dropsToXrp(txJSON.Fee), diff --git a/test/api-test.js b/test/api-test.js index 95ef12c1..b5fd703e 100644 --- a/test/api-test.js +++ b/test/api-test.js @@ -560,9 +560,11 @@ describe('RippleAPI', function() { }); it('getBalanceSheet - invalid options', function() { - assert.throws(() => { - this.api.getBalanceSheet(address, {invalid: 'options'}); - }, this.api.errors.ValidationError); + return this.api.getBalanceSheet(address, {invalid: 'options'}).then(() => { + assert(false, 'Should throw ValidationError'); + }).catch(error => { + assert(error instanceof this.api.errors.ValidationError); + }); }); it('getBalanceSheet - empty', function() { @@ -1050,9 +1052,11 @@ describe('RippleAPI', function() { }); it('getAccountInfo - invalid options', function() { - assert.throws(() => { - this.api.getAccountInfo(address, {invalid: 'options'}); - }, this.api.errors.ValidationError); + return this.api.getAccountInfo(address, {invalid: 'options'}).then(() => { + assert(false, 'Should throw ValidationError'); + }).catch(error => { + assert(error instanceof this.api.errors.ValidationError); + }); }); it('getOrders', function() { @@ -1060,31 +1064,36 @@ describe('RippleAPI', function() { _.partial(checkResult, responses.getOrders, 'getOrders')); }); - it('getOrders', function() { - return this.api.getOrders(address, undefined).then( + it('getOrders - limit', function() { + return this.api.getOrders(address, {limit: 20}).then( _.partial(checkResult, responses.getOrders, 'getOrders')); }); it('getOrders - invalid options', function() { - assert.throws(() => { - this.api.getOrders(address, {invalid: 'options'}); - }, this.api.errors.ValidationError); + return this.api.getOrders(address, {invalid: 'options'}).then(() => { + assert(false, 'Should throw ValidationError'); + }).catch(error => { + assert(error instanceof this.api.errors.ValidationError); + }); }); describe('getOrderbook', function() { it('normal', function() { return this.api.getOrderbook(address, - requests.getOrderbook.normal, undefined).then( + requests.getOrderbook.normal, {limit: 20}).then( _.partial(checkResult, responses.getOrderbook.normal, 'getOrderbook')); }); it('invalid options', function() { - assert.throws(() => { - this.api.getOrderbook(address, requests.getOrderbook.normal, - {invalid: 'options'}); - }, this.api.errors.ValidationError); + return this.api.getOrderbook( + address, requests.getOrderbook.normal, {invalid: 'options'} + ).then(() => { + assert(false, 'Should throw ValidationError'); + }).catch(error => { + assert(error instanceof this.api.errors.ValidationError); + }); }); it('with XRP', function() {