mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-12-06 17:27:59 +00:00
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:
29
src/api.ts
29
src/api.ts
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
33
src/common/schemas/input/generate-x-address.json
Normal file
33
src/common/schemas/input/generate-x-address.json
Normal 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
|
||||
}
|
||||
@@ -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"}
|
||||
]
|
||||
}
|
||||
|
||||
9
src/common/schemas/objects/classic-address.json
Normal file
9
src/common/schemas/objects/classic-address.json
Normal 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}$"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
9
src/common/schemas/objects/x-address.json
Normal file
9
src/common/schemas/objects/x-address.json
Normal 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}$"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
18
src/common/schemas/output/generate-x-address.json
Normal file
18
src/common/schemas/output/generate-x-address.json
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user