Add support for the X-address format (#1041)

* Update schema-validator

* Update to ripple-address-codec 4.0.0

* Update ./src/common/hashes/index.ts

* Add generateXAddress method

* Deprecate generateAddress method

* Add unit tests

* Add documentation
This commit is contained in:
Elliot Lee
2019-10-23 12:03:59 -07:00
committed by GitHub
parent 3a20123e0f
commit e1964ac5ed
40 changed files with 4704 additions and 111 deletions

View File

@@ -7,7 +7,8 @@ import {
dropsToXrp,
rippleTimeToISO8601,
iso8601ToRippleTime,
txFlags
txFlags,
ensureClassicAddress
} from './common'
import {
connect,
@@ -46,8 +47,8 @@ import prepareSettings from './transaction/settings'
import sign from './transaction/sign'
import combine from './transaction/combine'
import submit from './transaction/submit'
import {generateAddressAPI} from './offline/generate-address'
import {deriveKeypair, deriveAddress} from './offline/derive'
import {generateAddressAPI, GenerateAddressOptions, GeneratedAddress} from './offline/generate-address'
import {deriveKeypair, deriveAddress, deriveXAddress} from './offline/derive'
import computeLedgerHash from './offline/ledgerhash'
import signPaymentChannelClaim from './offline/sign-payment-channel-claim'
import verifyPaymentChannelClaim from './offline/verify-payment-channel-claim'
@@ -74,6 +75,7 @@ import {getServerInfo, getFee} from './common/serverinfo'
import {clamp, renameCounterpartyToIssuer} from './ledger/utils'
import {TransactionJSON, Instructions, Prepare} from './transaction/types'
import {ConnectionOptions} from './common/connection'
import {isValidXAddress, isValidClassicAddress} from 'ripple-address-codec'
export interface APIOptions extends ConnectionOptions {
server?: string,
@@ -175,7 +177,8 @@ class RippleAPI extends EventEmitter {
async request(command: string, params: any = {}): Promise<any> {
return this.connection.request({
...params,
command
command,
account: params.account ? ensureClassicAddress(params.account) : undefined
})
}
@@ -288,6 +291,15 @@ class RippleAPI extends EventEmitter {
return results
}
// @deprecated Use X-addresses instead
generateAddress(options: GenerateAddressOptions = {}): GeneratedAddress {
return generateAddressAPI({...options, includeClassicAddress: true})
}
generateXAddress(options: GenerateAddressOptions = {}): GeneratedAddress {
return generateAddressAPI(options)
}
connect = connect
disconnect = disconnect
isConnected = isConnected
@@ -328,7 +340,6 @@ class RippleAPI extends EventEmitter {
combine = combine
submit = submit
generateAddress = generateAddressAPI
deriveKeypair = deriveKeypair
deriveAddress = deriveAddress
computeLedgerHash = computeLedgerHash
@@ -336,6 +347,14 @@ class RippleAPI extends EventEmitter {
verifyPaymentChannelClaim = verifyPaymentChannelClaim
errors = errors
static deriveXAddress = deriveXAddress
// RippleAPI.deriveClassicAddress (static) is a new name for api.deriveAddress
static deriveClassicAddress = deriveAddress
static isValidXAddress = isValidXAddress
static isValidClassicAddress = isValidClassicAddress
xrpToDrops = xrpToDrops
dropsToXrp = dropsToXrp
rippleTimeToISO8601 = rippleTimeToISO8601

View File

@@ -1,5 +1,5 @@
import BigNumber from 'bignumber.js'
import {decodeAddress} from 'ripple-address-codec'
import {decodeAccountID} from 'ripple-address-codec'
import sha512Half from './sha512Half'
import HashPrefix from './hash-prefix'
import {SHAMap, NodeType} from './shamap'
@@ -28,12 +28,12 @@ const ledgerSpaceHex = (name: string): string => {
}
const addressToHex = (address: string): string => {
return (Buffer.from(decodeAddress(address))).toString('hex')
return (Buffer.from(decodeAccountID(address))).toString('hex')
}
const currencyToHex = (currency: string): string => {
if (currency.length === 3) {
let bytes = new Array(20 + 1).join('0').split('').map(parseFloat)
const bytes = new Array(20 + 1).join('0').split('').map(parseFloat)
bytes[12] = currency.charCodeAt(0) & 0xff
bytes[13] = currency.charCodeAt(1) & 0xff
bytes[14] = currency.charCodeAt(2) & 0xff
@@ -81,7 +81,7 @@ export const computeAccountHash = (address: string): string => {
export const computeSignerListHash = (address: string): string => {
return sha512Half(ledgerSpaceHex('signerList') +
addressToHex(address) +
'00000000' /* uint32(0) signer list index */)
'00000000') // uint32(0) signer list index
}
export const computeOrderHash = (address: string, sequence: number): string => {
@@ -106,7 +106,7 @@ export const computeTrustlineHash = (address1: string, address2: string, currenc
export const computeTransactionTreeHash = (transactions: any[]): string => {
const shamap = new SHAMap()
transactions.forEach((txJSON) => {
transactions.forEach(txJSON => {
const txBlobHex = encode(txJSON)
const metaHex = encode(txJSON.metaData)
const txHash = computeBinaryTransactionHash(txBlobHex)
@@ -120,7 +120,7 @@ export const computeTransactionTreeHash = (transactions: any[]): string => {
export const computeStateTreeHash = (entries: any[]): string => {
const shamap = new SHAMap()
entries.forEach((ledgerEntry) => {
entries.forEach(ledgerEntry => {
const data = encode(ledgerEntry)
shamap.addItem(ledgerEntry.index, data, NodeType.ACCOUNT_STATE)
})

View File

@@ -2,13 +2,35 @@ import * as constants from './constants'
import * as errors from './errors'
import * as validate from './validate'
import * as serverInfo from './serverinfo'
import {xAddressToClassicAddress, isValidXAddress} from 'ripple-address-codec'
export function ensureClassicAddress(account: string): string {
if (isValidXAddress(account)) {
const {
classicAddress,
tag
} = xAddressToClassicAddress(account)
// Except for special cases, X-addresses used for requests
// must not have an embedded tag. In other words,
// `tag` should be `false`.
if (tag !== false) {
throw new Error('This command does not support the use of a tag. Use an address without a tag.')
}
// For rippled requests that use an account, always use a classic address.
return classicAddress
} else {
return account
}
}
export {
constants,
errors,
validate,
serverInfo
}
export {
dropsToXrp,
xrpToDrops,
@@ -20,4 +42,3 @@ export {
} from './utils'
export {default as Connection} from './connection'
export {txFlags} from './txflags'

View File

@@ -2,7 +2,7 @@ import * as _ from 'lodash'
import * as assert from 'assert'
const {Validator} = require('jsonschema')
import {ValidationError} from './errors'
import {isValidAddress} from 'ripple-address-codec'
import {isValidClassicAddress, isValidXAddress} from 'ripple-address-codec'
import {isValidSecret} from './utils'
function loadSchemas() {
@@ -34,6 +34,8 @@ function loadSchemas() {
require('./schemas/objects/destination-address-tag.json'),
require('./schemas/objects/transaction-hash.json'),
require('./schemas/objects/address.json'),
require('./schemas/objects/x-address.json'),
require('./schemas/objects/classic-address.json'),
require('./schemas/objects/adjustment.json'),
require('./schemas/objects/quality.json'),
require('./schemas/objects/amount.json'),
@@ -126,12 +128,23 @@ function loadSchemas() {
// Register custom format validators that ignore undefined instances
// since jsonschema will still call the format validator on a missing
// (optional) property
validator.customFormats.address = function(instance) {
// This relies on "format": "xAddress" in `x-address.json`!
validator.customFormats.xAddress = function(instance) {
if (instance === undefined) {
return true
}
return isValidXAddress(instance)
}
// This relies on "format": "classicAddress" in `classic-address.json`!
validator.customFormats.classicAddress = function(instance) {
if (instance === undefined) {
return true
}
return isValidAddress(instance)
}
validator.customFormats.secret = function(instance) {
if (instance === undefined) {
return true
@@ -158,6 +171,10 @@ function schemaValidate(schemaName: string, object: any): void {
}
}
function isValidAddress(address: string): boolean {
return isValidXAddress(address) || isValidClassicAddress(address)
}
export {
schemaValidate,
isValidSecret,

View File

@@ -20,6 +20,14 @@
"type": "string",
"enum": ["ecdsa-secp256k1", "ed25519"],
"description": "The digital signature algorithm to generate an address for. Can be `ecdsa-secp256k1` (default) or `ed25519`."
},
"test": {
"type": "boolean",
"description": "Specifies whether the address is intended for use on a test network such as Testnet or Devnet. If `true`, the address should only be used for testing, and will start with `T`. If `false`, the address should only be used on mainnet, and will start with `X`."
},
"includeClassicAddress": {
"type": "boolean",
"description": "If `true`, return the classic address, in addition to the X-address."
}
},
"additionalProperties": false

View File

@@ -0,0 +1,33 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "generateXAddressParameters",
"type": "object",
"properties": {
"options": {
"type": "object",
"description": "Options to control how the address and secret are generated.",
"properties": {
"entropy": {
"type": "array",
"items": {
"type": "integer",
"minimum": 0,
"maximum": 255
},
"description": "The entropy to use to generate the seed."
},
"algorithm": {
"type": "string",
"enum": ["ecdsa-secp256k1", "ed25519"],
"description": "The digital signature algorithm to generate an address for. Can be `ecdsa-secp256k1` (default) or `ed25519`."
},
"test": {
"type": "boolean",
"description": "Specifies whether the address is intended for use on a test network such as Testnet or Devnet. If `true`, the address should only be used for testing, and will start with `T`. If `false`, the address should only be used on mainnet, and will start with `X`."
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}

View File

@@ -1,9 +1,12 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "address",
"description": "A Ripple account address",
"description": "An account address on the XRP Ledger",
"type": "string",
"format": "address",
"link": "address",
"pattern": "^r[1-9A-HJ-NP-Za-km-z]{25,34}$"
"oneOf": [
{"$ref": "xAddress"},
{"$ref": "classicAddress"}
]
}

View File

@@ -0,0 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "classicAddress",
"description": "A classic address (Account ID) for the XRP Ledger",
"type": "string",
"format": "classicAddress",
"link": "classic-address",
"pattern": "^r[1-9A-HJ-NP-Za-km-z]{24,34}$"
}

View File

@@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "tag",
"description": "An arbitrary unsigned 32-bit integer that identifies a reason for payment or a non-Ripple account.",
"description": "An arbitrary 32-bit unsigned integer. It typically maps to an off-ledger account; for example, a hosted wallet or exchange account.",
"type": "integer",
"$ref": "uint32"
}

View File

@@ -0,0 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "xAddress",
"description": "An XRP Ledger address in X-address format",
"type": "string",
"format": "xAddress",
"link": "x-address",
"pattern": "^[XT][1-9A-HJ-NP-Za-km-z]{46}$"
}

View File

@@ -3,16 +3,24 @@
"title": "generateAddress",
"type": "object",
"properties": {
"xAddress": {
"$ref": "xAddress",
"description": "A randomly generated XRP Ledger address in X-address format."
},
"classicAddress": {
"$ref": "classicAddress",
"description": "A randomly generated XRP Ledger Account ID (classic address)."
},
"address": {
"$ref": "address",
"description": "A randomly generated Ripple account address."
"$ref": "classicAddress",
"description": "Deprecated: Use `classicAddress` instead."
},
"secret": {
"type": "string",
"format": "secret",
"description": "The secret corresponding to the `address`."
"description": "The secret corresponding to the address."
}
},
"required": ["address", "secret"],
"required": ["xAddress", "classicAddress", "address", "secret"],
"additionalProperties": false
}

View File

@@ -0,0 +1,18 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "generateXAddress",
"type": "object",
"properties": {
"xAddress": {
"$ref": "xAddress",
"description": "A randomly generated XRP Ledger address in X-address format."
},
"secret": {
"type": "string",
"format": "secret",
"description": "The secret corresponding to the address."
}
},
"required": ["xAddress", "secret"],
"additionalProperties": false
}

View File

@@ -1,4 +1,4 @@
import {validate, removeUndefined, dropsToXrp} from '../common'
import {validate, removeUndefined, dropsToXrp, ensureClassicAddress} from '../common'
import {RippleAPI} from '..'
import {AccountInfoResponse} from '../common/types/commands/account_info'
@@ -34,6 +34,11 @@ export default async function getAccountInfo(
): Promise<FormattedGetAccountInfoResponse> {
// 1. Validate
validate.getAccountInfo({address, options})
// Only support retrieving account info without a tag,
// since account info is not distinguished by tag.
address = ensureClassicAddress(address)
// 2. Make Request
const response = await this.request('account_info', {
account: address,

View File

@@ -1,11 +1,10 @@
import * as utils from './utils'
import {validate} from '../common'
import {validate, ensureClassicAddress} from '../common'
import {Connection} from '../common'
import {GetTrustlinesOptions} from './trustlines'
import {FormattedTrustline} from '../common/types/objects/trustlines'
import {RippleAPI} from '..'
export type Balance = {
value: string,
currency: string,
@@ -52,6 +51,13 @@ function getBalances(this: RippleAPI, address: string, options: GetTrustlinesOpt
): Promise<GetBalances> {
validate.getTrustlines({address, options})
// Only support retrieving balances without a tag,
// since we currently do not calculate balances
// on a per-tag basis. Apps must interpret and
// use tags independent of the XRP Ledger, comparing
// with the XRP Ledger's balance as an accounting check.
address = ensureClassicAddress(address)
return Promise.all([
getLedgerVersionHelper(this.connection, options.ledgerVersion).then(
ledgerVersion =>

View File

@@ -33,6 +33,6 @@ export default async function getOrders(
ledger_index: options.ledgerVersion || await this.getLedgerVersion(),
limit: options.limit
})
// 3. Return Formatted Response
// 3. Return Formatted Response, from the perspective of `address`
return formatResponse(address, responses)
}

View File

@@ -1,9 +1,10 @@
import * as _ from 'lodash'
import parseFields from './parse/fields'
import {validate, constants} from '../common'
import {validate, constants, ensureClassicAddress} from '../common'
import {FormattedSettings} from '../common/types/objects'
import {AccountInfoResponse} from '../common/types/commands'
import {RippleAPI} from '..'
const AccountFlags = constants.AccountFlags
export type SettingsOptions = {
@@ -38,6 +39,11 @@ export async function getSettings(
): Promise<FormattedSettings> {
// 1. Validate
validate.getSettings({address, options})
// Only support retrieving settings without a tag,
// since settings do not distinguish by tag.
address = ensureClassicAddress(address)
// 2. Make Request
const response = await this.request('account_info', {
account: address,

View File

@@ -4,11 +4,10 @@ import {computeTransactionHash} from '../common/hashes'
import * as utils from './utils'
import parseTransaction from './parse/transaction'
import getTransaction from './transaction'
import {validate, errors, Connection} from '../common'
import {validate, errors, Connection, ensureClassicAddress} from '../common'
import {FormattedTransactionType} from '../transaction/types'
import {RippleAPI} from '..'
export type TransactionsOptions = {
start?: string,
limit?: number,
@@ -167,6 +166,12 @@ function getTransactions(this: RippleAPI, address: string, options: Transactions
): Promise<GetTransactionsResponse> {
validate.getTransactions({address, options})
// Only support retrieving transactions without a tag,
// since we currently do not filter transactions based
// on tag. Apps must interpret and use tags
// independently, filtering transactions if needed.
address = ensureClassicAddress(address)
const defaults = {maxLedgerVersion: -1}
if (options.start) {
return getTransaction.call(this, options.start).then(tx => {

View File

@@ -1,5 +1,5 @@
import * as _ from 'lodash'
import {validate} from '../common'
import {validate, ensureClassicAddress} from '../common'
import parseAccountTrustline from './parse/account-trustline'
import {RippleAPI} from '..'
import {FormattedTrustline} from '../common/types/objects/trustlines'
@@ -20,11 +20,16 @@ async function getTrustlines(
): Promise<FormattedTrustline[]> {
// 1. Validate
validate.getTrustlines({address, options})
const ledgerVersion = await this.getLedgerVersion()
// Only support retrieving trustlines without a tag,
// since it does not make sense to filter trustlines
// by tag.
address = ensureClassicAddress(address)
// 2. Make Request
const responses = await this._requestAll('account_lines', {
account: address,
ledger_index: ledgerVersion,
ledger_index: await this.getLedgerVersion(),
limit: options.limit,
peer: options.counterparty
})

View File

@@ -1,6 +1,13 @@
import {deriveKeypair, deriveAddress} from 'ripple-keypairs'
import {classicAddressToXAddress} from 'ripple-address-codec'
function deriveXAddress(options: {publicKey: string, tag: number | false, test: boolean}): string {
const classicAddress = deriveAddress(options.publicKey)
return classicAddressToXAddress(classicAddress, options.tag, options.test)
}
export {
deriveKeypair,
deriveAddress
deriveAddress,
deriveXAddress
}

View File

@@ -1,19 +1,45 @@
import {classicAddressToXAddress} from 'ripple-address-codec'
import keypairs from 'ripple-keypairs'
import * as common from '../common'
const {errors, validate} = common
import {errors, validate} from '../common'
export type GeneratedAddress = {
secret: string,
address: string
xAddress: string,
classicAddress?: string,
address?: string, // @deprecated Use `classicAddress` instead.
secret: string
}
function generateAddressAPI(options?: any): GeneratedAddress {
export interface GenerateAddressOptions {
// The entropy to use to generate the seed.
entropy?: Uint8Array,
// The digital signature algorithm to generate an address for. Can be `ecdsa-secp256k1` (default) or `ed25519`.
algorithm?: 'ecdsa-secp256k1' | 'ed25519',
// Specifies whether the address is intended for use on a test network such as Testnet or Devnet.
// If `true`, the address should only be used for testing, and will start with `T`.
// If `false` (default), the address should only be used on mainnet, and will start with `X`.
test?: boolean,
// If `true`, return the classic address, in addition to the X-address.
includeClassicAddress?: boolean
}
function generateAddressAPI(options: GenerateAddressOptions): GeneratedAddress {
validate.generateAddress({options})
try {
const secret = keypairs.generateSeed(options)
const keypair = keypairs.deriveKeypair(secret)
const address = keypairs.deriveAddress(keypair.publicKey)
return {secret, address}
const classicAddress = keypairs.deriveAddress(keypair.publicKey)
const returnValue: any = {
xAddress: classicAddressToXAddress(classicAddress, false, options && options.test),
secret
}
if (options.includeClassicAddress) {
returnValue.classicAddress = classicAddress
returnValue.address = classicAddress
}
return returnValue
} catch (error) {
throw new errors.UnexpectedError(error.message)
}

View File

@@ -2,12 +2,12 @@ import * as _ from 'lodash'
import binary from 'ripple-binary-codec'
import * as utils from './utils'
import BigNumber from 'bignumber.js'
import {decodeAddress} from 'ripple-address-codec'
import {decodeAccountID} from 'ripple-address-codec'
import {validate} from '../common'
import {computeBinaryTransactionHash} from '../common/hashes'
function addressToBigNumber(address) {
const hex = (Buffer.from(decodeAddress(address))).toString('hex')
const hex = (Buffer.from(decodeAccountID(address))).toString('hex')
return new BigNumber(hex, 16)
}

View File

@@ -9,6 +9,7 @@ import {Amount, Adjustment, MaxAdjustment,
MinAdjustment, Memo} from '../common/types/objects'
import {xrpToDrops} from '../common'
import {RippleAPI} from '..'
import {getClassicAccountAndTag, ClassicAccountAndTag} from './utils'
export interface Payment {
@@ -84,15 +85,49 @@ function createMaximalAmount(amount: Amount): Amount {
return _.assign({}, amount, {value: maxValue})
}
/**
* Given an address and tag:
* 1. Get the classic account and tag;
* 2. If a tag is provided:
* 2a. If the address was an X-address, validate that the X-address has the expected tag;
* 2b. If the address was a classic address, return `expectedTag` as the tag.
* 3. If we do not want to use a tag in this case,
* set the tag in the return value to `undefined`.
*
* @param address The address to parse.
* @param expectedTag If provided, and the `Account` is an X-address,
* this method throws an error if `expectedTag`
* does not match the tag of the X-address.
* @returns {ClassicAccountAndTag}
* The classic account and tag.
*/
function validateAndNormalizeAddress(address: string, expectedTag: number | undefined): ClassicAccountAndTag {
const classicAddress = getClassicAccountAndTag(address, expectedTag)
classicAddress.tag = classicAddress.tag === false ? undefined : classicAddress.tag
return classicAddress
}
function createPaymentTransaction(address: string, paymentArgument: Payment
): TransactionJSON {
const payment = _.cloneDeep(paymentArgument)
applyAnyCounterpartyEncoding(payment)
if (address !== payment.source.address) {
const sourceAddressAndTag = validateAndNormalizeAddress(payment.source.address, payment.source.tag)
const addressToVerifyAgainst = validateAndNormalizeAddress(address, undefined)
if (addressToVerifyAgainst.classicAccount !== sourceAddressAndTag.classicAccount) {
throw new ValidationError('address must match payment.source.address')
}
if (addressToVerifyAgainst.tag !== undefined &&
sourceAddressAndTag.tag !== undefined &&
addressToVerifyAgainst.tag !== sourceAddressAndTag.tag) {
throw new ValidationError(
'address includes a tag that does not match payment.source.tag')
}
const destinationAddressAndTag = validateAndNormalizeAddress(payment.destination.address, payment.destination.tag)
if (
(isMaxAdjustment(payment.source) && isMinAdjustment(payment.destination))
||
@@ -119,8 +154,8 @@ function createPaymentTransaction(address: string, paymentArgument: Payment
const txJSON: any = {
TransactionType: 'Payment',
Account: payment.source.address,
Destination: payment.destination.address,
Account: sourceAddressAndTag.classicAccount,
Destination: destinationAddressAndTag.classicAccount,
Amount: toRippledAmount(amount),
Flags: 0
}
@@ -128,11 +163,11 @@ function createPaymentTransaction(address: string, paymentArgument: Payment
if (payment.invoiceID !== undefined) {
txJSON.InvoiceID = payment.invoiceID
}
if (payment.source.tag !== undefined) {
txJSON.SourceTag = payment.source.tag
if (sourceAddressAndTag.tag !== undefined) {
txJSON.SourceTag = sourceAddressAndTag.tag
}
if (payment.destination.tag !== undefined) {
txJSON.DestinationTag = payment.destination.tag
if (destinationAddressAndTag.tag !== undefined) {
txJSON.DestinationTag = destinationAddressAndTag.tag
}
if (payment.memos !== undefined) {
txJSON.Memos = _.map(payment.memos, utils.convertMemo)

View File

@@ -1,10 +1,13 @@
import BigNumber from 'bignumber.js'
import * as common from '../common'
import {Memo, RippledAmount} from '../common/types/objects'
const txFlags = common.txFlags
import {Instructions, Prepare} from './types'
import {RippleAPI} from '..'
import {ValidationError} from '../common/errors'
import {xAddressToClassicAddress, isValidXAddress} from 'ripple-address-codec'
const txFlags = common.txFlags
const TRANSACTION_TYPES_WITH_DESTINATION_TAG_FIELD = ['Payment', 'CheckCreate', 'EscrowCreate', 'PaymentChannelCreate']
export type ApiMemo = {
MemoData?: string,
@@ -56,6 +59,49 @@ function scaleValue(value, multiplier, extra = 0) {
return (new BigNumber(value)).times(multiplier).plus(extra).toString()
}
/**
* @typedef {Object} ClassicAccountAndTag
* @property {string} classicAccount - The classic account address.
* @property {number | false | undefined } tag - The destination tag;
* `false` if no tag should be used;
* `undefined` if the input could not specify whether a tag should be used.
*/
interface ClassicAccountAndTag {
classicAccount: string,
tag: number | false | undefined
}
/**
* Given an address (account), get the classic account and tag.
* If an `expectedTag` is provided:
* 1. If the `Account` is an X-address, validate that the tags match.
* 2. If the `Account` is a classic address, return `expectedTag` as the tag.
*
* @param Account The address to parse.
* @param expectedTag If provided, and the `Account` is an X-address,
* this method throws an error if `expectedTag`
* does not match the tag of the X-address.
* @returns {ClassicAccountAndTag}
* The classic account and tag.
*/
function getClassicAccountAndTag(Account: string, expectedTag?: number): ClassicAccountAndTag {
if (isValidXAddress(Account)) {
const classic = xAddressToClassicAddress(Account)
if (expectedTag !== undefined && classic.tag !== expectedTag) {
throw new ValidationError('address includes a tag that does not match the tag specified in the transaction')
}
return {
classicAccount: classic.classicAddress,
tag: classic.tag
}
} else {
return {
classicAccount: Account,
tag: expectedTag
}
}
}
function prepareTransaction(txJSON: TransactionJSON, api: RippleAPI,
instructions: Instructions
): Promise<Prepare> {
@@ -68,56 +114,105 @@ function prepareTransaction(txJSON: TransactionJSON, api: RippleAPI,
'" exists in instance when not allowed'))
}
// To remove the signer list, SignerEntries field should be omitted.
const newTxJSON = Object.assign({}, txJSON)
// To remove the signer list, `SignerEntries` field should be omitted.
if (txJSON['SignerQuorum'] === 0) {
delete txJSON.SignerEntries
delete newTxJSON.SignerEntries
}
const account = txJSON.Account
setCanonicalFlag(txJSON)
// Sender:
const {classicAccount, tag: sourceTag} = getClassicAccountAndTag(txJSON.Account)
newTxJSON.Account = classicAccount
if (sourceTag !== undefined) {
if (txJSON.SourceTag && txJSON.SourceTag !== sourceTag) {
return Promise.reject(new ValidationError(
'The `SourceTag`, if present, must match the tag of the `Account` X-address'))
}
if (sourceTag) {
newTxJSON.SourceTag = sourceTag
}
}
function prepareMaxLedgerVersion(): Promise<TransactionJSON> {
// Destination:
if (typeof txJSON.Destination === 'string') {
const {classicAccount: destinationAccount, tag: destinationTag} = getClassicAccountAndTag(txJSON.Destination)
newTxJSON.Destination = destinationAccount
if (destinationTag !== undefined) {
if (TRANSACTION_TYPES_WITH_DESTINATION_TAG_FIELD.includes(txJSON.TransactionType)) {
if (txJSON.DestinationTag && txJSON.DestinationTag !== destinationTag) {
return Promise.reject(new ValidationError(
'The Payment `DestinationTag`, if present, must match the tag of the `Destination` X-address'))
}
if (destinationTag) {
newTxJSON.DestinationTag = destinationTag
}
}
}
}
function convertToClassicAccountIfPresent(fieldName: string): void {
const account = txJSON[fieldName]
if (typeof account === 'string') {
const {classicAccount: ca} = getClassicAccountAndTag(account)
newTxJSON[fieldName] = ca
}
}
// DepositPreauth:
convertToClassicAccountIfPresent('Authorize')
convertToClassicAccountIfPresent('Unauthorize')
// EscrowCancel, EscrowFinish:
convertToClassicAccountIfPresent('Owner')
// SetRegularKey:
convertToClassicAccountIfPresent('RegularKey')
setCanonicalFlag(newTxJSON)
function prepareMaxLedgerVersion(): Promise<void> {
// Up to one of the following is allowed:
// txJSON.LastLedgerSequence
// instructions.maxLedgerVersion
// instructions.maxLedgerVersionOffset
if (txJSON.LastLedgerSequence && instructions.maxLedgerVersion) {
if (newTxJSON.LastLedgerSequence && instructions.maxLedgerVersion) {
return Promise.reject(new ValidationError('`LastLedgerSequence` in txJSON and `maxLedgerVersion`' +
' in `instructions` cannot both be set'))
}
if (txJSON.LastLedgerSequence && instructions.maxLedgerVersionOffset) {
if (newTxJSON.LastLedgerSequence && instructions.maxLedgerVersionOffset) {
return Promise.reject(new ValidationError('`LastLedgerSequence` in txJSON and `maxLedgerVersionOffset`' +
' in `instructions` cannot both be set'))
}
if (txJSON.LastLedgerSequence) {
return Promise.resolve(txJSON)
if (newTxJSON.LastLedgerSequence) {
return Promise.resolve()
}
if (instructions.maxLedgerVersion !== undefined) {
if (instructions.maxLedgerVersion !== null) {
txJSON.LastLedgerSequence = instructions.maxLedgerVersion
newTxJSON.LastLedgerSequence = instructions.maxLedgerVersion
}
return Promise.resolve(txJSON)
return Promise.resolve()
}
const offset = instructions.maxLedgerVersionOffset !== undefined ?
instructions.maxLedgerVersionOffset : 3
return api.connection.getLedgerVersion().then(ledgerVersion => {
txJSON.LastLedgerSequence = ledgerVersion + offset
return txJSON
newTxJSON.LastLedgerSequence = ledgerVersion + offset
return
})
}
function prepareFee(): Promise<TransactionJSON> {
function prepareFee(): Promise<void> {
// instructions.fee is scaled (for multi-signed transactions) while txJSON.Fee is not.
// Due to this difference, we do NOT allow both to be set, as the behavior would be complex and
// potentially ambiguous.
// Furthermore, txJSON.Fee is in drops while instructions.fee is in XRP, which would just add to
// the confusion. It is simpler to require that only one is used.
if (txJSON.Fee && instructions.fee) {
if (newTxJSON.Fee && instructions.fee) {
return Promise.reject(new ValidationError('`Fee` in txJSON and `fee` in `instructions` cannot both be set'))
}
if (txJSON.Fee) {
if (newTxJSON.Fee) {
// txJSON.Fee is set. Use this value and do not scale it.
return Promise.resolve(txJSON)
return Promise.resolve()
}
const multiplier = instructions.signersCount === undefined ? 1 :
instructions.signersCount + 1
@@ -128,50 +223,50 @@ function prepareTransaction(txJSON: TransactionJSON, api: RippleAPI,
`max of ${api._maxFeeXRP} XRP. To use this fee, increase ` +
'`maxFeeXRP` in the RippleAPI constructor.'))
}
txJSON.Fee = scaleValue(common.xrpToDrops(instructions.fee), multiplier)
return Promise.resolve(txJSON)
newTxJSON.Fee = scaleValue(common.xrpToDrops(instructions.fee), multiplier)
return Promise.resolve()
}
const cushion = api._feeCushion
return api.getFee(cushion).then(fee => {
return api.connection.getFeeRef().then(feeRef => {
const extraFee =
(txJSON.TransactionType !== 'EscrowFinish' ||
txJSON.Fulfillment === undefined) ? 0 :
(newTxJSON.TransactionType !== 'EscrowFinish' ||
newTxJSON.Fulfillment === undefined) ? 0 :
(cushion * feeRef * (32 + Math.floor(
Buffer.from(txJSON.Fulfillment, 'hex').length / 16)))
Buffer.from(newTxJSON.Fulfillment, 'hex').length / 16)))
const feeDrops = common.xrpToDrops(fee)
const maxFeeXRP = instructions.maxFee ?
BigNumber.min(api._maxFeeXRP, instructions.maxFee) : api._maxFeeXRP
const maxFeeDrops = common.xrpToDrops(maxFeeXRP)
const normalFee = scaleValue(feeDrops, multiplier, extraFee)
txJSON.Fee = BigNumber.min(normalFee, maxFeeDrops).toString(10)
newTxJSON.Fee = BigNumber.min(normalFee, maxFeeDrops).toString(10)
return txJSON
return
})
})
}
async function prepareSequence(): Promise<TransactionJSON> {
async function prepareSequence(): Promise<void> {
if (instructions.sequence !== undefined) {
if (txJSON.Sequence === undefined || instructions.sequence === txJSON.Sequence) {
txJSON.Sequence = instructions.sequence
return Promise.resolve(txJSON)
if (newTxJSON.Sequence === undefined || instructions.sequence === newTxJSON.Sequence) {
newTxJSON.Sequence = instructions.sequence
return Promise.resolve()
} 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)
if (newTxJSON.Sequence !== undefined) {
return Promise.resolve()
}
try {
// Consider requesting from the 'current' ledger (instead of 'validated').
const response = await api.request('account_info', {
account
account: classicAccount
})
txJSON.Sequence = response.account_data.Sequence
return Promise.resolve(txJSON)
newTxJSON.Sequence = response.account_data.Sequence
return Promise.resolve()
} catch (e) {
return Promise.reject(e)
}
@@ -181,7 +276,7 @@ function prepareTransaction(txJSON: TransactionJSON, api: RippleAPI,
prepareMaxLedgerVersion(),
prepareFee(),
prepareSequence()
]).then(() => formatPrepareResponse(txJSON))
]).then(() => formatPrepareResponse(newTxJSON))
}
function convertStringToHex(string: string): string {
@@ -203,5 +298,7 @@ export {
convertMemo,
prepareTransaction,
common,
setCanonicalFlag
setCanonicalFlag,
getClassicAccountAndTag,
ClassicAccountAndTag
}