prepareTransaction should not overwrite Sequence (#990)

* Cleans up some code and fixes some type errors

* Clarify how null settings work

* Document updated RippledError

* Updates per review by @mDuo13
This commit is contained in:
Elliot Lee
2019-03-18 15:55:42 -07:00
committed by GitHub
parent 8213861ab7
commit d82703f41b
30 changed files with 614 additions and 118 deletions

View File

@@ -2,25 +2,9 @@
## UNRELEASED
### New in rippled 1.2.1
### [BREAKING CHANGE] `prepare*` methods reject the Promise on error
As this is the first release of ripple-lib following the release of rippled 1.2.1, we would like to highlight the following API improvements:
1. The [`delivered_amount` field](https://developers.ripple.com/partial-payments.html#the-delivered-amount-field) has been added to the `ledger` method, and to transaction subscriptions.
api.getLedger({includeTransactions: true, includeAllData: true, ledgerVersion: 17718771}).then(...)
You can also call `ledger` directly:
request('ledger', {...}).then(...)
2. [Support for Ed25519 seeds encoded using ripple-lib](https://github.com/ripple/rippled/pull/2734)
You have access to these improvements when you use a rippled server running version 1.2.1 or later. At the time of writing, we recommend using rippled version **1.2.2** or later.
**BREAKING CHANGE:**
The `prepare*` methods now reject the Promise when an error occurs.
The `prepare*` methods now always reject the Promise when an error occurs, instead of throwing.
Previously, the methods would synchronously throw on validation errors, despite being asynchronous methods that return Promises.
@@ -64,6 +48,64 @@ This applies to:
* preparePaymentChannelClaim
* preparePaymentChannelFund
### New in rippled 1.2.1
As this is the first release of ripple-lib following the release of rippled 1.2.1, we would like to highlight the following API improvements:
1. The [`delivered_amount` field](https://developers.ripple.com/partial-payments.html#the-delivered-amount-field) has been added to the `ledger` method, and to transaction subscriptions.
api.getLedger({includeTransactions: true, includeAllData: true, ledgerVersion: 17718771}).then(...)
You can also call `ledger` directly:
request('ledger', {...}).then(...)
2. [Support for Ed25519 seeds encoded using ripple-lib](https://github.com/ripple/rippled/pull/2734)
You have access to these improvements when you use a rippled server running version 1.2.1 or later. At the time of writing, we recommend using rippled version **1.2.2** or later.
### Improved `RippledError` `message`
Previously, `RippledErrors` (errors from rippled) used rippled's `error` field as the `message`.
Now, the `error_message` field is used as the `message`.
This helps to surface the specific cause of an error.
For example, before:
```
[RippledError(invalidParams, { error: 'invalidParams',
error_code: 31,
error_message: 'Missing field \'account\'.',
id: 3,
request: { command: 'account_info', id: 3 },
status: 'error',
type: 'response' })]
```
After:
```
[RippledError(Missing field 'account'., { error: 'invalidParams',
error_code: 31,
error_message: 'Missing field \'account\'.',
id: 3,
request: { command: 'account_info', id: 3 },
status: 'error',
type: 'response' })]
```
In this case, you can see at a glance that `account` is the missing field.
The `error` field is still available in `errorObject.data.error`.
When `error_message` is not set (as with e.g. error 'entryNotFound'), the `error` field is used as the `message`.
### [BUG FIX] `prepareTransaction` does not overwrite the `Sequence` field
The `prepareTransaction` method now allows `Sequence` to be set in the Transaction JSON object, instead of overwriting it with the account's expected sequence based on the state of the ledger.
Previously, you had to use the `sequence` field in the `instructions` object to manually set a transaction's sequence number.
## 1.1.2 (2018-12-12)
+ Update `submit` response (#978)

View File

@@ -1494,7 +1494,7 @@ sequence | [sequence](#account-sequence-number) | The account sequence number of
type | [transactionType](#transaction-types) | The type of the transaction.
specification | object | A specification that would produce the same outcome as this transaction. *Exception:* For payment transactions, this omits the `destination.amount` field, to prevent misunderstanding. The structure of the specification depends on the value of the `type` field (see [Transaction Types](#transaction-types) for details). *Note:* This is **not** necessarily the same as the original specification.
outcome | object | The outcome of the transaction (what effects it had).
*outcome.* result | string | Result code returned by rippled. See [Transaction Results](https://ripple.com/build/transactions/#full-transaction-response-list) for a complete list.
*outcome.* result | string | Result code returned by rippled. See [Transaction Results](https://developers.ripple.com/transaction-results.html) for a complete list.
*outcome.* fee | [value](#value) | The XRP fee that was charged for the transaction.
*outcome.balanceChanges.* \* | array\<[balance](#amount)\> | Key is the XRP Ledger address; value is an array of signed amounts representing changes of balances for that address.
*outcome.orderbookChanges.* \* | array | Key is the maker's XRP Ledger address; value is an array of changes
@@ -5491,7 +5491,7 @@ engine_result | string | Code indicating the preliminary result of the transacti
engine_result_code | integer | Numeric code indicating the preliminary result of the transaction, directly correlated to `engine_result`
engine_result_message | string | Human-readable explanation of the transaction's preliminary result.
tx_blob | string | The complete transaction in hex string format.
tx_json | [tx](https://ripple.com/build/transactions/) | The complete transaction in JSON format.
tx_json | [tx-json](https://developers.ripple.com/transaction-formats.html) | The complete transaction in JSON format.
### Example

View File

@@ -71,7 +71,7 @@ import * as transactionUtils from './transaction/utils'
import * as schemaValidator from './common/schema-validator'
import {getServerInfo, getFee} from './common/serverinfo'
import {clamp, renameCounterpartyToIssuer} from './ledger/utils'
import {Instructions, Prepare} from './transaction/types'
import {TransactionJSON, Instructions, Prepare} from './transaction/types'
export type APIOptions = {
server?: string,
@@ -210,7 +210,7 @@ class RippleAPI extends EventEmitter {
*
* You can later submit the transaction with `submit()`.
*/
async prepareTransaction(txJSON: object, instructions: Instructions = {}):
async prepareTransaction(txJSON: TransactionJSON, instructions: Instructions = {}):
Promise<Prepare> {
return transactionUtils.prepareTransaction(txJSON, this, instructions)
}

View File

@@ -445,7 +445,7 @@ class Connection extends EventEmitter {
this.once(eventName, response => {
if (response.status === 'error') {
_reject(new RippledError(response.error, response))
_reject(new RippledError(response.error_message || response.error, response))
} else if (response.status === 'success') {
_resolve(response.result)
} else {

View File

@@ -1,11 +1,12 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "tx",
"link": "https://ripple.com/build/transactions/",
"title": "tx-json",
"link": "https://developers.ripple.com/transaction-formats.html",
"description": "An object in rippled txJSON format",
"type": "object",
"properties": {
"Account": {"$ref": "address"}
"Account": {"$ref": "address"},
"TransactionType": {"type": "string"}
},
"required": ["Account"]
"required": ["Account", "TransactionType"]
}

View File

@@ -6,7 +6,7 @@
"properties": {
"result": {
"type": "string",
"description": "Result code returned by rippled. See [Transaction Results](https://ripple.com/build/transactions/#full-transaction-response-list) for a complete list."
"description": "Result code returned by rippled. See [Transaction Results](https://developers.ripple.com/transaction-results.html) for a complete list."
},
"timestamp": {
"type": "string",

View File

@@ -28,7 +28,7 @@
"description": "The complete transaction in hex string format."
},
"tx_json": {
"$ref": "tx",
"$ref": "tx-json",
"description": "The complete transaction in JSON format."
}
},

View File

@@ -124,3 +124,6 @@ _.partial(schemaValidate, 'api-options')
export const instructions =
_.partial(schemaValidate, 'instructions')
export const tx_json =
_.partial(schemaValidate, 'tx-json')

View File

@@ -46,9 +46,7 @@ function requestPathFind(connection: Connection, pathfind: PathFind
&& !request.destination_amount.issuer) {
// Convert blank issuer to sender's address
// (Ripple convention for 'any issuer')
// https://ripple.com/build/transactions/
// #special-issuer-values-for-sendmax-and-amount
// https://ripple.com/build/ripple-rest/#counterparties-in-payments
// https://developers.ripple.com/payment.html#special-issuer-values-for-sendmax-and-amount
request.destination_amount.issuer = request.destination_account
}
if (pathfind.source.currencies && pathfind.source.currencies.length > 0) {

View File

@@ -4,6 +4,7 @@ import parseTransaction from './parse/transaction'
import {validate, errors} from '../common'
import {Connection} from '../common'
import {FormattedTransactionType} from '../transaction/types'
import {RippledError} from '../common/errors'
export type TransactionOptions = {
minLedgerVersion?: number,
@@ -59,10 +60,16 @@ function isTransactionInRange(tx: any, options: TransactionOptions) {
}
function convertError(connection: Connection, options: TransactionOptions,
error: Error
error: RippledError
): Promise<Error> {
const _error = (error.message === 'txnNotFound') ?
new errors.NotFoundError('Transaction not found') : error
let shouldUseNotFoundError = false
if ((error.data && error.data.error === 'txnNotFound') || error.message === 'txnNotFound') {
shouldUseNotFoundError = true
}
// In the future, we should deprecate this error, instead passing through the one from rippled.
const _error = shouldUseNotFoundError ? new errors.NotFoundError('Transaction not found') : error
if (_error instanceof errors.NotFoundError) {
return utils.hasCompleteLedgerRange(connection, options.minLedgerVersion,
options.maxLedgerVersion).then(hasCompleteLedgerRange => {

View File

@@ -76,7 +76,7 @@ function signum(num) {
* Order two rippled transactions based on their ledger_index.
* If two transactions took place in the same ledger, sort
* them based on TransactionIndex
* See: https://ripple.com/build/transactions/
* See: https://developers.ripple.com/transaction-metadata.html
*/
function compareTransactions(
first: FormattedTransactionType, second: FormattedTransactionType

View File

@@ -1,14 +1,14 @@
import * as utils from './utils'
import {TransactionJSON, prepareTransaction} from './utils'
import {validate} from '../common'
import {Instructions, Prepare} from './types'
export type CheckCancel = {
export type CheckCancelParameters = {
checkID: string
}
function createCheckCancelTransaction(account: string,
cancel: CheckCancel
): object {
cancel: CheckCancelParameters
): TransactionJSON {
const txJSON = {
Account: account,
TransactionType: 'CheckCancel',
@@ -19,7 +19,7 @@ function createCheckCancelTransaction(account: string,
}
function prepareCheckCancel(address: string,
checkCancel: CheckCancel,
checkCancel: CheckCancelParameters,
instructions: Instructions = {}
): Promise<Prepare> {
try {
@@ -27,7 +27,7 @@ function prepareCheckCancel(address: string,
{address, checkCancel, instructions})
const txJSON = createCheckCancelTransaction(
address, checkCancel)
return utils.prepareTransaction(txJSON, this, instructions)
return prepareTransaction(txJSON, this, instructions)
} catch (e) {
return Promise.reject(e)
}

View File

@@ -2,18 +2,18 @@ import * as utils from './utils'
const ValidationError = utils.common.errors.ValidationError
const toRippledAmount = utils.common.toRippledAmount
import {validate} from '../common'
import {Instructions, Prepare} from './types'
import {Instructions, Prepare, TransactionJSON} from './types'
import {Amount} from '../common/types/objects'
export type CheckCash = {
export type CheckCashParameters = {
checkID: string,
amount?: Amount,
deliverMin?: Amount
}
function createCheckCashTransaction(account: string,
checkCash: CheckCash
): object {
checkCash: CheckCashParameters
): TransactionJSON {
if (checkCash.amount && checkCash.deliverMin) {
throw new ValidationError('"amount" and "deliverMin" properties on '
+ 'CheckCash are mutually exclusive')
@@ -37,7 +37,7 @@ function createCheckCashTransaction(account: string,
}
function prepareCheckCash(address: string,
checkCash: CheckCash,
checkCash: CheckCashParameters,
instructions: Instructions = {}
): Promise<Prepare> {
try {

View File

@@ -1,10 +1,10 @@
import * as utils from './utils'
const toRippledAmount = utils.common.toRippledAmount
import {validate, iso8601ToRippleTime} from '../common'
import {Instructions, Prepare} from './types'
import {Instructions, Prepare, TransactionJSON} from './types'
import {Amount} from '../common/types/objects'
export type CheckCreate = {
export type CheckCreateParameters = {
destination: string,
sendMax: Amount,
destinationTag?: number,
@@ -13,8 +13,8 @@ export type CheckCreate = {
}
function createCheckCreateTransaction(account: string,
check: CheckCreate
): object {
check: CheckCreateParameters
): TransactionJSON {
const txJSON: any = {
Account: account,
TransactionType: 'CheckCreate',
@@ -38,7 +38,7 @@ function createCheckCreateTransaction(account: string,
}
function prepareCheckCreate(address: string,
checkCreate: CheckCreate,
checkCreate: CheckCreateParameters,
instructions: Instructions = {}
): Promise<Prepare> {
try {

View File

@@ -1,18 +1,20 @@
import * as _ from 'lodash'
import * as utils from './utils'
const validate = utils.common.validate
import {Instructions, Prepare} from './types'
import {Instructions, Prepare, TransactionJSON} from './types'
import {Memo} from '../common/types/objects'
export type EscrowCancellation = {
owner: string,
escrowSequence: number,
// TODO: This ripple-lib memo format should be deprecated in favor of rippled's format.
// If necessary, expose a public method for converting between the two formats.
memos?: Array<Memo>
}
function createEscrowCancellationTransaction(account: string,
payment: EscrowCancellation
): object {
): TransactionJSON {
const txJSON: any = {
TransactionType: 'EscrowCancel',
Account: account,
@@ -20,7 +22,7 @@ function createEscrowCancellationTransaction(account: string,
OfferSequence: payment.escrowSequence
}
if (payment.memos !== undefined) {
txJSON.Memos = _.map(payment.memos, utils.convertMemo)
txJSON.Memos = payment.memos.map(utils.convertMemo)
}
return txJSON
}

View File

@@ -1,8 +1,7 @@
import * as _ from 'lodash'
import * as utils from './utils'
import {validate, iso8601ToRippleTime, xrpToDrops} from '../common'
const ValidationError = utils.common.errors.ValidationError
import {Instructions, Prepare} from './types'
import {Instructions, Prepare, TransactionJSON} from './types'
import {Memo} from '../common/types/objects'
export type EscrowCreation = {
@@ -18,7 +17,7 @@ export type EscrowCreation = {
function createEscrowCreationTransaction(account: string,
payment: EscrowCreation
): object {
): TransactionJSON {
const txJSON: any = {
TransactionType: 'EscrowCreate',
Account: account,
@@ -42,7 +41,7 @@ function createEscrowCreationTransaction(account: string,
txJSON.DestinationTag = payment.destinationTag
}
if (payment.memos !== undefined) {
txJSON.Memos = _.map(payment.memos, utils.convertMemo)
txJSON.Memos = payment.memos.map(utils.convertMemo)
}
if (Boolean(payment.allowCancelAfter) && Boolean(payment.allowExecuteAfter) &&
txJSON.CancelAfter <= txJSON.FinishAfter) {

View File

@@ -1,4 +1,3 @@
import * as _ from 'lodash'
import * as utils from './utils'
const validate = utils.common.validate
const ValidationError = utils.common.errors.ValidationError
@@ -15,7 +14,7 @@ export type EscrowExecution = {
function createEscrowExecutionTransaction(account: string,
payment: EscrowExecution
): object {
): utils.TransactionJSON {
const txJSON: any = {
TransactionType: 'EscrowFinish',
Account: account,
@@ -35,7 +34,7 @@ function createEscrowExecutionTransaction(account: string,
txJSON.Fulfillment = payment.fulfillment
}
if (payment.memos !== undefined) {
txJSON.Memos = _.map(payment.memos, utils.convertMemo)
txJSON.Memos = payment.memos.map(utils.convertMemo)
}
return txJSON
}

View File

@@ -1,4 +1,3 @@
import * as _ from 'lodash'
import * as utils from './utils'
const offerFlags = utils.common.txFlags.OfferCreate
import {validate, iso8601ToRippleTime} from '../common'
@@ -39,7 +38,7 @@ function createOrderTransaction(
txJSON.OfferSequence = order.orderToReplace
}
if (order.memos !== undefined) {
txJSON.Memos = _.map(order.memos, utils.convertMemo)
txJSON.Memos = order.memos.map(utils.convertMemo)
}
return txJSON as OfferCreateTransaction
}

View File

@@ -1,18 +1,17 @@
import * as _ from 'lodash'
import * as utils from './utils'
const validate = utils.common.validate
import {Instructions, Prepare} from './types'
import {Instructions, Prepare, TransactionJSON} from './types'
function createOrderCancellationTransaction(account: string,
orderCancellation: any
): object {
): TransactionJSON {
const txJSON: any = {
TransactionType: 'OfferCancel',
Account: account,
OfferSequence: orderCancellation.orderSequence
}
if (orderCancellation.memos !== undefined) {
txJSON.Memos = _.map(orderCancellation.memos, utils.convertMemo)
txJSON.Memos = orderCancellation.memos.map(utils.convertMemo)
}
return txJSON
}

View File

@@ -16,8 +16,8 @@ export type PaymentChannelClaim = {
function createPaymentChannelClaimTransaction(account: string,
claim: PaymentChannelClaim
): object {
const txJSON: any = {
): utils.TransactionJSON {
const txJSON: utils.TransactionJSON = {
Account: account,
TransactionType: 'PaymentChannelClaim',
Channel: claim.channel,

View File

@@ -14,7 +14,7 @@ export type PaymentChannelCreate = {
function createPaymentChannelCreateTransaction(account: string,
paymentChannel: PaymentChannelCreate
): object {
): utils.TransactionJSON {
const txJSON: any = {
Account: account,
TransactionType: 'PaymentChannelCreate',

View File

@@ -10,8 +10,8 @@ export type PaymentChannelFund = {
function createPaymentChannelFundTransaction(account: string,
fund: PaymentChannelFund
): object {
const txJSON: any = {
): utils.TransactionJSON {
const txJSON: utils.TransactionJSON = {
Account: account,
TransactionType: 'PaymentChannelFund',
Channel: fund.channel,

View File

@@ -4,7 +4,7 @@ const validate = utils.common.validate
const toRippledAmount = utils.common.toRippledAmount
const paymentFlags = utils.common.txFlags.Payment
const ValidationError = utils.common.errors.ValidationError
import {Instructions, Prepare} from './types'
import {Instructions, Prepare, TransactionJSON} from './types'
import {Amount, Adjustment, MaxAdjustment,
MinAdjustment, Memo} from '../common/types/objects'
import {xrpToDrops} from '../common'
@@ -59,9 +59,7 @@ function isIOUWithoutCounterparty(amount: Amount): boolean {
function applyAnyCounterpartyEncoding(payment: Payment): void {
// Convert blank counterparty to sender or receiver's address
// (Ripple convention for 'any counterparty')
// https://ripple.com/build/transactions/
// #special-issuer-values-for-sendmax-and-amount
// https://ripple.com/build/ripple-rest/#counterparties-in-payments
// https://developers.ripple.com/payment.html#special-issuer-values-for-sendmax-and-amount
_.forEach([payment.source, payment.destination], adjustment => {
_.forEach(['amount', 'minAmount', 'maxAmount'], key => {
if (isIOUWithoutCounterparty(adjustment[key])) {
@@ -86,7 +84,7 @@ function createMaximalAmount(amount: Amount): Amount {
}
function createPaymentTransaction(address: string, paymentArgument: Payment
): object {
): TransactionJSON {
const payment = _.cloneDeep(paymentArgument)
applyAnyCounterpartyEncoding(payment)

View File

@@ -1,17 +1,13 @@
import * as _ from 'lodash'
import * as assert from 'assert'
import BigNumber from 'bignumber.js'
import * as utils from './utils'
const validate = utils.common.validate
const AccountFlagIndices = utils.common.constants.AccountFlagIndices
const AccountFields = utils.common.constants.AccountFields
import {Instructions, Prepare} from './types'
import {Instructions, Prepare, SettingsTransaction} from './types'
import {FormattedSettings, WeightedSigner} from '../common/types/objects'
// Empty string passed to setting will clear it
const CLEAR_SETTING = null
function setTransactionFlags(txJSON: any, values: FormattedSettings) {
function setTransactionFlags(txJSON: utils.TransactionJSON, values: FormattedSettings) {
const keys = Object.keys(values)
assert(keys.length === 1, 'ERROR: can only set one setting per transaction')
const flagName = keys[0]
@@ -26,7 +22,8 @@ function setTransactionFlags(txJSON: any, values: FormattedSettings) {
}
}
function setTransactionFields(txJSON: object, input: FormattedSettings) {
// Sets `null` fields to their `default`.
function setTransactionFields(txJSON: utils.TransactionJSON, input: FormattedSettings) {
const fieldSchema = AccountFields
for (const fieldName in fieldSchema) {
const field = fieldSchema[fieldName]
@@ -37,7 +34,7 @@ function setTransactionFields(txJSON: object, input: FormattedSettings) {
}
// The value required to clear an account root field varies
if (value === CLEAR_SETTING && field.hasOwnProperty('defaults')) {
if (value === null && field.hasOwnProperty('defaults')) {
value = field.defaults
}
@@ -63,7 +60,7 @@ function setTransactionFields(txJSON: object, input: FormattedSettings) {
* are returned
*/
function convertTransferRate(transferRate: number | string): number | string {
function convertTransferRate(transferRate: number): number {
return (new BigNumber(transferRate)).shift(9).toNumber()
}
@@ -78,7 +75,7 @@ function formatSignerEntry(signer: WeightedSigner): object {
function createSettingsTransactionWithoutMemos(
account: string, settings: FormattedSettings
): any {
): SettingsTransaction {
if (settings.regularKey !== undefined) {
const removeRegularKey = {
TransactionType: 'SetRegularKey',
@@ -87,7 +84,7 @@ function createSettingsTransactionWithoutMemos(
if (settings.regularKey === null) {
return removeRegularKey
}
return _.assign({}, removeRegularKey, {RegularKey: settings.regularKey})
return Object.assign({}, removeRegularKey, {RegularKey: settings.regularKey})
}
if (settings.signers !== undefined) {
@@ -95,17 +92,19 @@ function createSettingsTransactionWithoutMemos(
TransactionType: 'SignerListSet',
Account: account,
SignerQuorum: settings.signers.threshold,
SignerEntries: _.map(settings.signers.weights, formatSignerEntry)
SignerEntries: settings.signers.weights.map(formatSignerEntry)
}
}
const txJSON: any = {
const txJSON: SettingsTransaction = {
TransactionType: 'AccountSet',
Account: account
}
setTransactionFlags(txJSON, _.omit(settings, 'memos'))
setTransactionFields(txJSON, settings)
const settingsWithoutMemos = Object.assign({}, settings)
delete settingsWithoutMemos.memos
setTransactionFlags(txJSON, settingsWithoutMemos)
setTransactionFields(txJSON, settings) // Sets `null` fields to their `default`.
if (txJSON.TransferRate !== undefined) {
txJSON.TransferRate = convertTransferRate(txJSON.TransferRate)
@@ -114,10 +113,10 @@ function createSettingsTransactionWithoutMemos(
}
function createSettingsTransaction(account: string, settings: FormattedSettings
): object {
): SettingsTransaction {
const txJSON = createSettingsTransactionWithoutMemos(account, settings)
if (settings.memos !== undefined) {
txJSON.Memos = _.map(settings.memos, utils.convertMemo)
txJSON.Memos = settings.memos.map(utils.convertMemo)
}
return txJSON
}

View File

@@ -1,9 +1,8 @@
import * as _ from 'lodash'
import BigNumber from 'bignumber.js'
import * as utils from './utils'
const validate = utils.common.validate
const trustlineFlags = utils.common.txFlags.TrustSet
import {Instructions, Prepare} from './types'
import {Instructions, Prepare, TransactionJSON} from './types'
import {
FormattedTrustlineSpecification
} from '../common/types/objects/trustlines'
@@ -14,7 +13,7 @@ function convertQuality(quality) {
function createTrustlineTransaction(account: string,
trustline: FormattedTrustlineSpecification
): object {
): TransactionJSON {
const limit = {
currency: trustline.currency,
issuer: trustline.counterparty,
@@ -45,7 +44,7 @@ function createTrustlineTransaction(account: string,
trustlineFlags.SetFreeze : trustlineFlags.ClearFreeze
}
if (trustline.memos !== undefined) {
txJSON.Memos = _.map(trustline.memos, utils.convertMemo)
txJSON.Memos = trustline.memos.map(utils.convertMemo)
}
return txJSON
}

View File

@@ -7,7 +7,12 @@ import {
Memo,
FormattedSettings
} from '../common/types/objects'
import {ApiMemo} from './utils'
import {
ApiMemo,
TransactionJSON
} from './utils'
export type TransactionJSON = TransactionJSON
export type Instructions = {
sequence?: number,
@@ -37,7 +42,7 @@ export type Submit = {
txJson?: object
}
export interface OfferCreateTransaction {
export interface OfferCreateTransaction extends TransactionJSON {
TransactionType: 'OfferCreate',
Account: string,
Fee: string,
@@ -48,7 +53,11 @@ export interface OfferCreateTransaction {
TakerPays: RippledAmount,
Expiration?: number,
OfferSequence?: number,
Memos: {Memo: ApiMemo}[]
Memos?: {Memo: ApiMemo}[]
}
export interface SettingsTransaction extends TransactionJSON {
TransferRate?: number
}
export type KeyPair = {

View File

@@ -1,6 +1,6 @@
import BigNumber from 'bignumber.js'
import * as common from '../common'
import {Memo} from '../common/types/objects'
import {Memo, RippledAmount} from '../common/types/objects'
const txFlags = common.txFlags
import {Instructions, Prepare} from './types'
import {RippleAPI} from '../api'
@@ -12,6 +12,15 @@ export type ApiMemo = {
MemoFormat?: string
}
export type TransactionJSON = {
Account: string,
TransactionType: string,
Memos?: {Memo: ApiMemo}[],
Flags?: number,
Fulfillment?: string,
[Field: string]: string | number | Array<any> | RippledAmount
}
function formatPrepareResponse(txJSON: any): Prepare {
const instructions = {
fee: common.dropsToXrp(txJSON.Fee),
@@ -37,10 +46,11 @@ function scaleValue(value, multiplier, extra = 0) {
return (new BigNumber(value)).times(multiplier).plus(extra).toString()
}
function prepareTransaction(txJSON: any, api: RippleAPI,
function prepareTransaction(txJSON: TransactionJSON, api: RippleAPI,
instructions: Instructions
): Promise<Prepare> {
common.validate.instructions(instructions)
common.validate.tx_json(txJSON)
const account = txJSON.Account
setCanonicalFlag(txJSON)
@@ -96,14 +106,28 @@ function prepareTransaction(txJSON: any, api: RippleAPI,
async function prepareSequence(): Promise<object> {
if (instructions.sequence !== undefined) {
txJSON.Sequence = instructions.sequence
if (txJSON.Sequence === undefined || instructions.sequence === txJSON.Sequence) {
txJSON.Sequence = instructions.sequence
return Promise.resolve(txJSON)
} else {
// Both txJSON.Sequence and instructions.sequence are defined, and they are NOT equal
return Promise.reject(new ValidationError('`Sequence` in txJSON must match `sequence` in Instructions'))
}
}
if (txJSON.Sequence !== undefined) {
return Promise.resolve(txJSON)
}
const response = await api.request('account_info', {
account: account as string
})
txJSON.Sequence = response.account_data.Sequence
return txJSON
try {
// Consider requesting from the 'current' ledger (instead of 'validated').
const response = await api.request('account_info', {
account
})
txJSON.Sequence = response.account_data.Sequence
return Promise.resolve(txJSON)
} catch (e) {
return Promise.reject(e)
}
}
return Promise.all([

View File

@@ -371,6 +371,389 @@ describe('RippleAPI', function () {
});
describe('prepareTransaction - auto-fillable fields', function () {
it('does not overwrite Sequence in txJSON', function () {
const localInstructions = _.defaults({
maxFee: '0.000012'
}, instructions)
const txJSON = {
TransactionType: 'DepositPreauth',
Account: address,
Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo',
Sequence: 100
}
return this.api.prepareTransaction(txJSON, localInstructions).then(response => {
const expected = {
txJSON: '{"TransactionType":"DepositPreauth","Account":"' + address + '","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"12","Sequence":100}',
instructions: {
fee: '0.000012',
sequence: 100,
maxLedgerVersion: 8820051
}
}
return checkResult(expected, 'prepare', response)
})
})
it('does not overwrite Sequence in Instructions', function () {
const localInstructions = _.defaults({
maxFee: '0.000012',
sequence: 100
}, instructions)
const txJSON = {
TransactionType: 'DepositPreauth',
Account: address,
Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo'
}
return this.api.prepareTransaction(txJSON, localInstructions).then(response => {
const expected = {
txJSON: '{"TransactionType":"DepositPreauth","Account":"' + address + '","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"12","Sequence":100}',
instructions: {
fee: '0.000012',
sequence: 100,
maxLedgerVersion: 8820051
}
}
return checkResult(expected, 'prepare', response)
})
})
it('does not overwrite Sequence when same sequence is provided in both txJSON and Instructions', function () {
const localInstructions = _.defaults({
maxFee: '0.000012',
sequence: 100
}, instructions)
const txJSON = {
TransactionType: 'DepositPreauth',
Account: address,
Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo',
Sequence: 100
}
return this.api.prepareTransaction(txJSON, localInstructions).then(response => {
const expected = {
txJSON: '{"TransactionType":"DepositPreauth","Account":"' + address + '","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"12","Sequence":100}',
instructions: {
fee: '0.000012',
sequence: 100,
maxLedgerVersion: 8820051
}
}
return checkResult(expected, 'prepare', response)
})
})
it('rejects Promise when Sequence in txJSON does not match sequence in Instructions', function (done) {
const localInstructions = _.defaults({
maxFee: '0.000012',
sequence: 100
}, instructions)
const txJSON = {
TransactionType: 'DepositPreauth',
Account: address,
Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo',
Sequence: 101
}
try {
this.api.prepareTransaction(txJSON, localInstructions).then(response => {
done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response)));
}).catch(err => {
assert.strictEqual(err.name, 'ValidationError');
assert.strictEqual(err.message, '`Sequence` in txJSON must match `sequence` in Instructions');
done();
}).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout.
} catch (err) {
done(new Error('Expected method to reject, but method threw. Thrown: ' + err));
}
})
it('rejects Promise when the Sequence is capitalized in Instructions', function (done) {
const localInstructions = _.defaults({
maxFee: '0.000012',
Sequence: 100 // Intentionally capitalized in this test, but the correct field would be `sequence`
}, instructions)
const txJSON = {
TransactionType: 'DepositPreauth',
Account: address,
Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo'
}
try {
this.api.prepareTransaction(txJSON, localInstructions).then(response => {
done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response)));
}).catch(err => {
assert.strictEqual(err.name, 'ValidationError');
assert.strictEqual(err.message, 'instance additionalProperty "Sequence" exists in instance when not allowed');
done();
}).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout.
} catch (err) {
done(new Error('Expected method to reject, but method threw. Thrown: ' + err));
}
})
it('rejects Promise when an unrecognized field is in Instructions', function (done) {
const localInstructions = _.defaults({
maxFee: '0.000012',
foo: 'bar'
}, instructions)
const txJSON = {
TransactionType: 'DepositPreauth',
Account: address,
Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo'
}
try {
this.api.prepareTransaction(txJSON, localInstructions).then(response => {
done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response)));
}).catch(err => {
assert.strictEqual(err.name, 'ValidationError');
assert.strictEqual(err.message, 'instance additionalProperty "foo" exists in instance when not allowed');
done();
}).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout.
} catch (err) {
done(new Error('Expected method to reject, but method threw. Thrown: ' + err));
}
})
})
it('rejects Promise when Account is missing', function (done) {
const localInstructions = _.defaults({
maxFee: '0.000012'
}, instructions)
const txJSON = {
TransactionType: 'DepositPreauth',
Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo'
}
try {
this.api.prepareTransaction(txJSON, localInstructions).then(response => {
done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response)));
}).catch(err => {
// assert.strictEqual(err.name, 'RippledError');
// assert.strictEqual(err.message, 'Missing field \'account\'.');
// assert.strictEqual(err.data.error, 'invalidParams');
assert.strictEqual(err.name, 'ValidationError');
assert.strictEqual(err.message, 'instance requires property "Account"');
done();
}).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout.
} catch (err) {
done(new Error('Expected method to reject, but method threw. Thrown: ' + err));
}
})
it('rejects Promise when Account is not a string', function (done) {
const localInstructions = _.defaults({
maxFee: '0.000012'
}, instructions)
const txJSON = {
Account: 1234,
TransactionType: 'DepositPreauth',
Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo'
}
try {
this.api.prepareTransaction(txJSON, localInstructions).then(response => {
done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response)));
}).catch(err => {
assert.strictEqual(err.name, 'ValidationError');
assert.strictEqual(err.message, 'instance.Account is not of a type(s) string,instance.Account does not conform to the "address" format');
done();
}).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout.
} catch (err) {
done(new Error('Expected method to reject, but method threw. Thrown: ' + err));
}
})
it('rejects Promise when Account is invalid', function (done) {
const localInstructions = _.defaults({
maxFee: '0.000012'
}, instructions)
const txJSON = {
Account: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xkXXXX', // Invalid checksum
TransactionType: 'DepositPreauth',
Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo'
}
try {
this.api.prepareTransaction(txJSON, localInstructions).then(response => {
done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response)));
}).catch(err => {
assert.strictEqual(err.name, 'ValidationError');
assert.strictEqual(err.message, 'instance.Account does not conform to the "address" format');
done();
}).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout.
} catch (err) {
done(new Error('Expected method to reject, but method threw. Thrown: ' + err));
}
})
it('rejects Promise when Account is valid but non-existent on the ledger', function (done) {
const localInstructions = _.defaults({
maxFee: '0.000012'
}, instructions)
const txJSON = {
Account: 'rogvkYnY8SWjxkJNgU4ZRVfLeRyt5DR9i',
TransactionType: 'DepositPreauth',
Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo'
}
try {
this.api.prepareTransaction(txJSON, localInstructions).then(response => {
done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response)));
}).catch(err => {
assert.strictEqual(err.name, 'RippledError');
assert.strictEqual(err.message, 'Account not found.');
done();
}).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout.
} catch (err) {
done(new Error('Expected method to reject, but method threw. Thrown: ' + err));
}
})
it('rejects Promise when TransactionType is missing', function (done) {
const localInstructions = _.defaults({
maxFee: '0.000012'
}, instructions)
const txJSON = {
Account: address,
Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo'
}
try {
this.api.prepareTransaction(txJSON, localInstructions).then(response => {
done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response)));
}).catch(err => {
// If not caught by ripple-lib validation, the rippled error looks like:
// { error: 'invalidTransaction',
// error_exception: 'Field not found',
// id: 4,
// request:
// { command: 'submit',
// id: 4,
// tx_blob: '24000000032B7735940068400000000000000C732102E1EA8199F570E7F997A7B34EDFDA0A7D8B38173A17450B121A2EB048FDD16CA97446304402206CE34A79A44AEF15786F23DB25C8420E739C167E66750C0B7999EE4BF74A93A1022052E077A6435548F0EE0C5FE2EAB1E5A56376BA360F924DA2E162CCA6C7CB30CB8114D51F9A17208CF113AF23B97ECD5FCD314FBAE52E' },
// status: 'error',
// type: 'response' }
assert.strictEqual(err.name, 'ValidationError');
assert.strictEqual(err.message, 'instance requires property "TransactionType"');
done();
}).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout.
} catch (err) {
done(new Error('Expected method to reject, but method threw. Thrown: ' + err));
}
})
// Note: This transaction will fail at the `sign` step:
//
// Error: DepositPreXXXX is not a valid name or ordinal for TransactionType
//
// at Function.from (ripple-binary-codec/distrib/npm/enums/index.js:43:15)
it('prepares tx when TransactionType is invalid', function () {
const localInstructions = _.defaults({
maxFee: '0.000012'
}, instructions)
const txJSON = {
Account: address,
TransactionType: 'DepositPreXXXX',
Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo'
}
return this.api.prepareTransaction(txJSON, localInstructions).then(response => {
const expected = {
txJSON: '{"TransactionType":"DepositPreXXXX","Account":"' + address + '","Authorize":"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"12","Sequence":23}',
instructions: {
fee: '0.000012',
sequence: 23,
maxLedgerVersion: 8820051
}
}
return checkResult(expected, 'prepare', response)
})
})
it('rejects Promise when TransactionType is not a string', function (done) {
const localInstructions = _.defaults({
maxFee: '0.000012'
}, instructions)
const txJSON = {
Account: address,
TransactionType: 1234,
Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo'
}
try {
this.api.prepareTransaction(txJSON, localInstructions).then(response => {
done(new Error('Expected method to reject. Prepared transaction: ' + JSON.stringify(response)));
}).catch(err => {
assert.strictEqual(err.name, 'ValidationError');
assert.strictEqual(err.message, 'instance.TransactionType is not of a type(s) string');
done();
}).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout.
} catch (err) {
done(new Error('Expected method to reject, but method threw. Thrown: ' + err));
}
})
// Note: This transaction will fail at the `submit` step:
//
// [RippledError(Submit failed, { resultCode: 'temMALFORMED',
// resultMessage: 'Malformed transaction.',
// engine_result: 'temMALFORMED',
// engine_result_code: -299,
// engine_result_message: 'Malformed transaction.',
// tx_blob:
// '120013240000000468400000000000000C732102E1EA8199F570E7F997A7B34EDFDA0A7D8B38173A17450B121A2EB048FDD16CA97446304402201F0EF6A2DE7F96966F7082294D14F3EC1EF59C21E29443E5858A0120079357A302203CDB7FEBDEAAD93FF39CB589B55778CB80DC3979F96F27E828D5E659BEB26B7A8114D51F9A17208CF113AF23B97ECD5FCD314FBAE52E',
// tx_json:
// { Account: 'rLRt8bmZFBEeM5VMSxZy15k8KKJEs68W6C',
// Fee: '12',
// Sequence: 4,
// SigningPubKey:
// '02E1EA8199F570E7F997A7B34EDFDA0A7D8B38173A17450B121A2EB048FDD16CA9',
// TransactionType: 'DepositPreauth',
// TxnSignature:
// '304402201F0EF6A2DE7F96966F7082294D14F3EC1EF59C21E29443E5858A0120079357A302203CDB7FEBDEAAD93FF39CB589B55778CB80DC3979F96F27E828D5E659BEB26B7A',
// hash:
// 'C181D470684311658852713DA81F8201062535C8DE2FF853F7DD9981BB85312F' } })]
it('prepares tx when a required field is missing', function () {
const localInstructions = _.defaults({
maxFee: '0.000012'
}, instructions)
const txJSON = {
Account: address,
TransactionType: 'DepositPreauth',
// Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo' // Normally required, intentionally removed
}
return this.api.prepareTransaction(txJSON, localInstructions).then(response => {
const expected = {
txJSON: '{"TransactionType":"DepositPreauth","Account":"' + address + '","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"12","Sequence":23}',
instructions: {
fee: '0.000012',
sequence: 23,
maxLedgerVersion: 8820051
}
}
return checkResult(expected, 'prepare', response)
})
})
describe('preparePayment', function () {
it('normal', function () {
@@ -2041,10 +2424,12 @@ describe('RippleAPI', function () {
it('getTransaction - not validated', function () {
const hash =
'4FB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA10';
return this.api.getTransaction(hash).then(() => {
return this.api.getTransaction(hash).then((response) => {
console.log(response);
assert(false, 'Should throw NotFoundError');
}).catch(error => {
assert(error instanceof this.api.errors.NotFoundError);
assert.equal(error.message, 'Transaction not found');
});
});
@@ -2360,7 +2745,8 @@ describe('RippleAPI', function () {
start: hashes.NOTFOUND_TRANSACTION_HASH,
counterparty: address
};
return this.api.getTransactions(address, options).then(() => {
return this.api.getTransactions(address, options).then((response) => {
console.log(response);
assert(false, 'Should throw NotFoundError');
}).catch(error => {
assert(error instanceof this.api.errors.NotFoundError);
@@ -3006,7 +3392,8 @@ describe('RippleAPI', function () {
assert(false, 'Should throw entryNotFound');
}).catch(error => {
assert(error instanceof this.api.errors.RippledError);
assert(_.includes(error.message, 'entryNotFound'));
assert.equal(error.message, 'entryNotFound');
assert.equal(error.data.error, 'entryNotFound');
});
});
@@ -3037,7 +3424,8 @@ describe('RippleAPI', function () {
assert(false, 'Should throw NetworkError');
}).catch(error => {
assert(error instanceof this.api.errors.RippledError);
assert(_.includes(error.message, 'slowDown'));
assert.equal(error.message, 'You are placing too much load on the server.');
assert.equal(error.data.error, 'slowDown');
});
});

View File

@@ -396,7 +396,8 @@ describe('Connection', function() {
it('propagates RippledError data', function(done) {
this.api.request('subscribe', {streams: 'validations'}).catch(error => {
assert.strictEqual(error.name, 'RippledError')
assert.strictEqual(error.message, 'invalidParams')
assert.strictEqual(error.data.error, 'invalidParams')
assert.strictEqual(error.message, 'Invalid parameters.')
assert.strictEqual(error.data.error_code, 31)
assert.strictEqual(error.data.error_message, 'Invalid parameters.')
assert.deepEqual(error.data.request, { command: 'subscribe', id: 0, streams: 'validations' })

View File

@@ -238,11 +238,40 @@ module.exports = function createMockRippled(port) {
} else if (request.account === addresses.NOTFOUND) {
conn.send(createResponse(request, fixtures.account_info.notfound));
} else if (request.account === addresses.THIRD_ACCOUNT) {
const response = _.assign({}, fixtures.account_info.normal);
const response = Object.assign({}, fixtures.account_info.normal);
response.Account = addresses.THIRD_ACCOUNT;
conn.send(createResponse(request, response));
} else if (request.account === undefined) {
const response = Object.assign({}, {
error: 'invalidParams',
error_code: 31,
error_message: 'Missing field \'account\'.',
id: 2,
request: { command: 'account_info', id: 2 },
status: 'error',
type: 'response'
});
conn.send(createResponse(request, response));
} else {
assert(false, 'Unrecognized account address: ' + request.account);
const response = Object.assign({}, {
account: request.account,
error: 'actNotFound',
error_code: 19,
error_message: 'Account not found.',
id: 2,
ledger_current_index: 17714714,
request:
// This will be inaccurate, but that's OK because this is just a mock rippled
{ account: 'rogvkYnY8SWjxkJNgU4ZRVfLeRyt5DR9i',
command: 'account_info',
id: 2 },
status: 'error',
type: 'response',
validated: false
});
conn.send(createResponse(request, response));
}
});