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

@@ -1,5 +1,12 @@
# ripple-lib Release History
## 1.4.0-b1 (2019-09-26)
* Add support for the new X-address format.
* Some error messages have changed slightly. For example:
* `-instance.Account is not of a type(s) string,instance.Account does not conform to the "address" format`
* `+instance.Account is not of a type(s) string,instance.Account is not exactly one from <xAddress>,<classicAddress>`
## 1.3.4 (2019-10-18)
* Update ripple-lib-transactionparser

View File

@@ -80,6 +80,7 @@
- [sign](#sign)
- [combine](#combine)
- [submit](#submit)
- [generateXAddress](#generatexaddress)
- [generateAddress](#generateaddress)
- [isValidAddress](#isvalidaddress)
- [isValidSecret](#isvalidsecret)
@@ -225,7 +226,19 @@ Methods that depend on the state of the XRP Ledger are unavailable in offline mo
"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59"
```
Every XRP Ledger account has an *address*, which is a base58-encoding of a hash of the account's public key. XRP Ledger addresses always start with the lowercase letter `r`.
```json
"X7AcgcsBL6XDcUb289X4mJ8djcdyKaB5hJDWMArnXr61cqZ"
```
An *address* refers to a specific XRP Ledger account. It is a base-58 encoding of a hash of the account's public key. There are two kinds of addresses in common use:
### Classic Address
A *classic address* encodes a hash of the account's public key and a checksum. It has no other data. This kind of address always starts with the lowercase letter `r`.
### X-address
An *X-address* encodes a hash of the account's public key, a tag, and a checksum. This kind of address starts with the uppercase letter `X` if it is intended for use on the production XRP Ledger (mainnet). It starts with the uppercase letter `T` if it is intended for use on a test network such as Testnet or Devnet.
## Account Sequence Number
@@ -377,12 +390,12 @@ Name | Type | Description
source | object | The source of the funds to be sent.
*source.* address | [address](#address) | The address to send from.
*source.* amount | [laxAmount](#amount) | An exact amount to send. If the counterparty is not specified, amounts with any counterparty may be used. (This field cannot be used with source.maxAmount)
*source.* tag | integer | *Optional* An arbitrary unsigned 32-bit integer that identifies a reason for payment or a non-Ripple account.
*source.* tag | integer | *Optional* An arbitrary 32-bit unsigned integer. It typically maps to an off-ledger account; for example, a hosted wallet or exchange account.
*source.* maxAmount | [laxAmount](#amount) | The maximum amount to send. (This field cannot be used with source.amount)
destination | object | The destination of the funds to be sent.
*destination.* address | [address](#address) | An address representing the destination of the transaction.
*destination.* amount | [laxAmount](#amount) | An exact amount to deliver to the recipient. If the counterparty is not specified, amounts with any counterparty may be used. (This field cannot be used with `destination.minAmount`.)
*destination.* tag | integer | *Optional* An arbitrary unsigned 32-bit integer that identifies a reason for payment or a non-Ripple account.
*destination.* tag | integer | *Optional* An arbitrary 32-bit unsigned integer. It typically maps to an off-ledger account; for example, a hosted wallet or exchange account.
*destination.* minAmount | [laxAmount](#amount) | The minimum amount to be delivered. (This field cannot be used with destination.amount)
allowPartialPayment | boolean | *Optional* If true, this payment should proceed even if the whole amount cannot be delivered due to a lack of liquidity or a lack of funds in the source account.
invoiceID | string | *Optional* A 256-bit hash that can be used to identify a particular payment.
@@ -538,7 +551,7 @@ signers | object | *Optional* Settings that determine what sets of accounts can
*signers.* threshold | integer | A target number for the signer weights. A multi-signature from this list is valid only if the sum weights of the signatures provided is equal or greater than this value. To delete the signers setting, use the value `0`.
*signers.* weights | array | *Optional* Weights of signatures for each signer.
*signers.* weights[] | object | An association of an address and a weight.
*signers.weights[].* address | [address](#address) | A Ripple account address
*signers.weights[].* address | [address](#address) | An account address on the XRP Ledger
*signers.weights[].* weight | integer | The weight that the signature of this account counts as towards the threshold.
transferRate | number,null | *Optional* The fee to charge when users transfer this accounts issuances, as the decimal amount that must be sent to deliver 1 unit. Has precision up to 9 digits beyond the decimal point. Use `null` to set no fee.
@@ -851,7 +864,7 @@ Returns the response from invoking the specified command, with the specified opt
Refer to [rippled APIs](https://ripple.com/build/rippled-apis/) for commands and options. All XRP amounts must be specified in drops. One drop is equal to 0.000001 XRP. See [Specifying Currency Amounts](https://ripple.com/build/rippled-apis/#specifying-currency-amounts).
Most commands return data for the `current` (in-progress, open) ledger by default. Do not rely on this. Always specify a ledger version in your request. In the example below, the 'validated' ledger is requested, which is the most recent ledger that has been validated by the whole network. See [Specifying Ledgers](https://ripple.com/build/rippled-apis/#specifying-ledgers).
Most commands return data for the `current` (in-progress, open) ledger by default. Do not rely on this. Always specify a ledger version in your request. In the example below, the 'validated' ledger is requested, which is the most recent ledger that has been validated by the whole network. See [Specifying Ledgers](https://xrpl.org/basic-data-types.html#specifying-ledgers).
### Return Value
@@ -2289,12 +2302,12 @@ Name | Type | Description
source | object | Properties of the source of the payment.
*source.* address | [address](#address) | The address to send from.
*source.* amount | [laxAmount](#amount) | An exact amount to send. If the counterparty is not specified, amounts with any counterparty may be used. (This field cannot be used with source.maxAmount)
*source.* tag | integer | *Optional* An arbitrary unsigned 32-bit integer that identifies a reason for payment or a non-Ripple account.
*source.* tag | integer | *Optional* An arbitrary 32-bit unsigned integer. It typically maps to an off-ledger account; for example, a hosted wallet or exchange account.
*source.* maxAmount | [laxAmount](#amount) | The maximum amount to send. (This field cannot be used with source.amount)
destination | object | Properties of the destination of the payment.
*destination.* address | [address](#address) | An address representing the destination of the transaction.
*destination.* amount | [laxAmount](#amount) | An exact amount to deliver to the recipient. If the counterparty is not specified, amounts with any counterparty may be used. (This field cannot be used with `destination.minAmount`.)
*destination.* tag | integer | *Optional* An arbitrary unsigned 32-bit integer that identifies a reason for payment or a non-Ripple account.
*destination.* tag | integer | *Optional* An arbitrary 32-bit unsigned integer. It typically maps to an off-ledger account; for example, a hosted wallet or exchange account.
*destination.* minAmount | [laxAmount](#amount) | The minimum amount to be delivered. (This field cannot be used with destination.amount)
paths | string | The paths of trustlines and orders to use in executing the payment.
@@ -3906,7 +3919,7 @@ signers | object | *Optional* Settings that determine what sets of accounts can
*signers.* threshold | integer | A target number for the signer weights. A multi-signature from this list is valid only if the sum weights of the signatures provided is equal or greater than this value. To delete the signers setting, use the value `0`.
*signers.* weights | array | *Optional* Weights of signatures for each signer.
*signers.* weights[] | object | An association of an address and a weight.
*signers.weights[].* address | [address](#address) | A Ripple account address
*signers.weights[].* address | [address](#address) | An account address on the XRP Ledger
*signers.weights[].* weight | integer | The weight that the signature of this account counts as towards the threshold.
transferRate | number,null | *Optional* The fee to charge when users transfer this accounts issuances, as the decimal amount that must be sent to deliver 1 unit. Has precision up to 9 digits beyond the decimal point. Use `null` to set no fee.
@@ -5403,7 +5416,7 @@ options | object | *Optional* Options that control the type of signature that wi
*options.* signAs | [address](#address) | *Optional* The account that the signature should count for in multisigning.
secret | secret string | *Optional* The secret of the account that is initiating the transaction. (This field cannot be used with keypair).
Please note that when this method is used for multisigning, the `options` parameter is not *Optional* anymore. It will be compulsory. See the multisigning example in this section for more details.
When this method is used for multisigning, the `options` parameter is required. See the multisigning example in this section for more details.
### Return Value
@@ -5431,6 +5444,7 @@ return api.sign(txJSON, secret); // or: api.sign(txJSON, keypair);
}
```
### Example (multisigning)
```javascript
@@ -5620,9 +5634,9 @@ return api.submit(signedTransaction)
```
## generateAddress
## generateXAddress
`generateAddress(): {address: string, secret: string}`
`generateXAddress(options?: object): {address: string, secret: string}`
Generate a new XRP Ledger address and corresponding secret.
@@ -5633,6 +5647,7 @@ Name | Type | Description
options | object | *Optional* Options to control how the address and secret are generated.
*options.* algorithm | string | *Optional* The digital signature algorithm to generate an address for. Can be `ecdsa-secp256k1` (default) or `ed25519`.
*options.* entropy | array\<integer\> | *Optional* The entropy to use to generate the seed.
*options.* test | boolean | *Optional* 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`.
### Return Value
@@ -5640,8 +5655,8 @@ This method returns an object with the following structure:
Name | Type | Description
---- | ---- | -----------
address | [address](#address) | A randomly generated Ripple account address.
secret | secret string | The secret corresponding to the `address`.
xAddress | [xAddress](#x-address) | A randomly generated XRP Ledger address in X-address format.
secret | secret string | The secret corresponding to the address.
### Example
@@ -5652,6 +5667,52 @@ return api.generateAddress();
```json
{
"xAddress": "XVLcsWWNiFdUEqoDmSwgxh1abfddG1LtbGFk7omPgYpbyE8",
"secret": "sp6JS7f14BuwFY8Mw6bTtLKWauoUs"
}
```
## generateAddress
`generateAddress(options?: object): {address: string, secret: string}`
Deprecated: This method returns a classic address. If you do not need the classic address, use `generateXAddress` instead.
Generate a new XRP Ledger address and corresponding secret.
### Parameters
Name | Type | Description
---- | ---- | -----------
options | object | *Optional* Options to control how the address and secret are generated.
*options.* algorithm | string | *Optional* The digital signature algorithm to generate an address for. Can be `ecdsa-secp256k1` (default) or `ed25519`.
*options.* entropy | array\<integer\> | *Optional* The entropy to use to generate the seed.
*options.* includeClassicAddress | boolean | *Optional* If `true`, return the classic address, in addition to the X-address.
*options.* test | boolean | *Optional* 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`.
### Return Value
This method returns an object with the following structure:
Name | Type | Description
---- | ---- | -----------
xAddress | [xAddress](#x-address) | A randomly generated XRP Ledger address in X-address format.
classicAddress | [classicAddress](#classic-address) | A randomly generated XRP Ledger Account ID (classic address).
address | [classicAddress](#classic-address) | Deprecated: Use `classicAddress` instead.
secret | secret string | The secret corresponding to the address.
### Example
```javascript
return api.generateAddress();
```
```json
{
"xAddress": "XVLcsWWNiFdUEqoDmSwgxh1abfddG1LtbGFk7omPgYpbyE8",
"classicAddress": "rGCkuB7PBr5tNy68tPEABEtcdno4hE6Y7f",
"address": "rGCkuB7PBr5tNy68tPEABEtcdno4hE6Y7f",
"secret": "sp6JS7f14BuwFY8Mw6bTtLKWauoUs"
}
@@ -5662,7 +5723,7 @@ return api.generateAddress();
`isValidAddress(address: string): boolean`
Checks if the specified string contains a valid address.
Checks if the specified string contains a valid address. X-addresses are considered valid with ripple-lib v1.4.0 and higher.
### Parameters

View File

@@ -6,7 +6,19 @@
"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59"
```
Every XRP Ledger account has an *address*, which is a base58-encoding of a hash of the account's public key. XRP Ledger addresses always start with the lowercase letter `r`.
```json
"X7AcgcsBL6XDcUb289X4mJ8djcdyKaB5hJDWMArnXr61cqZ"
```
An *address* refers to a specific XRP Ledger account. It is a base-58 encoding of a hash of the account's public key. There are two kinds of addresses in common use:
### Classic Address
A *classic address* encodes a hash of the account's public key and a checksum. It has no other data. This kind of address always starts with the lowercase letter `r`.
### X-address
An *X-address* encodes a hash of the account's public key, a tag, and a checksum. This kind of address starts with the uppercase letter `X` if it is intended for use on the production XRP Ledger (mainnet). It starts with the uppercase letter `T` if it is intended for use on a test network such as Testnet or Devnet.
## Account Sequence Number

View File

@@ -1,6 +1,8 @@
## generateAddress
`generateAddress(): {address: string, secret: string}`
`generateAddress(options?: object): {address: string, secret: string}`
Deprecated: This method returns a classic address. If you do not need the classic address, use `generateXAddress` instead.
Generate a new XRP Ledger address and corresponding secret.

View File

@@ -0,0 +1,23 @@
## generateXAddress
`generateXAddress(options?: object): {address: string, secret: string}`
Generate a new XRP Ledger address and corresponding secret.
### Parameters
<%- renderSchema('input/generate-x-address.json') %>
### Return Value
This method returns an object with the following structure:
<%- renderSchema('output/generate-x-address.json') %>
### Example
```javascript
return api.generateAddress();
```
<%- renderFixture('responses/generate-x-address.json') %>

View File

@@ -52,6 +52,7 @@
<% include sign.md.ejs %>
<% include combine.md.ejs %>
<% include submit.md.ejs %>
<% include generateXAddress.md.ejs %>
<% include generateAddress.md.ejs %>
<% include isValidAddress.md.ejs %>
<% include isValidSecret.md.ejs %>

View File

@@ -2,7 +2,7 @@
`isValidAddress(address: string): boolean`
Checks if the specified string contains a valid address.
Checks if the specified string contains a valid address. X-addresses are considered valid with ripple-lib v1.4.0 and higher.
### Parameters

View File

@@ -6,7 +6,7 @@ Returns the response from invoking the specified command, with the specified opt
Refer to [rippled APIs](https://ripple.com/build/rippled-apis/) for commands and options. All XRP amounts must be specified in drops. One drop is equal to 0.000001 XRP. See [Specifying Currency Amounts](https://ripple.com/build/rippled-apis/#specifying-currency-amounts).
Most commands return data for the `current` (in-progress, open) ledger by default. Do not rely on this. Always specify a ledger version in your request. In the example below, the 'validated' ledger is requested, which is the most recent ledger that has been validated by the whole network. See [Specifying Ledgers](https://ripple.com/build/rippled-apis/#specifying-ledgers).
Most commands return data for the `current` (in-progress, open) ledger by default. Do not rely on this. Always specify a ledger version in your request. In the example below, the 'validated' ledger is requested, which is the most recent ledger that has been validated by the whole network. See [Specifying Ledgers](https://xrpl.org/basic-data-types.html#specifying-ledgers).
### Return Value

View File

@@ -15,6 +15,8 @@ This method can sign any of [the transaction types supported by ripple-binary-co
<%- renderSchema("input/sign.json") %>
When this method is used for multisigning, the `options` parameter is required. See the multisigning example in this section for more details.
### Return Value
This method returns an object with the following structure:
@@ -31,3 +33,91 @@ return api.sign(txJSON, secret); // or: api.sign(txJSON, keypair);
```
<%- renderFixture("responses/sign.json") %>
### Example (multisigning)
```javascript
const RippleAPI = require('ripple-lib').RippleAPI;
// jon's address will have a multi-signing setup with a quorum of 2
const jon = {
account: 'rJKpme4m2zBQceBuU89d7vLMzgoUw2Ptj',
secret: 'sh4Va7b1wQof8knHFV2sxwX12fSgK'
};
const aya = {
account: 'rnrPdBjs98fFFfmRpL6hM7exT788SWQPFN',
secret: 'snaMuMrXeVc2Vd4NYvHofeGNjgYoe'
};
const bran = {
account: 'rJ93RLnT1t5A8fCr7HTScw7WtfKJMRXodH',
secret: 'shQtQ8Um5MS218yvEU3Ehy1eZQKqH'
};
// Setup the signers list with a quorum of 2
const multiSignSetupTransaction = {
"Flags": 0,
"TransactionType": "SignerListSet",
"Account": "rJKpme4m2zBQceBuU89d7vLMzgoUw2Ptj",
"Fee": "120",
"SignerQuorum": 2,
"SignerEntries": [
{
"SignerEntry": {
"Account": "rnrPdBjs98fFFfmRpL6hM7exT788SWQPFN",
"SignerWeight": 2
}
},
{
"SignerEntry": {
"Account": "rJ93RLnT1t5A8fCr7HTScw7WtfKJMRXodH",
"SignerWeight": 1
}
},
]
};
// a transaction which requires multi signing
const multiSignPaymentTransaction = {
TransactionType: 'Payment',
Account: 'rJKpme4m2zBQceBuU89d7vLMzgoUw2Ptj',
Destination: 'rJ93RLnT1t5A8fCr7HTScw7WtfKJMRXodH',
Amount: '88000000'
};
const api = new RippleAPI({
server: 'wss://s.altnet.rippletest.net:51233'
});
api.connect().then(() => {
// adding the multi signing feature to jon's account
api.prepareTransaction(multiSignSetupTransaction).then((prepared) => {
console.log(prepared);
jonSign = api.sign(prepared.txJSON, jon.secret).signedTransaction;
api.submit(jonSign).then( response => {
console.log(response.resultCode, response.resultMessage);
// multi sign a transaction
api.prepareTransaction(multiSignPaymentTransaction).then(prepared => {
console.log(prepared);
// Aya and Bran sign it too but with 'signAs' set to their own account
let ayaSign = api.sign(prepared.txJSON, aya.secret, {'signAs': aya.account}).signedTransaction;
let branSign = api.sign(prepared.txJSON, bran.secret, {'signAs': bran.account}).signedTransaction;
// signatures are combined and submitted
let combinedTx = api.combine([ayaSign, branSign]);
api.submit(combinedTx.signedTransaction).then(response => {
console.log(response.tx_json.hash);
return api.disconnect();
}).catch(console.error);
}).catch(console.error);
}).catch(console.error)
}).catch(console.error);
}).catch(console.error);
```
Assuming the multisigning account was setup properly, the above example will respond with `resultCode: 'tesSUCCESS'` and the hash for the transaction.
If any of `{signAs: some_address}` options were missing the code will return a validation error as follow:
```
[ValidationError(txJSON is not the same for all signedTransactions)]
```

View File

@@ -1,6 +1,6 @@
{
"name": "ripple-lib",
"version": "1.3.4",
"version": "1.4.0-b2",
"license": "ISC",
"description": "A JavaScript API for interacting with Ripple in Node.js and the browser",
"files": [
@@ -23,8 +23,8 @@
"https-proxy-agent": "^3.0.0",
"jsonschema": "1.2.2",
"lodash": "^4.17.4",
"ripple-address-codec": "^4.0.0",
"lodash.isequal": "^4.5.0",
"ripple-address-codec": "^3.0.4",
"ripple-binary-codec": "^0.2.4",
"ripple-keypairs": "^0.10.1",
"ripple-lib-transactionparser": "0.8.0",

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
}

View File

@@ -996,7 +996,7 @@ describe('RippleAPI', function () {
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');
assert.strictEqual(err.message, 'instance.Account is not of a type(s) string,instance.Account is not exactly one from <xAddress>,<classicAddress>');
done();
}).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout.
} catch (err) {
@@ -1020,7 +1020,7 @@ describe('RippleAPI', function () {
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');
assert.strictEqual(err.message, 'instance.Account is not exactly one from <xAddress>,<classicAddress>');
done();
}).catch(done); // Finish test with assertion failure immediately instead of waiting for timeout.
} catch (err) {
@@ -1581,7 +1581,7 @@ describe('RippleAPI', function () {
});
it('prepareOrder - invalid', function (done) {
const request = requests.prepareOrder.sell;
const request = Object.assign({}, requests.prepareOrder.sell);
delete request.direction; // Make invalid
try {
this.api.prepareOrder(address, request, instructionsWithMaxLedgerVersionOffset).then(prepared => {
@@ -1620,7 +1620,7 @@ describe('RippleAPI', function () {
});
it('prepareOrderCancellation - invalid', function (done) {
const request = requests.prepareOrderCancellation.withMemos;
const request = Object.assign({}, requests.prepareOrderCancellation.withMemos);
delete request.orderSequence; // Make invalid
try {
this.api.prepareOrderCancellation(address, request).then(prepared => {
@@ -1654,7 +1654,7 @@ describe('RippleAPI', function () {
});
it('prepareTrustline - invalid', function (done) {
const trustline = requests.prepareTrustline.complex;
const trustline = Object.assign({}, requests.prepareTrustline.complex);
delete trustline.limit; // Make invalid
try {
this.api.prepareTrustline(
@@ -2958,7 +2958,7 @@ describe('RippleAPI', function () {
const options = {
includeRawTransaction: true
}
const expected = responses.getTransaction.settings
const expected = Object.assign({}, responses.getTransaction.settings) // Avoid mutating test fixture
expected.rawTransaction = "{\"Account\":\"rLVKsA4F9iJBbA6rX2x4wCmkj6drgtqpQe\",\"Fee\":\"10\",\"Flags\":2147483648,\"Sequence\":1,\"SetFlag\":2,\"SigningPubKey\":\"03EA3ADCA632F125EC2CC4F7F6A82DE0DCE2B65290CAC1F22242C5163F0DA9652D\",\"TransactionType\":\"AccountSet\",\"TxnSignature\":\"3045022100DE8B666B1A31EA65011B0F32130AB91A5747E32FA49B3054CEE8E8362DBAB98A022040CF0CF254677A8E5CD04C59CA2ED7F6F15F7E184641BAE169C561650967B226\",\"date\":460832270,\"hash\":\"4FB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA1B\",\"inLedger\":8206418,\"ledger_index\":8206418,\"meta\":{\"AffectedNodes\":[{\"ModifiedNode\":{\"FinalFields\":{\"Account\":\"rLVKsA4F9iJBbA6rX2x4wCmkj6drgtqpQe\",\"Balance\":\"29999990\",\"Flags\":786432,\"OwnerCount\":0,\"Sequence\":2},\"LedgerEntryType\":\"AccountRoot\",\"LedgerIndex\":\"3F5072C4875F32ED770DAF3610A716600ED7C7BB0348FADC7A98E011BB2CD36F\",\"PreviousFields\":{\"Balance\":\"30000000\",\"Flags\":4194304,\"Sequence\":1},\"PreviousTxnID\":\"3FB0350A3742BBCC0D8AA3C5247D1AEC01177D0A24D9C34762BAA2FEA8AD88B3\",\"PreviousTxnLgrSeq\":8206397}}],\"TransactionIndex\":5,\"TransactionResult\":\"tesSUCCESS\"},\"validated\":true}"
return this.api.getTransaction(hash, options).then(
_.partial(checkResult, expected,
@@ -3447,6 +3447,7 @@ describe('RippleAPI', function () {
_.partial(checkResult, responses.getTrustlines.all, 'getTrustlines'));
});
// @deprecated See corresponding test in `x-address-api-test.js`
it('generateAddress', function () {
function random() {
return _.fill(Array(16), 0);

View File

@@ -1,6 +1,8 @@
'use strict';
module.exports = {
ACCOUNT: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59',
ACCOUNT_X: 'X7AcgcsBL6XDcUb289X4mJ8djcdyKaB5hJDWMArnXr61cqZ',
ACCOUNT_T: 'T719a5UwUCnEs54UsxG9CJYYDhwmFCqkr7wxCcNcfZ6p5GZ',
OTHER_ACCOUNT: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo',
THIRD_ACCOUNT: 'rwBYyfufTzk77zUSKEu4MvixfarC35av1J',
FOURTH_ACCOUNT: 'rJnZ4YHCUsHvQu7R6mZohevKJDHFzVD6Zr',

View File

@@ -1,4 +1,6 @@
{
"xAddress": "XVLcsWWNiFdUEqoDmSwgxh1abfddG1LtbGFk7omPgYpbyE8",
"classicAddress": "rGCkuB7PBr5tNy68tPEABEtcdno4hE6Y7f",
"address": "rGCkuB7PBr5tNy68tPEABEtcdno4hE6Y7f",
"secret": "sp6JS7f14BuwFY8Mw6bTtLKWauoUs"
}

View File

@@ -0,0 +1,4 @@
{
"xAddress": "XVLcsWWNiFdUEqoDmSwgxh1abfddG1LtbGFk7omPgYpbyE8",
"secret": "sp6JS7f14BuwFY8Mw6bTtLKWauoUs"
}

View File

@@ -5,6 +5,7 @@ function buildList(options) {
}
module.exports = {
generateXAddress: require('./generate-x-address.json'),
generateAddress: require('./generate-address.json'),
getAccountInfo: require('./get-account-info.json'),
getAccountObjects: require('./get-account-objects.json'),

4034
test/x-address-api-test.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -99,6 +99,13 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"
integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==
"@types/base-x@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/base-x/-/base-x-3.0.0.tgz#a1365259d1d3fa3ff973ab543192a6bdd4cb2f90"
integrity sha512-vnqSlpsv9uFX5/z8GyKWAfWHhLGJDBkrgRRsnxlsX23DHOlNyqP/eHQiv4TwnYcZULzQIxaWA/xRWU9Dyy4qzw==
dependencies:
"@types/node" "*"
"@types/lodash@^4.14.136":
version "4.14.144"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.144.tgz#12e57fc99064bce45e5ab3c8bc4783feb75eab8e"
@@ -4389,6 +4396,15 @@ ripple-address-codec@^3.0.4:
base-x "3.0.4"
create-hash "^1.1.2"
ripple-address-codec@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/ripple-address-codec/-/ripple-address-codec-4.0.0.tgz#c20f39eea38def43d2379462e47bff4adabece30"
integrity sha512-PsKl9aytg6fZG2F4RtfPT0c1gj42suAQY9VvJVGz+DfQTdXQaTT9V/StVhaJ6jhVpl7oCd981BB9p2Kq+Kyrng==
dependencies:
"@types/base-x" "^3.0.0"
base-x "3.0.4"
create-hash "^1.1.2"
ripple-binary-codec@^0.2.4:
version "0.2.4"
resolved "https://registry.yarnpkg.com/ripple-binary-codec/-/ripple-binary-codec-0.2.4.tgz#555d64c52182a215222af6045b5170e43f0530b1"