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
This commit is contained in:
Fred K. Schott
2018-02-20 11:44:36 -08:00
committed by Elliot Lee
parent 4a21360e37
commit 365de6d18a
42 changed files with 605 additions and 307 deletions

View File

@@ -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<AccountInfoResponse>
async _request(command: 'account_lines', params: AccountLinesRequest):
Promise<AccountLinesResponse>
async _request(command: 'account_offers', params: AccountOffersRequest):
Promise<AccountOffersResponse>
async _request(command: 'book_offers', params: BookOffersRequest):
Promise<BookOffersResponse>
async _request(command: 'gateway_balances', params: GatewayBalancesRequest):
Promise<GatewayBalancesResponse>
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<AccountOffersResponse[]>
async _requestAll(command: 'book_offers', params: BookOffersRequest):
Promise<BookOffersResponse[]>
async _requestAll(command: 'account_lines', params: AccountLinesRequest):
Promise<AccountLinesResponse[]>
async _requestAll(
command: string,
params: any = {},
options: {collect?: string} = {}): Promise<any[]> {
// 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 (<Function>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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -0,0 +1,20 @@
import {Amount} from '../objects'
export interface GatewayBalancesRequest {
account: string,
strict?: boolean,
hotwallet: string|Array<string>,
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
}

View File

@@ -0,0 +1,5 @@
export * from './account_info'
export * from './account_lines'
export * from './account_offers'
export * from './book_offers'
export * from './gateway_balances'

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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'

View File

@@ -0,0 +1,6 @@
export type Memo = {
type?: string,
format?: string,
data?: string
}

View File

@@ -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
}

View File

@@ -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,
}

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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<AccountInfoResponse> {
export default async function getAccountInfo(
this: RippleAPI, address: string, options: GetAccountInfoOptions = {}
): Promise<FormattedGetAccountInfoResponse> {
// 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

View File

@@ -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<string>,
@@ -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<GetBalanceSheet> {
// 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

View File

@@ -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<Balance>
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<GetBalances> {
validate.getTrustlines({address, options})

View File

@@ -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<Object[]> {
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<GetOrderbook> {
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<GetOrderbook> {
// 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])
}

View File

@@ -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<Order>
function requestAccountOffers(connection: Connection, address: string,
ledgerVersion: number, marker: string, limit: number
): Promise<Object> {
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<GetOrders> {
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<Order[]> {
// 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

View File

@@ -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)

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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

View File

@@ -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,

View File

@@ -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'

View File

@@ -1,5 +1,5 @@
import {Amount, Memo} from '../common/types'
import {Amount, Memo} from '../common/types/objects'
type Outcome = {
result: string,

View File

@@ -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
}

View File

@@ -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<Trustline>
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<GetAccountLinesResponse> {
const request = {
command: 'account_lines',
async function getTrustlines(
this: RippleAPI, address: string, options: GetTrustlinesOptions = {}
): Promise<FormattedTrustline[]> {
// 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<GetTrustlinesResponse> {
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)
})
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 = {

View File

@@ -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 = {

View File

@@ -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<Prepare> {
validate.prepareTrustline({address, trustline, instructions})
const txJSON = createTrustlineTransaction(address, trustline)

View File

@@ -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),

View File

@@ -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() {