mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-11-04 21:15:47 +00:00
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:
78
HISTORY.md
78
HISTORY.md
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
},
|
||||
|
||||
@@ -124,3 +124,6 @@ _.partial(schemaValidate, 'api-options')
|
||||
|
||||
export const instructions =
|
||||
_.partial(schemaValidate, 'instructions')
|
||||
|
||||
export const tx_json =
|
||||
_.partial(schemaValidate, 'tx-json')
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -14,7 +14,7 @@ export type PaymentChannelCreate = {
|
||||
|
||||
function createPaymentChannelCreateTransaction(account: string,
|
||||
paymentChannel: PaymentChannelCreate
|
||||
): object {
|
||||
): utils.TransactionJSON {
|
||||
const txJSON: any = {
|
||||
Account: account,
|
||||
TransactionType: 'PaymentChannelCreate',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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([
|
||||
|
||||
396
test/api-test.js
396
test/api-test.js
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user