build: Initial linting setup (#1560)

* sets up linting config and runs `yarn lint --fix` once, so that all changes will show up correctly in future PRs.

* Note that there are still a lot of linter errors.
This commit is contained in:
Nathan Nichols
2021-08-26 21:22:40 -05:00
committed by Mayukha Vadari
parent cfa014c44b
commit 6742e2048a
286 changed files with 15508 additions and 12691 deletions

2
.eslintignore Normal file
View File

@@ -0,0 +1,2 @@
dist
node_modules

99
.eslintrc.js Normal file
View File

@@ -0,0 +1,99 @@
module.exports = {
root: true,
// Make ESLint compatible with TypeScript
parser: '@typescript-eslint/parser',
parserOptions: {
// Enable linting rules with type information from our tsconfig
tsconfigRootDir: __dirname,
project: ['./tsconfig.eslint.json'],
// Allow the use of imports / ES modules
sourceType: 'module',
ecmaFeatures: {
// Enable global strict mode
impliedStrict: true
}
},
// Specify global variables that are predefined
env: {
node: true, // Enable node global variables & Node.js scoping
es2020: true // Add all ECMAScript 2020 globals and automatically set the ecmaVersion parser option to ES2020
},
plugins: [],
extends: ['@xrplf/eslint-config/base', 'plugin:mocha/recommended'],
rules: {
// Certain rippled APIs require snake_case naming
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'interface',
format: ['PascalCase']
},
{
selector: 'interface',
format: ['snake_case']
}
],
// Ignore type imports when counting dependencies.
'import/max-dependencies': [
'error',
{
max: 5,
ignoreTypeImports: true
}
],
// Removes comments and blank lines from the max-line rules
'max-lines-per-function': [
'warn',
{
max: 50,
skipBlankLines: true,
skipComments: true
}
],
'max-lines': [
'warn',
{
max: 250,
skipBlankLines: true,
skipComments: true
}
]
},
overrides: [
{
files: ['test/**/*.ts'],
rules: {
// Removed the max for test files and test helper files, since tests usually need to import more things
'import/max-dependencies': 'off',
// describe blocks count as a function in Mocha tests, and can be insanely long
'max-lines-per-function': 'off',
// Tests can be very long turns off max-line count
'max-lines': 'off',
// We have lots of statements in tests
'max-statements': 'off',
// We have lots of magic numbers in tests
'no-magic-number': 'off'
}
},
{
files: ['.eslintrc.js', 'jest.config.js'],
rules: {
// Removed no-commonjs requirement as eslint must be in common js format
'import/no-commonjs': 'off',
// Removed this as eslint prevents us from doing this differently
'import/unambiguous': 'off'
}
}
]
}

View File

@@ -1,29 +0,0 @@
{
"env": {
"browser": true,
"es6": true,
"node": true,
"mocha": true
},
"extends": [
"eslint:recommended"
],
"globals": {
"NodeJS": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"no-useless-constructor": 0,
"no-unused-vars": 0,
"no-prototype-builtins": 0,
"require-atomic-updates": 0,
"no-dupe-class-members": 0
}
}

View File

@@ -10,10 +10,27 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
unit: # build-and-lint:
# runs-on: ubuntu-latest
# strategy:
# matrix:
# node-version: [14.x]
# steps:
# - uses: actions/checkout@v2
# - name: Use Node.js ${{ matrix.node-version }}
# uses: actions/setup-node@v1
# with:
# node-version: ${{ matrix.node-version }}
# - run: yarn install
# - run: yarn lint
# - run: yarn build
unit:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [12.x, 14.x, 16.x] node-version: [12.x, 14.x, 16.x]
@@ -24,10 +41,8 @@ jobs:
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: yarn install - run: yarn install --ignore-engines
- run: yarn test - run: yarn test
- run: yarn lint
- run: yarn build
integration: integration:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -42,7 +57,7 @@ jobs:
ports: ports:
- 6006:6006 - 6006:6006
options: options:
--health-cmd="wget localhost:6006 || exit 1" --health-interval=5s --health-retries=10 --health-timeout=2s --health-cmd="wget localhost:6006 || exit 1" --health-interval=5s --health-retries=10 --health-timeout=2s
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -50,7 +65,7 @@ jobs:
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: yarn install - run: yarn install --ignore-engines
- run: yarn test:integration - run: yarn test:integration
env: env:
HOST: localhost HOST: localhost
@@ -69,7 +84,7 @@ jobs:
ports: ports:
- 6006:6006 - 6006:6006
options: options:
--health-cmd="wget localhost:6006 || exit 1" --health-interval=5s --health-retries=10 --health-timeout=2s --health-cmd="wget localhost:6006 || exit 1" --health-interval=5s --health-retries=10 --health-timeout=2s
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@@ -1,10 +0,0 @@
{
"parser": "typescript",
"printWidth": 80,
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"quoteProps": "consistent",
"bracketSpacing": false
}

6
.vscode/extenstions.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

35
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,35 @@
{
"editor.tabSize": 2,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"eslint.alwaysShowStatus": true,
"eslint.lintTask.enable": true,
"eslint.codeAction.showDocumentation": {
"enable": true
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true,
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/.hg/store/**": true
},
}

View File

@@ -12,10 +12,6 @@
"unpkg": "build/ripple-latest-min.js", "unpkg": "build/ripple-latest-min.js",
"jsdelivr": "build/ripple-latest-min.js", "jsdelivr": "build/ripple-latest-min.js",
"types": "dist/npm/index.d.ts", "types": "dist/npm/index.d.ts",
"browser": {
"ws": "./dist/npm/client/wsWrapper.js",
"https-proxy-agent": false
},
"directories": { "directories": {
"test": "test" "test": "test"
}, },
@@ -41,8 +37,10 @@
"@types/chai": "^4.2.21", "@types/chai": "^4.2.21",
"@types/mocha": "^9.0.0", "@types/mocha": "^9.0.0",
"@types/node": "^16.4.3", "@types/node": "^16.4.3",
"@typescript-eslint/eslint-plugin": "^2.3.3", "@types/puppeteer": "5.4.4",
"@typescript-eslint/parser": "^2.27.0", "@typescript-eslint/eslint-plugin": "^3.7.0",
"@typescript-eslint/parser": "^3.7.0",
"@xrplf/eslint-config": "^1.1.0",
"assert": "^2.0.0", "assert": "^2.0.0",
"assert-diff": "^3.0.0", "assert-diff": "^3.0.0",
"buffer": "^6.0.2", "buffer": "^6.0.2",
@@ -50,21 +48,29 @@
"crypto-browserify": "^3.12.0", "crypto-browserify": "^3.12.0",
"doctoc": "^2.0.0", "doctoc": "^2.0.0",
"ejs": "^3.0.1", "ejs": "^3.0.1",
"eslint": "^6.5.1", "eslint": "^7.5.0",
"eslint-plugin-array-func": "^3.1.7",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-import": "^2.24.1",
"eslint-plugin-jsdoc": "^29.0.0",
"eslint-plugin-mocha": "^9.0.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-tsdoc": "^0.2.14",
"eventemitter2": "^6.0.0", "eventemitter2": "^6.0.0",
"https-browserify": "^1.0.0", "https-browserify": "^1.0.0",
"json-schema-to-markdown-table": "^0.4.0", "json-schema-to-markdown-table": "^0.4.0",
"mocha": "^9", "mocha": "^9",
"nyc": "^15", "nyc": "^15",
"path-browserify": "1.0.1", "path-browserify": "1.0.1",
"prettier": "^2.0.5", "prettier": "^2.3.2",
"process": "^0.11.10", "process": "^0.11.10",
"puppeteer": "10.2.0", "puppeteer": "10.2.0",
"stream-browserify": "^3.0.0", "stream-browserify": "^3.0.0",
"stream-http": "3.2.0", "stream-http": "3.2.0",
"ts-loader": "^9.2.5", "ts-loader": "^9.2.5",
"ts-node": "^10.1.0", "ts-node": "^10.1.0",
"typescript": "^3.9.9", "typescript": "^3.9.10",
"url": "^0.11.0", "url": "^0.11.0",
"webpack": "^5.6.0", "webpack": "^5.6.0",
"webpack-bundle-analyzer": "^4.1.0", "webpack-bundle-analyzer": "^4.1.0",
@@ -72,21 +78,22 @@
}, },
"scripts": { "scripts": {
"build:schemas": "mkdir -p dist/npm/common && cp -r src/common/schemas dist/npm/common/", "build:schemas": "mkdir -p dist/npm/common && cp -r src/common/schemas dist/npm/common/",
"build:lib": "tsc --build", "build:snippets": "tsc --build ./snippets/tsconfig.json",
"build:lib": "tsc --build tsconfig.build.json",
"build:web": "webpack", "build:web": "webpack",
"build": "yarn build:schemas && yarn build:lib && yarn build:web", "build": "yarn build:schemas && yarn build:lib && yarn build:snippets && yarn build:web",
"analyze": "yarn build:web --analyze", "analyze": "yarn build:web --analyze",
"watch": "yarn build:lib --watch", "watch": "yarn build:lib --watch",
"clean": "rm -rf dist/npm", "clean": "rm -rf dist",
"doctoc": "doctoc docs/index.md --title '# RippleAPI Reference' --github --maxlevel 2", "doctoc": "doctoc docs/index.md --title '# RippleAPI Reference' --github --maxlevel 2",
"docgen": "node --harmony scripts/build_docs.js", "docgen": "node --harmony scripts/build_docs.js",
"prepublish": "yarn clean && yarn build", "prepublish": "yarn clean && yarn build",
"test": "TS_NODE_PROJECT=src/tsconfig.json nyc mocha --config=test/.mocharc.json --exit", "test": "nyc mocha --config=test/.mocharc.json --exit",
"test:integration": "TS_NODE_PROJECT=src/tsconfig.json nyc mocha ./test/integration/*.ts", "test:integration": "TS_NODE_PROJECT=tsconfig.build.json nyc mocha ./test/integration/*.ts",
"test:browser": "TS_NODE_PROJECT=src/tsconfig.json nyc mocha ./test/browser/*.ts", "test:browser": "TS_NODE_PROJECT=tsconfig.build.json nyc mocha ./test/browser/*.ts",
"test:watch": "TS_NODE_PROJECT=src/tsconfig.json mocha --config=test/.mocharc.json --watch --reporter dot", "test:watch": "TS_NODE_PROJECT=src/tsconfig.json mocha --config=test/.mocharc.json --watch --reporter dot",
"format": "prettier --write '{src,test}/**/*.ts'", "format": "prettier --write '{src,test}/**/*.ts'",
"lint": "eslint 'src/**/*.ts' 'test/*.{ts,js}'", "lint": "eslint . --ext .ts --max-warnings 0",
"perf": "./scripts/perf_test.sh", "perf": "./scripts/perf_test.sh",
"compile:snippets": "tsc -p snippets/tsconfig.json", "compile:snippets": "tsc -p snippets/tsconfig.json",
"start:snippet": "npm run compile:snippets && node ./snippets/dist/start.js", "start:snippet": "npm run compile:snippets && node ./snippets/dist/start.js",

View File

@@ -1,9 +1,13 @@
import * as codec from 'ripple-binary-codec' // import * as codec from 'ripple-binary-codec'
const original = codec.decode('12000022800200002400000001201B00EF81E661EC6386F26FC0FFFF0000000000000000000000005553440000000000054F6F784A58F9EFB0A9EB90B83464F9D166461968400000000000000C6940000000000000646AD3504529A0465E2E0000000000000000000000005553440000000000054F6F784A58F9EFB0A9EB90B83464F9D1664619732102F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D87446304402200A693FB5CA6B21250EBDFD8CFF526EE0DF7C9E4E31EB0660692E75E6A93BF5F802203CC39463DDA21386898CA31E18AD1A6828647D65741DD637BAD71BC83E29DB9481145E7B112523F68D2F5E879DB4EAC51C6698A693048314CA6EDC7A28252DAEA6F2045B24F4D7C333E146170112300000000000000000000000005553440000000000054F6F784A58F9EFB0A9EB90B83464F9D166461900') // const original = codec.decode(
// '12000022800200002400000001201B00EF81E661EC6386F26FC0FFFF0000000000000000000000005553440000000000054F6F784A58F9EFB0A9EB90B83464F9D166461968400000000000000C6940000000000000646AD3504529A0465E2E0000000000000000000000005553440000000000054F6F784A58F9EFB0A9EB90B83464F9D1664619732102F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D87446304402200A693FB5CA6B21250EBDFD8CFF526EE0DF7C9E4E31EB0660692E75E6A93BF5F802203CC39463DDA21386898CA31E18AD1A6828647D65741DD637BAD71BC83E29DB9481145E7B112523F68D2F5E879DB4EAC51C6698A693048314CA6EDC7A28252DAEA6F2045B24F4D7C333E146170112300000000000000000000000005553440000000000054F6F784A58F9EFB0A9EB90B83464F9D166461900'
// )
const test = codec.decode('12000022800200002400000017201B008694F261EC6386F26FC0FFFF0000000000000000000000005553440000000000054F6F784A58F9EFB0A9EB90B83464F9D166461968400000000000000C6940000000000000646AD3504529A0465E2E0000000000000000000000005553440000000000054F6F784A58F9EFB0A9EB90B83464F9D1664619732102F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D874473045022100D8B57E8E06EAE27B1343AF8CAD3F501E18260CCF8BCED08066074106F0F191A3022058FEA6CE9E7FA69D1244C3A70F18983CC2DAF0B10CBB86A6677CF2A5D2B8A68081145E7B112523F68D2F5E879DB4EAC51C6698A693048314CA6EDC7A28252DAEA6F2045B24F4D7C333E146170112300000000000000000000000005553440000000000054F6F784A58F9EFB0A9EB90B83464F9D166461900') // const test = codec.decode(
// '12000022800200002400000017201B008694F261EC6386F26FC0FFFF0000000000000000000000005553440000000000054F6F784A58F9EFB0A9EB90B83464F9D166461968400000000000000C6940000000000000646AD3504529A0465E2E0000000000000000000000005553440000000000054F6F784A58F9EFB0A9EB90B83464F9D1664619732102F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D874473045022100D8B57E8E06EAE27B1343AF8CAD3F501E18260CCF8BCED08066074106F0F191A3022058FEA6CE9E7FA69D1244C3A70F18983CC2DAF0B10CBB86A6677CF2A5D2B8A68081145E7B112523F68D2F5E879DB4EAC51C6698A693048314CA6EDC7A28252DAEA6F2045B24F4D7C333E146170112300000000000000000000000005553440000000000054F6F784A58F9EFB0A9EB90B83464F9D166461900'
// )
console.log('original:', JSON.stringify(original)) // console.log('original:', JSON.stringify(original))
console.log('test:', JSON.stringify(test)) // console.log('test:', JSON.stringify(test))

View File

@@ -1,19 +1,22 @@
import {Client} from '../../dist/npm' // import {Client} from '../../dist/npm'
import { TransactionMetadata } from '../../src/models/common/transaction' // import {TransactionMetadata} from '../../src/models/common/transaction'
const client = new Client('wss://s.altnet.rippletest.net:51233') // const client = new Client('wss://s.altnet.rippletest.net:51233')
getTransaction() // getTransaction()
async function getTransaction() { // async function getTransaction() {
await client.connect() // await client.connect()
const ledger = await client.request({command: 'ledger', transactions: true}) // const ledger = await client.request({command: 'ledger', transactions: true})
console.log(ledger) // console.log(ledger)
const tx = await client.request({ // const tx = await client.request({
command: 'tx', // command: 'tx',
transaction: ledger.result.ledger.transactions[0] as string // transaction: ledger.result.ledger.transactions[0] as string
}) // })
console.log(tx) // console.log(tx)
console.log('deliveredAmount:', (tx.result.meta as TransactionMetadata).DeliveredAmount) // console.log(
process.exit(0) // 'deliveredAmount:',
} // (tx.result.meta as TransactionMetadata).DeliveredAmount
// )
// process.exit(0)
// }

View File

@@ -1,18 +1,21 @@
import {Client} from '../../dist/npm' // import {Client} from '../../dist/npm'
import { AccountFlags } from '../../dist/npm/common/constants' // import {AccountFlags} from '../../dist/npm/common/constants'
const client = new Client('wss://s.altnet.rippletest.net:51233') // const client = new Client('wss://s.altnet.rippletest.net:51233')
parseAccountFlags() // parseAccountFlags()
async function parseAccountFlags() { // async function parseAccountFlags() {
await client.connect() // await client.connect()
const account_info = await client.request({command: 'account_info', account: 'rKsdkGhyZH6b2Zzd5hNnEqSv2wpznn4n6N'}) // const account_info = await client.request({
const flags = account_info.result.account_data.Flags // command: 'account_info',
for (const flagName in AccountFlags) { // account: 'rKsdkGhyZH6b2Zzd5hNnEqSv2wpznn4n6N'
if (flags & AccountFlags[flagName]) { // })
console.log(`${flagName} enabled`) // const flags = account_info.result.account_data.Flags
} // for (const flagName in AccountFlags) {
} // if (flags & AccountFlags[flagName]) {
process.exit(0) // console.log(`${flagName} enabled`)
} // }
// }
// process.exit(0)
// }

View File

@@ -1,47 +1,53 @@
import {Client} from '../../dist/npm' // import {Client} from '../../dist/npm'
const client = new Client( // const client = new Client(
// 'wss://s.altnet.rippletest.net:51233' // // 'wss://s.altnet.rippletest.net:51233'
// 'ws://35.158.96.209:51233' // // 'ws://35.158.96.209:51233'
'ws://34.210.87.206:51233' // 'ws://34.210.87.206:51233'
) // )
sign() // sign()
async function sign() { // async function sign() {
await client.connect() // await client.connect()
const pathfind: any = { // const pathfind: any = {
source: { // source: {
address: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', // address: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59',
amount: { // amount: {
currency: 'drops', // currency: 'drops',
value: '100' // value: '100'
} // }
}, // },
destination: { // destination: {
address: 'rKT4JX4cCof6LcDYRz8o3rGRu7qxzZ2Zwj', // address: 'rKT4JX4cCof6LcDYRz8o3rGRu7qxzZ2Zwj',
amount: { // amount: {
currency: 'USD', // currency: 'USD',
counterparty: 'rVnYNK9yuxBz4uP8zC8LEFokM2nqH3poc' // counterparty: 'rVnYNK9yuxBz4uP8zC8LEFokM2nqH3poc'
} // }
} // }
} // }
await client.getPaths(pathfind).then(async (data) => { // await client
console.log('paths:', JSON.stringify(data)) // .getPaths(pathfind)
const fakeSecret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' // .then(async (data) => {
// console.log('paths:', JSON.stringify(data))
// const fakeSecret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV'
pathfind.paths = data[0].paths // pathfind.paths = data[0].paths
pathfind.destination = data[0].destination // pathfind.destination = data[0].destination
await client.preparePayment('r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', pathfind).then(ret => { // await client
const signed = client.sign(ret.txJSON, fakeSecret) // .preparePayment('r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', pathfind)
console.log('signed:', signed) // .then((ret) => {
}).catch(err => { // const signed = client.sign(ret.txJSON, fakeSecret)
console.log('ERR 1:', JSON.stringify(err)) // console.log('signed:', signed)
}) // })
}).catch(err => { // .catch((err) => {
console.log('ERR 2:', err) // console.log('ERR 1:', JSON.stringify(err))
}) // })
// })
// .catch((err) => {
// console.log('ERR 2:', err)
// })
client.disconnect() // client.disconnect()
} // }

View File

@@ -1,201 +1,215 @@
import { // import https = require('https')
Client,
AccountInfoResponse,
LedgerClosedEvent
} from '../../dist/npm'
import https = require('https')
/** // import {Client, AccountInfoResponse, LedgerClosedEvent} from '../../dist/npm'
* When implementing Reliable Transaction Submission, there are many potential solutions, each with different trade-offs. The main decision points are:
* 1) Transaction preparation:
* - How do we decide which account sequence and LastLedgerSequence numbers to use?
* (To prevent unintentional duplicate transactions, an {account, account_sequence} pair can be used as a transaction's idempotency key)
* - How do we decide how much to pay for the transaction fee? (If our transactions have been failing due to low fee, we should consider increasing this value)
* 2) Transaction status retrieval. Options include:
* - Poll for transaction status:
* - On a regular interval (e.g. every 3-5 seconds), or
* - When a new validated ledger is detected
* + (To accommodate an edge case in transaction retrieval, check the sending account's Sequence number to confirm that it has the expected value;
* alternatively, wait until a few additional ledgers have been validated before deciding that a transaction has definitively not been included in a validated ledger)
* - Listen for transaction status: scan all validated transactions to see if our transactions are among them
* 3) What do we do when a transaction fails? It is possible to implement retry logic, but caution is advised. Note that there are a few ways for a transaction to fail:
* A) `tec`: The transaction was included in a ledger but only claimed the transaction fee
* B) `tesSUCCESS` but unexpected result: The transaction was successful but did not have the expected result. This generally does not occur for XRP-to-XRP payments
* C) The transaction was not, and never will be, included in a validated ledger [3C]
*
* References:
* - https://xrpl.org/reliable-transaction-submission.html
* - https://xrpl.org/send-xrp.html
* - https://xrpl.org/look-up-transaction-results.html
* - https://xrpl.org/get-started-with-rippleapi-for-javascript.html
* - https://xrpl.org/monitor-incoming-payments-with-websocket.html
*
* For the implementation in this example, we have made the following decisions:
* 1) The script will choose the account sequence and LastLedgerSequence numbers automatically. We allow ripple-lib to choose the fee.
* Payments are defined upfront, and idempotency is not needed. If the script is run a second time, duplicate payments will result.
* 2) We will listen for notification that a new validated ledger has been found, and poll for transaction status at that time.
* Futhermore, as a precaution, we will wait until the server is 3 ledgers past the transaction's LastLedgerSequence
* (with the transaction nowhere to be seen) before deciding that it has definitively failed per [3C]
* 3) Transactions will not be automatically retried. Transactions are limited to XRP-to-XRP payments and cannot "succeed" in an unexpected way.
*/
reliableTransactionSubmissionExample()
async function reliableTransactionSubmissionExample() { // /**
/** // * When implementing Reliable Transaction Submission, there are many potential solutions, each with different trade-offs. The main decision points are:
* Array of payments to execute. // * 1) Transaction preparation:
* // * - How do we decide which account sequence and LastLedgerSequence numbers to use?
* For brevity, these are XRP-to-XRP payments, taking a source, destination, and an amount in drops. // * (To prevent unintentional duplicate transactions, an {account, account_sequence} pair can be used as a transaction's idempotency key)
* // * - How do we decide how much to pay for the transaction fee? (If our transactions have been failing due to low fee, we should consider increasing this value)
* The script will attempt to make all of these payments as quickly as possible, and report the final status of each. Transactions that fail are NOT retried. // * 2) Transaction status retrieval. Options include:
*/ // * - Poll for transaction status:
const payments = [] // * - On a regular interval (e.g. Every 3-5 seconds), or
// * - When a new validated ledger is detected
// * + (To accommodate an edge case in transaction retrieval, check the sending account's Sequence number to confirm that it has the expected value;
// * alternatively, wait until a few additional ledgers have been validated before deciding that a transaction has definitively not been included in a validated ledger)
// * - Listen for transaction status: scan all validated transactions to see if our transactions are among them
// * 3) What do we do when a transaction fails? It is possible to implement retry logic, but caution is advised. Note that there are a few ways for a transaction to fail:
// * A) `tec`: The transaction was included in a ledger but only claimed the transaction fee
// * B) `tesSUCCESS` but unexpected result: The transaction was successful but did not have the expected result. This generally does not occur for XRP-to-XRP payments
// * C) The transaction was not, and never will be, included in a validated ledger [3C].
// *
// * References:
// * - https://xrpl.org/reliable-transaction-submission.html
// * - https://xrpl.org/send-xrp.html
// * - https://xrpl.org/look-up-transaction-results.html
// * - https://xrpl.org/get-started-with-rippleapi-for-javascript.html
// * - https://xrpl.org/monitor-incoming-payments-with-websocket.html.
// *
// * For the implementation in this example, we have made the following decisions:
// * 1) The script will choose the account sequence and LastLedgerSequence numbers automatically. We allow ripple-lib to choose the fee.
// * Payments are defined upfront, and idempotency is not needed. If the script is run a second time, duplicate payments will result.
// * 2) We will listen for notification that a new validated ledger has been found, and poll for transaction status at that time.
// * Futhermore, as a precaution, we will wait until the server is 3 ledgers past the transaction's LastLedgerSequence
// * (with the transaction nowhere to be seen) before deciding that it has definitively failed per [3C]
// * 3) Transactions will not be automatically retried. Transactions are limited to XRP-to-XRP payments and cannot "succeed" in an unexpected way.
// */
// reliableTransactionSubmissionExample()
const sourceAccount = (await generateTestnetAccount()).account // async function reliableTransactionSubmissionExample() {
console.log(`Generated new Testnet account: ${sourceAccount.classicAddress}/${sourceAccount.secret}`) // /**
// Send amounts from 1 drop to 10 drops // * Array of payments to execute.
for (let i = 1; i <= 10; i++) { // *
payments.push({ // * For brevity, these are XRP-to-XRP payments, taking a source, destination, and an amount in drops.
source: sourceAccount, // *
destination: 'rhsoCozhUxwcyQgzFi1FVRoMVQgk7cZd4L', // Random Testnet destination // * The script will attempt to make all of these payments as quickly as possible, and report the final status of each. Transactions that fail are NOT retried.
amount_drops: i.toString(), // */
}) // const payments = []
}
const results = await performPayments(payments)
console.log(JSON.stringify(results, null, 2))
process.exit(0)
}
async function performPayments(payments) { // const sourceAccount = (await generateTestnetAccount()).account
const finalResults = [] // console.log(
const txFinalizedPromises = [] // `Generated new Testnet account: ${sourceAccount.classicAddress}/${sourceAccount.secret}`
const client = new Client('wss://s.altnet.rippletest.net:51233') // )
await client.connect() // // Send amounts from 1 drop to 10 drops
// for (let i = 1; i <= 10; i++) {
// payments.push({
// source: sourceAccount,
// destination: 'rhsoCozhUxwcyQgzFi1FVRoMVQgk7cZd4L', // Random Testnet destination
// amount_drops: i.toString()
// })
// }
// const results = await performPayments(payments)
// console.log(JSON.stringify(results, null, 2))
// process.exit(0)
// }
for (let i = 0; i < payments.length; i++) { // async function performPayments(payments) {
const payment = payments[i] // const finalResults = []
const account_info: AccountInfoResponse = await client.request({ // const txFinalizedPromises = []
command: 'account_info', // const client = new Client('wss://s.altnet.rippletest.net:51233')
account: payment.source.classicAddress, // await client.connect()
ledger_index: 'current'})
const sequence = account_info.result.account_data.Sequence
const preparedPayment = await client.preparePayment(payment.source.classicAddress, {
source: {
address: payment.source.classicAddress,
amount: {
value: payment.amount_drops,
currency: 'drops'
}
},
destination: {
address: payment.destination,
minAmount: {
value: payment.amount_drops,
currency: 'drops'
}
}
}, {
sequence
})
const signed = client.sign(preparedPayment.txJSON, payment.source.secret)
finalResults.push({
id: signed.id
})
const response = await client.request({command: 'submit', tx_blob: signed.signedTransaction})
// Most of the time we'll get 'tesSUCCESS' or (after many submissions) 'terQUEUED' // for (let i = 0; i < payments.length; i++) {
console.log(`tx ${i} - tentative: ${response.result.engine_result}`) // const payment = payments[i]
// const account_info: AccountInfoResponse = await client.request({
// command: 'account_info',
// account: payment.source.classicAddress,
// ledger_index: 'current'
// })
// const sequence = account_info.result.account_data.Sequence
// const preparedPayment = await client.preparePayment(
// payment.source.classicAddress,
// {
// source: {
// address: payment.source.classicAddress,
// amount: {
// value: payment.amount_drops,
// currency: 'drops'
// }
// },
// destination: {
// address: payment.destination,
// minAmount: {
// value: payment.amount_drops,
// currency: 'drops'
// }
// }
// },
// {
// sequence
// }
// )
// const signed = client.sign(preparedPayment.txJSON, payment.source.secret)
// finalResults.push({
// id: signed.id
// })
// const response = await client.request({
// command: 'submit',
// tx_blob: signed.signedTransaction
// })
const txFinalizedPromise = new Promise<void>((resolve) => { // // Most of the time we'll get 'tesSUCCESS' or (after many submissions) 'terQUEUED'
const ledgerClosedCallback = async (event: LedgerClosedEvent) => { // console.log(`tx ${i} - tentative: ${response.result.engine_result}`)
let status
try {
status = await client.request({command: 'tx', transaction: signed.id})
} catch (e) {
// Typical error when the tx hasn't been validated yet:
if (e.name !== 'MissingLedgerHistoryError') {
console.log(e)
}
if (event.ledger_index > preparedPayment.instructions.maxLedgerVersion + 3) { // const txFinalizedPromise = new Promise<void>((resolve) => {
// Assumptions: // const ledgerClosedCallback = async (event: LedgerClosedEvent) => {
// - We are still connected to the same rippled server // let status
// - No ledger gaps occurred // try {
// - All ledgers between the time we submitted the tx and now have been checked for the tx // status = await client.request({command: 'tx', transaction: signed.id})
status = { // } catch (e) {
finalResult: 'Transaction was not, and never will be, included in a validated ledger' // // Typical error when the tx hasn't been validated yet:
} // if (e.name !== 'MissingLedgerHistoryError') {
} else { // console.log(e)
// Check again later: // }
client.connection.once('ledgerClosed', ledgerClosedCallback)
return
}
}
for (let j = 0; j < finalResults.length; j++) { // if (
if (finalResults[j].id === signed.id) { // event.ledger_index >
finalResults[j].result = status.address ? { // preparedPayment.instructions.maxLedgerVersion + 3
source: status.address, // ) {
destination: status.specification.destination.address, // // Assumptions:
deliveredAmount: status.outcome.deliveredAmount, // // - We are still connected to the same rippled server
result: status.outcome.result, // // - No ledger gaps occurred
timestamp: status.outcome.timestamp, // // - All ledgers between the time we submitted the tx and now have been checked for the tx
ledgerVersion: status.outcome.ledgerVersion // status = {
} : status // finalResult:
process.stdout.write('.') // 'Transaction was not, and never will be, included in a validated ledger'
return resolve() // }
} // } else {
} // // Check again later:
} // client.connection.once('ledgerClosed', ledgerClosedCallback)
client.connection.once('ledgerClosed', ledgerClosedCallback) // return
}) // }
txFinalizedPromises.push(txFinalizedPromise) // }
}
await Promise.all(txFinalizedPromises)
return finalResults
}
/** // for (let j = 0; j < finalResults.length; j++) {
* Generate a new Testnet account by requesting one from the faucet // if (finalResults[j].id === signed.id) {
*/ // finalResults[j].result = status.address
async function generateTestnetAccount(): Promise<{ // ? {
account: { // source: status.address,
xAddress: string, // destination: status.specification.destination.address,
classicAddress, string, // deliveredAmount: status.outcome.deliveredAmount,
secret: string // result: status.outcome.result,
}, // timestamp: status.outcome.timestamp,
balance: number // ledgerVersion: status.outcome.ledgerVersion
}> { // }
const options = { // : status
hostname: 'faucet.altnet.rippletest.net', // process.stdout.write('.')
port: 443, // return resolve()
path: '/accounts', // }
method: 'POST' // }
} // }
return new Promise((resolve, reject) => { // client.connection.once('ledgerClosed', ledgerClosedCallback)
const request = https.request(options, response => { // })
const chunks = [] // txFinalizedPromises.push(txFinalizedPromise)
response.on('data', d => { // }
chunks.push(d) // await Promise.all(txFinalizedPromises)
}) // return finalResults
response.on('end', () => { // }
const body = Buffer.concat(chunks).toString()
// "application/json; charset=utf-8" // /**
if (response.headers['content-type'].startsWith('application/json')) { // * Generate a new Testnet account by requesting one from the faucet.
resolve(JSON.parse(body)) // */
} else { // async function generateTestnetAccount(): Promise<{
reject({ // account: {
statusCode: response.statusCode, // xAddress: string
contentType: response.headers['content-type'], // classicAddress
body // string
}) // secret: string
} // }
}) // balance: number
}) // }> {
request.on('error', error => { // const options = {
console.error(error) // hostname: 'faucet.altnet.rippletest.net',
reject(error) // port: 443,
}) // path: '/accounts',
request.end() // method: 'POST'
}) // }
} // return new Promise((resolve, reject) => {
// const request = https.request(options, (response) => {
// const chunks = []
// response.on('data', (d) => {
// chunks.push(d)
// })
// response.on('end', () => {
// const body = Buffer.concat(chunks).toString()
// // "application/json; charset=utf-8"
// if (response.headers['content-type'].startsWith('application/json')) {
// resolve(JSON.parse(body))
// } else {
// reject({
// statusCode: response.statusCode,
// contentType: response.headers['content-type'],
// body
// })
// }
// })
// })
// request.on('error', (error) => {
// console.error(error)
// reject(error)
// })
// request.end()
// })
// }

View File

@@ -1,12 +1,8 @@
{ {
"extends": "../tsconfig-base", "extends": "../tsconfig.build.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src"
}, },
"references": [
{ "path": "../src" }
],
"include": [ "include": [
"./src/**/*.ts" "./src/**/*.ts"
] ]

View File

@@ -1,12 +1,13 @@
import {fromSeed} from 'bip32' import { fromSeed } from "bip32";
import {mnemonicToSeedSync} from 'bip39' import { mnemonicToSeedSync } from "bip39";
import {decode, encodeForSigning} from 'ripple-binary-codec' import { decode, encodeForSigning } from "ripple-binary-codec";
import {deriveKeypair, generateSeed, verify} from 'ripple-keypairs' import { deriveKeypair, generateSeed, verify } from "ripple-keypairs";
import ECDSA from './common/ecdsa'
import {SignedTransaction} from './common/types/objects' import ECDSA from "./common/ecdsa";
import {signOffline} from './transaction/sign' import { ValidationError } from "./common/errors";
import {SignOptions} from './transaction/types' import { SignedTransaction } from "./common/types/objects";
import {ValidationError} from './common/errors' import { signOffline } from "./transaction/sign";
import { SignOptions } from "./transaction/types";
/** /**
* A utility for deriving a wallet composed of a keypair (publicKey/privateKey). * A utility for deriving a wallet composed of a keypair (publicKey/privateKey).
@@ -14,53 +15,61 @@ import {ValidationError} from './common/errors'
* It provides functionality to sign/verify transactions offline. * It provides functionality to sign/verify transactions offline.
*/ */
class Wallet { class Wallet {
readonly publicKey: string readonly publicKey: string;
readonly privateKey: string readonly privateKey: string;
private static readonly defaultAlgorithm: ECDSA = ECDSA.ed25519 private static readonly defaultAlgorithm: ECDSA = ECDSA.ed25519;
private static readonly defaultDerivationPath: string = "m/44'/144'/0'/0/0" private static readonly defaultDerivationPath: string = "m/44'/144'/0'/0/0";
constructor(publicKey: string, privateKey: string) { constructor(publicKey: string, privateKey: string) {
this.publicKey = publicKey this.publicKey = publicKey;
this.privateKey = privateKey this.privateKey = privateKey;
} }
/** /**
* Derives a wallet from a seed. * Derives a wallet from a seed.
* @param {string} seed A string used to generate a keypair (publicKey/privateKey) to derive a wallet. *
* @param {ECDSA} algorithm The digital signature algorithm to generate an address for. * @param seed - A string used to generate a keypair (publicKey/privateKey) to derive a wallet.
* @returns {Wallet} A Wallet derived from a seed. * @param algorithm - The digital signature algorithm to generate an address for.
* @returns A Wallet derived from a seed.
*/ */
static fromSeed(seed: string, algorithm: ECDSA = Wallet.defaultAlgorithm): Wallet { static fromSeed(
return Wallet.deriveWallet(seed, algorithm) seed: string,
algorithm: ECDSA = Wallet.defaultAlgorithm
): Wallet {
return Wallet.deriveWallet(seed, algorithm);
} }
/** /**
* Derives a wallet from a mnemonic. * Derives a wallet from a mnemonic.
* @param {string} mnemonic A string consisting of words (whitespace delimited) used to derive a wallet. *
* @param {string} derivationPath The path to derive a keypair (publicKey/privateKey) from a seed (that was converted from a mnemonic). * @param mnemonic - A string consisting of words (whitespace delimited) used to derive a wallet.
* @returns {Wallet} A Wallet derived from a mnemonic. * @param derivationPath - The path to derive a keypair (publicKey/privateKey) from a seed (that was converted from a mnemonic).
* @returns A Wallet derived from a mnemonic.
*/ */
static fromMnemonic( static fromMnemonic(
mnemonic: string, mnemonic: string,
derivationPath: string = Wallet.defaultDerivationPath derivationPath: string = Wallet.defaultDerivationPath
): Wallet { ): Wallet {
const seed = mnemonicToSeedSync(mnemonic) const seed = mnemonicToSeedSync(mnemonic);
const masterNode = fromSeed(seed) const masterNode = fromSeed(seed);
const node = masterNode.derivePath(derivationPath) const node = masterNode.derivePath(derivationPath);
if (node.privateKey === undefined) { if (node.privateKey === undefined) {
throw new ValidationError('Unable to derive privateKey from mnemonic input') throw new ValidationError(
"Unable to derive privateKey from mnemonic input"
);
} }
const publicKey = Wallet.hexFromBuffer(node.publicKey) const publicKey = Wallet.hexFromBuffer(node.publicKey);
const privateKey = Wallet.hexFromBuffer(node.privateKey) const privateKey = Wallet.hexFromBuffer(node.privateKey);
return new Wallet(publicKey, `00${privateKey}`) return new Wallet(publicKey, `00${privateKey}`);
} }
/** /**
* Derives a wallet from an entropy (array of random numbers). * Derives a wallet from an entropy (array of random numbers).
* @param {Uint8Array | number[]} entropy An array of random numbers to generate a seed used to derive a wallet. *
* @param {ECDSA} algorithm The digital signature algorithm to generate an address for. * @param entropy - An array of random numbers to generate a seed used to derive a wallet.
* @returns {Wallet} A Wallet derived from an entropy. * @param algorithm - The digital signature algorithm to generate an address for.
* @returns A Wallet derived from an entropy.
*/ */
static fromEntropy( static fromEntropy(
entropy: Uint8Array | number[], entropy: Uint8Array | number[],
@@ -68,45 +77,50 @@ class Wallet {
): Wallet { ): Wallet {
const options = { const options = {
entropy: Uint8Array.from(entropy), entropy: Uint8Array.from(entropy),
algorithm algorithm,
} };
const seed = generateSeed(options) const seed = generateSeed(options);
return Wallet.deriveWallet(seed, algorithm) return Wallet.deriveWallet(seed, algorithm);
} }
private static hexFromBuffer(buffer: Buffer): string { private static hexFromBuffer(buffer: Buffer): string {
return buffer.toString('hex').toUpperCase() return buffer.toString("hex").toUpperCase();
} }
private static deriveWallet(seed: string, algorithm: ECDSA = Wallet.defaultAlgorithm): Wallet { private static deriveWallet(
const {publicKey, privateKey} = deriveKeypair(seed, {algorithm}) seed: string,
return new Wallet(publicKey, privateKey) algorithm: ECDSA = Wallet.defaultAlgorithm
): Wallet {
const { publicKey, privateKey } = deriveKeypair(seed, { algorithm });
return new Wallet(publicKey, privateKey);
} }
/** /**
* Signs a transaction offline. * Signs a transaction offline.
* @param {object} transaction A transaction to be signed offline. *
* @param {SignOptions} options Options to include for signing. * @param transaction - A transaction to be signed offline.
* @returns {SignedTransaction} A signed transaction. * @param options - Options to include for signing.
* @returns A signed transaction.
*/ */
signTransaction( signTransaction(
transaction: any, // TODO: transaction should be typed with Transaction type. transaction: any, // TODO: transaction should be typed with Transaction type.
options: SignOptions = {signAs: ''} options: SignOptions = { signAs: "" }
): SignedTransaction { ): SignedTransaction {
return signOffline(this, JSON.stringify(transaction), options) return signOffline(this, JSON.stringify(transaction), options);
} }
/** /**
* Verifies a signed transaction offline. * Verifies a signed transaction offline.
* @param {string} signedTransaction A signed transaction (hex string of signTransaction result) to be verified offline. *
* @returns {boolean} Returns true if a signedTransaction is valid. * @param signedTransaction - A signed transaction (hex string of signTransaction result) to be verified offline.
* @returns Returns true if a signedTransaction is valid.
*/ */
verifyTransaction(signedTransaction: string): boolean { verifyTransaction(signedTransaction: string): boolean {
const tx = decode(signedTransaction) const tx = decode(signedTransaction);
const messageHex: string = encodeForSigning(tx) const messageHex: string = encodeForSigning(tx);
const signature = tx.TxnSignature const signature = tx.TxnSignature;
return verify(messageHex, signature, this.publicKey) return verify(messageHex, signature, this.publicKey);
} }
} }
export default Wallet export default Wallet;

View File

@@ -1,44 +1,42 @@
/* // Original code based on "backo" - https://github.com/segmentio/backo
* Original code based on "backo" - https://github.com/segmentio/backo // MIT License - Copyright 2014 Segment.io
* MIT License - Copyright 2014 Segment.io // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/** /**
* A Back off strategy that increases exponentially. Useful with repeated * A Back off strategy that increases exponentially. Useful with repeated
* setTimeout calls over a network (where the destination may be down). * setTimeout calls over a network (where the destination may be down).
*/ */
export class ExponentialBackoff { export class ExponentialBackoff {
private readonly ms: number private readonly ms: number;
private readonly max: number private readonly max: number;
private readonly factor: number = 2 private readonly factor: number = 2;
private readonly jitter: number = 0 private readonly jitter: number = 0;
attempts: number = 0 attempts = 0;
constructor(opts: {min?: number; max?: number} = {}) { constructor(opts: { min?: number; max?: number } = {}) {
this.ms = opts.min || 100 this.ms = opts.min || 100;
this.max = opts.max || 10000 this.max = opts.max || 10000;
} }
/** /**
* Return the backoff duration. * Return the backoff duration.
*/ */
duration() { duration() {
var ms = this.ms * Math.pow(this.factor, this.attempts++) let ms = this.ms * this.factor ** this.attempts++;
if (this.jitter) { if (this.jitter) {
var rand = Math.random() const rand = Math.random();
var deviation = Math.floor(rand * this.jitter * ms) const deviation = Math.floor(rand * this.jitter * ms);
ms = (Math.floor(rand * 10) & 1) == 0 ? ms - deviation : ms + deviation ms = (Math.floor(rand * 10) & 1) == 0 ? ms - deviation : ms + deviation;
} }
return Math.min(ms, this.max) | 0 return Math.min(ms, this.max) | 0;
} }
/** /**
* Reset the number of attempts. * Reset the number of attempts.
*/ */
reset() { reset() {
this.attempts = 0 this.attempts = 0;
} }
} }

View File

@@ -1,62 +1,66 @@
import {Client, ClientOptions} from './' import { Client, ClientOptions } from ".";
class BroadcastClient extends Client { class BroadcastClient extends Client {
ledgerVersion: number | undefined = undefined ledgerVersion: number | undefined = undefined;
private _clients: Client[] private readonly _clients: Client[];
constructor(servers, options: ClientOptions = {}) { constructor(servers, options: ClientOptions = {}) {
super(servers[0], options) super(servers[0], options);
const clients: Client[] = servers.map( const clients: Client[] = servers.map(
(server) => new Client(server, options) (server) => new Client(server, options)
) );
// exposed for testing // exposed for testing
this._clients = clients this._clients = clients;
this.getMethodNames().forEach((name) => { this.getMethodNames().forEach((name) => {
this[name] = function () { this[name] = function () {
// eslint-disable-line no-loop-func // eslint-disable-line no-loop-func
return Promise.race(clients.map((client) => client[name](...arguments))) return Promise.race(
} clients.map((client) => client[name](...arguments))
}) );
};
});
// connection methods must be overridden to apply to all client instances // connection methods must be overridden to apply to all client instances
this.connect = async function () { this.connect = async function () {
await Promise.all(clients.map((client) => client.connect())) await Promise.all(clients.map((client) => client.connect()));
} };
this.disconnect = async function () { this.disconnect = async function () {
await Promise.all(clients.map((client) => client.disconnect())) await Promise.all(clients.map((client) => client.disconnect()));
} };
this.isConnected = function () { this.isConnected = function () {
return clients.map((client) => client.isConnected()).every(Boolean) return clients.map((client) => client.isConnected()).every(Boolean);
} };
// synchronous methods are all passed directly to the first client instance // synchronous methods are all passed directly to the first client instance
const defaultClient = clients[0] const defaultClient = clients[0];
const syncMethods = ['sign'] const syncMethods = ["sign"];
syncMethods.forEach((name) => { syncMethods.forEach((name) => {
this[name] = defaultClient[name].bind(defaultClient) this[name] = defaultClient[name].bind(defaultClient);
}) });
clients.forEach((client) => { clients.forEach((client) => {
client.on('error', (errorCode, errorMessage, data) => client.on("error", (errorCode, errorMessage, data) =>
this.emit('error', errorCode, errorMessage, data) this.emit("error", errorCode, errorMessage, data)
) );
}) });
} }
getMethodNames() { getMethodNames() {
const methodNames: string[] = [] const methodNames: string[] = [];
const firstClient = this._clients[0] const firstClient = this._clients[0];
const methods = Object.getOwnPropertyNames(firstClient) const methods = Object.getOwnPropertyNames(firstClient);
methods.push(...Object.getOwnPropertyNames(Object.getPrototypeOf(firstClient))) methods.push(
...Object.getOwnPropertyNames(Object.getPrototypeOf(firstClient))
);
for (const name of methods) { for (const name of methods) {
if (typeof firstClient[name] === 'function' && name !== 'constructor') { if (typeof firstClient[name] === "function" && name !== "constructor") {
methodNames.push(name) methodNames.push(name);
} }
} }
return methodNames return methodNames;
} }
} }
export {BroadcastClient} export { BroadcastClient };

View File

@@ -1,7 +1,9 @@
import _ from 'lodash' import { EventEmitter } from "events";
import {EventEmitter} from 'events' import { parse as parseURL } from "url";
import {parse as parseURL} from 'url'
import WebSocket from 'ws' import _ from "lodash";
import WebSocket from "ws";
import { import {
RippledError, RippledError,
DisconnectedError, DisconnectedError,
@@ -9,25 +11,26 @@ import {
TimeoutError, TimeoutError,
ResponseFormatError, ResponseFormatError,
ConnectionError, ConnectionError,
RippleError RippleError,
} from '../common/errors' } from "../common/errors";
import {ExponentialBackoff} from './backoff' import { Response } from "../models/methods";
import { Response } from '../models/methods'
import { ExponentialBackoff } from "./backoff";
/** /**
* ConnectionOptions is the configuration for the Connection class. * ConnectionOptions is the configuration for the Connection class.
*/ */
export interface ConnectionOptions { export interface ConnectionOptions {
trace?: boolean | ((id: string, message: string) => void) trace?: boolean | ((id: string, message: string) => void);
proxy?: string proxy?: string;
proxyAuthorization?: string proxyAuthorization?: string;
authorization?: string authorization?: string;
trustedCertificates?: string[] trustedCertificates?: string[];
key?: string key?: string;
passphrase?: string passphrase?: string;
certificate?: string certificate?: string;
timeout: number // request timeout timeout: number; // request timeout
connectionTimeout: number connectionTimeout: number;
} }
/** /**
@@ -35,82 +38,88 @@ export interface ConnectionOptions {
* is optional, so any ConnectionOptions configuration that has a default value is * is optional, so any ConnectionOptions configuration that has a default value is
* still optional at the point that the user provides it. * still optional at the point that the user provides it.
*/ */
export type ConnectionUserOptions = Partial<ConnectionOptions> export type ConnectionUserOptions = Partial<ConnectionOptions>;
/** //
* Represents an intentionally triggered web-socket disconnect code. // Represents an intentionally triggered web-socket disconnect code.
* WebSocket spec allows 4xxx codes for app/library specific codes. // WebSocket spec allows 4xxx codes for app/library specific codes.
* See: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent // See: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
**/ //
const INTENTIONAL_DISCONNECT_CODE = 4000 const INTENTIONAL_DISCONNECT_CODE = 4000;
/** /**
* Create a new websocket given your URL and optional proxy/certificate * Create a new websocket given your URL and optional proxy/certificate
* configuration. * configuration.
*
* @param url
* @param config
*/ */
function createWebSocket(url: string, config: ConnectionOptions): WebSocket { function createWebSocket(url: string, config: ConnectionOptions): WebSocket {
const options: WebSocket.ClientOptions = {} const options: WebSocket.ClientOptions = {};
if (config.proxy != null) { if (config.proxy != null) {
// TODO: replace deprecated method // TODO: replace deprecated method
const parsedURL = parseURL(url) const parsedURL = parseURL(url);
const parsedProxyURL = parseURL(config.proxy) const parsedProxyURL = parseURL(config.proxy);
const proxyOverrides = _.omitBy( const proxyOverrides = _.omitBy(
{ {
secureEndpoint: parsedURL.protocol === 'wss:', secureEndpoint: parsedURL.protocol === "wss:",
secureProxy: parsedProxyURL.protocol === 'https:', secureProxy: parsedProxyURL.protocol === "https:",
auth: config.proxyAuthorization, auth: config.proxyAuthorization,
ca: config.trustedCertificates, ca: config.trustedCertificates,
key: config.key, key: config.key,
passphrase: config.passphrase, passphrase: config.passphrase,
cert: config.certificate cert: config.certificate,
}, },
(value) => value == null (value) => value == null
) );
const proxyOptions = {...parsedProxyURL, ...proxyOverrides} const proxyOptions = { ...parsedProxyURL, ...proxyOverrides };
let HttpsProxyAgent let HttpsProxyAgent;
try { try {
HttpsProxyAgent = require('https-proxy-agent') HttpsProxyAgent = require("https-proxy-agent");
} catch (error) { } catch (error) {
throw new Error('"proxy" option is not supported in the browser') throw new Error('"proxy" option is not supported in the browser');
} }
options.agent = new HttpsProxyAgent(proxyOptions) options.agent = new HttpsProxyAgent(proxyOptions);
} }
if (config.authorization != null) { if (config.authorization != null) {
const base64 = Buffer.from(config.authorization).toString('base64') const base64 = Buffer.from(config.authorization).toString("base64");
options.headers = {Authorization: `Basic ${base64}`} options.headers = { Authorization: `Basic ${base64}` };
} }
const optionsOverrides = _.omitBy( const optionsOverrides = _.omitBy(
{ {
ca: config.trustedCertificates, ca: config.trustedCertificates,
key: config.key, key: config.key,
passphrase: config.passphrase, passphrase: config.passphrase,
cert: config.certificate cert: config.certificate,
}, },
(value) => value == null (value) => value == null
) );
const websocketOptions = {...options, ...optionsOverrides} const websocketOptions = { ...options, ...optionsOverrides };
const websocket = new WebSocket(url, null, websocketOptions) const websocket = new WebSocket(url, websocketOptions);
// we will have a listener for each outstanding request, // we will have a listener for each outstanding request,
// so we have to raise the limit (the default is 10) // so we have to raise the limit (the default is 10)
if (typeof websocket.setMaxListeners === 'function') { if (typeof websocket.setMaxListeners === "function") {
websocket.setMaxListeners(Infinity) websocket.setMaxListeners(Infinity);
} }
return websocket return websocket;
} }
/** /**
* ws.send(), but promisified. * Ws.send(), but promisified.
*
* @param ws
* @param message
*/ */
function websocketSendAsync(ws: WebSocket, message: string) { function websocketSendAsync(ws: WebSocket, message: string) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
ws.send(message, undefined, (error) => { ws.send(message, (error) => {
if (error) { if (error) {
reject(new DisconnectedError(error.message, error)) reject(new DisconnectedError(error.message, error));
} else { } else {
resolve() resolve();
} }
}) });
}) });
} }
/** /**
@@ -119,25 +128,25 @@ function websocketSendAsync(ws: WebSocket, message: string) {
* after-the-fact. * after-the-fact.
*/ */
class ConnectionManager { class ConnectionManager {
private promisesAwaitingConnection: { private promisesAwaitingConnection: Array<{
resolve: Function resolve: Function;
reject: Function reject: Function;
}[] = [] }> = [];
resolveAllAwaiting() { resolveAllAwaiting() {
this.promisesAwaitingConnection.map(({resolve}) => resolve()) this.promisesAwaitingConnection.map(({ resolve }) => resolve());
this.promisesAwaitingConnection = [] this.promisesAwaitingConnection = [];
} }
rejectAllAwaiting(error: Error) { rejectAllAwaiting(error: Error) {
this.promisesAwaitingConnection.map(({reject}) => reject(error)) this.promisesAwaitingConnection.map(({ reject }) => reject(error));
this.promisesAwaitingConnection = [] this.promisesAwaitingConnection = [];
} }
awaitConnection(): Promise<void> { awaitConnection(): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.promisesAwaitingConnection.push({resolve, reject}) this.promisesAwaitingConnection.push({ resolve, reject });
}) });
} }
} }
@@ -148,231 +157,247 @@ class ConnectionManager {
* original request. * original request.
*/ */
class RequestManager { class RequestManager {
private nextId = 0 private nextId = 0;
private promisesAwaitingResponse: { private promisesAwaitingResponse: Array<{
resolve: Function resolve: Function;
reject: Function reject: Function;
timer: NodeJS.Timeout timer: NodeJS.Timeout;
}[] = [] }> = [];
cancel(id: number) { cancel(id: number) {
const {timer} = this.promisesAwaitingResponse[id] const { timer } = this.promisesAwaitingResponse[id];
clearTimeout(timer) clearTimeout(timer);
delete this.promisesAwaitingResponse[id] delete this.promisesAwaitingResponse[id];
} }
resolve(id: string | number, data: Response) { resolve(id: string | number, data: Response) {
const {timer, resolve} = this.promisesAwaitingResponse[id] const { timer, resolve } = this.promisesAwaitingResponse[id];
clearTimeout(timer) clearTimeout(timer);
resolve(data) resolve(data);
delete this.promisesAwaitingResponse[id] delete this.promisesAwaitingResponse[id];
} }
reject(id: string | number, error: Error) { reject(id: string | number, error: Error) {
const {timer, reject} = this.promisesAwaitingResponse[id] const { timer, reject } = this.promisesAwaitingResponse[id];
clearTimeout(timer) clearTimeout(timer);
reject(error) reject(error);
delete this.promisesAwaitingResponse[id] delete this.promisesAwaitingResponse[id];
} }
rejectAll(error: Error) { rejectAll(error: Error) {
this.promisesAwaitingResponse.forEach((_, id) => { this.promisesAwaitingResponse.forEach((_, id) => {
this.reject(id, error) this.reject(id, error);
}) });
} }
/** /**
* Creates a new WebSocket request. This sets up a timeout timer to catch * Creates a new WebSocket request. This sets up a timeout timer to catch
* hung responses, and a promise that will resolve with the response once * hung responses, and a promise that will resolve with the response once
* the response is seen & handled. * the response is seen & handled.
*
* @param data
* @param timeout
*/ */
createRequest(data: any, timeout: number): [string | number, string, Promise<any>] { createRequest(
const newId = data.id ? data.id : this.nextId++ data: any,
const newData = JSON.stringify({...data, id: newId}) timeout: number
): [string | number, string, Promise<any>] {
const newId = data.id ? data.id : this.nextId++;
const newData = JSON.stringify({ ...data, id: newId });
const timer = setTimeout( const timer = setTimeout(
() => this.reject(newId, new TimeoutError()), () => this.reject(newId, new TimeoutError()),
timeout timeout
) );
// Node.js won't exit if a timer is still running, so we tell Node to ignore. // Node.js won't exit if a timer is still running, so we tell Node to ignore.
// (Node will still wait for the request to complete). // (Node will still wait for the request to complete).
if (timer.unref) { if (timer.unref) {
timer.unref() timer.unref();
} }
const newPromise = new Promise((resolve: (data: Response) => void, reject) => { const newPromise = new Promise(
this.promisesAwaitingResponse[newId] = {resolve, reject, timer} (resolve: (data: Response) => void, reject) => {
}) this.promisesAwaitingResponse[newId] = { resolve, reject, timer };
return [newId, newData, newPromise] }
);
return [newId, newData, newPromise];
} }
/** /**
* Handle a "response". Responses match to the earlier request handlers, * Handle a "response". Responses match to the earlier request handlers,
* and resolve/reject based on the data received. * and resolve/reject based on the data received.
*
* @param data
*/ */
handleResponse(data: Response) { handleResponse(data: Response) {
if (!Number.isInteger(data.id) || data.id < 0) { if (!Number.isInteger(data.id) || data.id < 0) {
throw new ResponseFormatError('valid id not found in response', data) throw new ResponseFormatError("valid id not found in response", data);
} }
if (!this.promisesAwaitingResponse[data.id]) { if (!this.promisesAwaitingResponse[data.id]) {
return return;
} }
if (data.status === 'error') { if (data.status === "error") {
const error = new RippledError(data.error_message || data.error, data) const error = new RippledError(data.error_message || data.error, data);
this.reject(data.id, error) this.reject(data.id, error);
return return;
} }
if (data.status !== 'success') { if (data.status !== "success") {
const error = new ResponseFormatError( const error = new ResponseFormatError(
`unrecognized status: ${data.status}`, `unrecognized status: ${data.status}`,
data data
) );
this.reject(data.id, error) this.reject(data.id, error);
return return;
} }
this.resolve(data.id, data) this.resolve(data.id, data);
} }
} }
/** /**
* The main Connection class. Responsible for connecting to & managing * The main Connection class. Responsible for connecting to & managing
* an active WebSocket connection to a XRPL node. * an active WebSocket connection to a XRPL node.
*
* @param errorOrCode
*/ */
export class Connection extends EventEmitter { export class Connection extends EventEmitter {
private _url: string private readonly _url: string | undefined;
private _ws: null | WebSocket = null private _ws: null | WebSocket = null;
private _reconnectTimeoutID: null | NodeJS.Timeout = null private _reconnectTimeoutID: null | NodeJS.Timeout = null;
private _heartbeatIntervalID: null | NodeJS.Timeout = null private _heartbeatIntervalID: null | NodeJS.Timeout = null;
private _retryConnectionBackoff = new ExponentialBackoff({ private readonly _retryConnectionBackoff = new ExponentialBackoff({
min: 100, min: 100,
max: 60 * 1000 max: 60 * 1000,
}) });
private _trace: (id: string, message: string) => void = () => {} private readonly _trace: (id: string, message: string) => void = () => {};
private _config: ConnectionOptions private readonly _config: ConnectionOptions;
private _requestManager = new RequestManager() private readonly _requestManager = new RequestManager();
private _connectionManager = new ConnectionManager() private readonly _connectionManager = new ConnectionManager();
constructor(url?: string, options: ConnectionUserOptions = {}) { constructor(url?: string, options: ConnectionUserOptions = {}) {
super() super();
this.setMaxListeners(Infinity) this.setMaxListeners(Infinity);
this._url = url this._url = url;
this._config = { this._config = {
timeout: 20 * 1000, timeout: 20 * 1000,
connectionTimeout: 5 * 1000, connectionTimeout: 5 * 1000,
...options ...options,
} };
if (typeof options.trace === 'function') { if (typeof options.trace === "function") {
this._trace = options.trace this._trace = options.trace;
} else if (options.trace === true) { } else if (options.trace) {
this._trace = console.log this._trace = console.log;
} }
} }
private _onMessage(message) { private _onMessage(message) {
this._trace('receive', message) this._trace("receive", message);
let data: any let data: any;
try { try {
data = JSON.parse(message) data = JSON.parse(message);
} catch (error) { } catch (error) {
this.emit('error', 'badMessage', error.message, message) this.emit("error", "badMessage", error.message, message);
return return;
} }
if (data.type == null && data.error) { if (data.type == null && data.error) {
this.emit('error', data.error, data.error_message, data) // e.g. slowDown this.emit("error", data.error, data.error_message, data); // e.g. slowDown
return return;
} }
if (data.type) { if (data.type) {
this.emit(data.type, data) this.emit(data.type, data);
} }
if (data.type === 'response') { if (data.type === "response") {
try { try {
this._requestManager.handleResponse(data) this._requestManager.handleResponse(data);
} catch (error) { } catch (error) {
this.emit('error', 'badMessage', error.message, message) this.emit("error", "badMessage", error.message, message);
} }
} }
} }
private get _state() { private get _state() {
return this._ws ? this._ws.readyState : WebSocket.CLOSED return this._ws ? this._ws.readyState : WebSocket.CLOSED;
} }
private get _shouldBeConnected() { private get _shouldBeConnected() {
return this._ws !== null return this._ws !== null;
} }
private _clearHeartbeatInterval = () => { private readonly _clearHeartbeatInterval = () => {
clearInterval(this._heartbeatIntervalID) if (this._heartbeatIntervalID) {
} clearInterval(this._heartbeatIntervalID);
}
};
private _startHeartbeatInterval = () => { private readonly _startHeartbeatInterval = () => {
this._clearHeartbeatInterval() this._clearHeartbeatInterval();
this._heartbeatIntervalID = setInterval( this._heartbeatIntervalID = setInterval(
() => this._heartbeat(), () => this._heartbeat(),
this._config.timeout this._config.timeout
) );
} };
/** /**
* A heartbeat is just a "ping" command, sent on an interval. * A heartbeat is just a "ping" command, sent on an interval.
* If this succeeds, we're good. If it fails, disconnect so that the consumer can reconnect, if desired. * If this succeeds, we're good. If it fails, disconnect so that the consumer can reconnect, if desired.
*/ */
private _heartbeat = () => { private readonly _heartbeat = () => {
return this.request({command: 'ping'}).catch(() => { return this.request({ command: "ping" }).catch(() => {
return this.reconnect().catch((error) => { return this.reconnect().catch((error) => {
this.emit('error', 'reconnect', error.message, error) this.emit("error", "reconnect", error.message, error);
}) });
}) });
} };
private _onConnectionFailed = (errorOrCode: Error | number | null) => { private readonly _onConnectionFailed = (
errorOrCode: Error | number | null
) => {
if (this._ws) { if (this._ws) {
this._ws.removeAllListeners() this._ws.removeAllListeners();
this._ws.on('error', () => { this._ws.on("error", () => {
// Correctly listen for -- but ignore -- any future errors: If you // Correctly listen for -- but ignore -- any future errors: If you
// don't have a listener on "error" node would log a warning on error. // don't have a listener on "error" node would log a warning on error.
}) });
this._ws.close() this._ws.close();
this._ws = null this._ws = null;
} }
if (typeof errorOrCode === 'number') { if (typeof errorOrCode === "number") {
this._connectionManager.rejectAllAwaiting( this._connectionManager.rejectAllAwaiting(
new NotConnectedError(`Connection failed with code ${errorOrCode}.`, { new NotConnectedError(`Connection failed with code ${errorOrCode}.`, {
code: errorOrCode code: errorOrCode,
}) })
) );
} else if (errorOrCode && errorOrCode.message) { } else if (errorOrCode && errorOrCode.message) {
this._connectionManager.rejectAllAwaiting( this._connectionManager.rejectAllAwaiting(
new NotConnectedError(errorOrCode.message, errorOrCode) new NotConnectedError(errorOrCode.message, errorOrCode)
) );
} else { } else {
this._connectionManager.rejectAllAwaiting( this._connectionManager.rejectAllAwaiting(
new NotConnectedError('Connection failed.') new NotConnectedError("Connection failed.")
) );
} }
} };
isConnected() { isConnected() {
return this._state === WebSocket.OPEN return this._state === WebSocket.OPEN;
} }
connect(): Promise<void> { connect(): Promise<void> {
if (this.isConnected()) { if (this.isConnected()) {
return Promise.resolve() return Promise.resolve();
} }
if (this._state === WebSocket.CONNECTING) { if (this._state === WebSocket.CONNECTING) {
return this._connectionManager.awaitConnection() return this._connectionManager.awaitConnection();
} }
if (!this._url) { if (!this._url) {
return Promise.reject( return Promise.reject(
new ConnectionError('Cannot connect because no server was specified') new ConnectionError("Cannot connect because no server was specified")
) );
} }
if (this._ws) { if (this._ws) {
return Promise.reject( return Promise.reject(
new RippleError('Websocket connection never cleaned up.', { new RippleError("Websocket connection never cleaned up.", {
state: this._state state: this._state,
}) })
) );
} }
// Create the connection timeout, in case the connection hangs longer than expected. // Create the connection timeout, in case the connection hangs longer than expected.
@@ -383,58 +408,71 @@ export class Connection extends EventEmitter {
`If your internet connection is working, the rippled server may be blocked or inaccessible. ` + `If your internet connection is working, the rippled server may be blocked or inaccessible. ` +
`You can also try setting the 'connectionTimeout' option in the Client constructor.` `You can also try setting the 'connectionTimeout' option in the Client constructor.`
) )
) );
}, this._config.connectionTimeout) }, this._config.connectionTimeout);
// Connection listeners: these stay attached only until a connection is done/open. // Connection listeners: these stay attached only until a connection is done/open.
this._ws = createWebSocket(this._url, this._config) this._ws = createWebSocket(this._url, this._config);
this._ws.on('error', this._onConnectionFailed)
this._ws.on('error', () => clearTimeout(connectionTimeoutID)) if (this._ws == null) {
this._ws.on('close', this._onConnectionFailed) throw new Error("Connect: created null websocket");
this._ws.on('close', () => clearTimeout(connectionTimeoutID)) }
this._ws.once('open', async () => {
this._ws.on("error", this._onConnectionFailed);
this._ws.on("error", () => clearTimeout(connectionTimeoutID));
this._ws.on("close", this._onConnectionFailed);
this._ws.on("close", () => clearTimeout(connectionTimeoutID));
this._ws.once("open", async () => {
if (this._ws == null) {
throw new Error("onceOpen: ws is null");
}
// Once the connection completes successfully, remove all old listeners // Once the connection completes successfully, remove all old listeners
this._ws.removeAllListeners() this._ws.removeAllListeners();
clearTimeout(connectionTimeoutID) clearTimeout(connectionTimeoutID);
// Add new, long-term connected listeners for messages and errors // Add new, long-term connected listeners for messages and errors
this._ws.on('message', (message: string) => this._onMessage(message)) this._ws.on("message", (message: string) => this._onMessage(message));
this._ws.on('error', (error) => this._ws.on("error", (error) =>
this.emit('error', 'websocket', error.message, error) this.emit("error", "websocket", error.message, error)
) );
// Handle a closed connection: reconnect if it was unexpected // Handle a closed connection: reconnect if it was unexpected
this._ws.once('close', (code, reason) => { this._ws.once("close", (code, reason) => {
this._clearHeartbeatInterval() if (this._ws == null) {
throw new Error("onceClose: ws is null");
}
this._clearHeartbeatInterval();
this._requestManager.rejectAll( this._requestManager.rejectAll(
new DisconnectedError(`websocket was closed, ${reason}`) new DisconnectedError(`websocket was closed, ${reason}`)
) );
this._ws.removeAllListeners() this._ws.removeAllListeners();
this._ws = null this._ws = null;
this.emit('disconnected', code) this.emit("disconnected", code);
// If this wasn't a manual disconnect, then lets reconnect ASAP. // If this wasn't a manual disconnect, then lets reconnect ASAP.
if (code !== INTENTIONAL_DISCONNECT_CODE) { if (code !== INTENTIONAL_DISCONNECT_CODE) {
const retryTimeout = this._retryConnectionBackoff.duration() const retryTimeout = this._retryConnectionBackoff.duration();
this._trace('reconnect', `Retrying connection in ${retryTimeout}ms.`) this._trace("reconnect", `Retrying connection in ${retryTimeout}ms.`);
this.emit('reconnecting', this._retryConnectionBackoff.attempts) this.emit("reconnecting", this._retryConnectionBackoff.attempts);
// Start the reconnect timeout, but set it to `this._reconnectTimeoutID` // Start the reconnect timeout, but set it to `this._reconnectTimeoutID`
// so that we can cancel one in-progress on disconnect. // so that we can cancel one in-progress on disconnect.
this._reconnectTimeoutID = setTimeout(() => { this._reconnectTimeoutID = setTimeout(() => {
this.reconnect().catch((error) => { this.reconnect().catch((error) => {
this.emit('error', 'reconnect', error.message, error) this.emit("error", "reconnect", error.message, error);
}) });
}, retryTimeout) }, retryTimeout);
} }
}) });
// Finalize the connection and resolve all awaiting connect() requests // Finalize the connection and resolve all awaiting connect() requests
try { try {
this._retryConnectionBackoff.reset() this._retryConnectionBackoff.reset();
this._startHeartbeatInterval() this._startHeartbeatInterval();
this._connectionManager.resolveAllAwaiting() this._connectionManager.resolveAllAwaiting();
this.emit('connected') this.emit("connected");
} catch (error) { } catch (error) {
this._connectionManager.rejectAllAwaiting(error) this._connectionManager.rejectAllAwaiting(error);
await this.disconnect().catch(() => {}) // Ignore this error, propagate the root cause. await this.disconnect().catch(() => {}); // Ignore this error, propagate the root cause.
} }
}) });
return this._connectionManager.awaitConnection() return this._connectionManager.awaitConnection();
} }
/** /**
@@ -445,20 +483,30 @@ export class Connection extends EventEmitter {
* If no open websocket connection exists, resolve with no code (`undefined`). * If no open websocket connection exists, resolve with no code (`undefined`).
*/ */
disconnect(): Promise<number | undefined> { disconnect(): Promise<number | undefined> {
clearTimeout(this._reconnectTimeoutID) if (this._reconnectTimeoutID !== null) {
this._reconnectTimeoutID = null clearTimeout(this._reconnectTimeoutID);
if (this._state === WebSocket.CLOSED || !this._ws) { this._reconnectTimeoutID = null;
return Promise.resolve(undefined)
} }
if (this._state === WebSocket.CLOSED) {
return Promise.resolve(undefined);
}
if (this._ws === null) {
return Promise.resolve(undefined);
}
return new Promise((resolve) => { return new Promise((resolve) => {
this._ws.once('close', (code) => resolve(code)) if (this._ws === null) {
return Promise.resolve(undefined);
}
this._ws.once("close", (code) => resolve(code));
// Connection already has a disconnect handler for the disconnect logic. // Connection already has a disconnect handler for the disconnect logic.
// Just close the websocket manually (with our "intentional" code) to // Just close the websocket manually (with our "intentional" code) to
// trigger that. // trigger that.
if (this._state !== WebSocket.CLOSING) { if (this._ws != null && this._state !== WebSocket.CLOSING) {
this._ws.close(INTENTIONAL_DISCONNECT_CODE) this._ws.close(INTENTIONAL_DISCONNECT_CODE);
} }
}) });
} }
/** /**
@@ -468,33 +516,36 @@ export class Connection extends EventEmitter {
// NOTE: We currently have a "reconnecting" event, but that only triggers // NOTE: We currently have a "reconnecting" event, but that only triggers
// through an unexpected connection retry logic. // through an unexpected connection retry logic.
// See: https://github.com/ripple/ripple-lib/pull/1101#issuecomment-565360423 // See: https://github.com/ripple/ripple-lib/pull/1101#issuecomment-565360423
this.emit('reconnect') this.emit("reconnect");
await this.disconnect() await this.disconnect();
await this.connect() await this.connect();
} }
async request<T extends {command: string}>(request: T, timeout?: number): Promise<any> { async request<T extends { command: string }>(
if (!this._shouldBeConnected) { request: T,
throw new NotConnectedError() timeout?: number
): Promise<any> {
if (!this._shouldBeConnected || this._ws == null) {
throw new NotConnectedError();
} }
const [id, message, responsePromise] = this._requestManager.createRequest( const [id, message, responsePromise] = this._requestManager.createRequest(
request, request,
timeout || this._config.timeout timeout || this._config.timeout
) );
this._trace('send', message) this._trace("send", message);
websocketSendAsync(this._ws, message).catch((error) => { websocketSendAsync(this._ws, message).catch((error) => {
this._requestManager.reject(id, error) this._requestManager.reject(id, error);
}) });
return responsePromise return responsePromise;
} }
/** /**
* Get the Websocket connection URL * Get the Websocket connection URL.
* *
* @returns The Websocket connection URL * @returns The Websocket connection URL.
*/ */
getUrl(): string { getUrl(): string {
return this._url return this._url ?? "";
} }
} }

View File

@@ -1,32 +1,31 @@
import {EventEmitter} from 'events' import { EventEmitter } from "events";
import { import {
constants, classicAddressToXAddress,
errors, xAddressToClassicAddress,
txFlags, isValidXAddress,
} from '../common' isValidClassicAddress,
import { Connection, ConnectionUserOptions } from './connection' encodeSeed,
import getTrustlines from '../ledger/trustlines' decodeSeed,
import getBalances from '../ledger/balances' encodeAccountID,
import getPaths from '../ledger/pathfind' decodeAccountID,
import {getOrderbook, formatBidsAndAsks} from '../ledger/orderbook' encodeNodePublic,
import preparePayment from '../transaction/payment' decodeNodePublic,
import prepareTrustline from '../transaction/trustline' encodeAccountPublic,
import prepareOrder from '../transaction/order' decodeAccountPublic,
import prepareOrderCancellation from '../transaction/ordercancellation' encodeXAddress,
import prepareEscrowCreation from '../transaction/escrow-creation' decodeXAddress,
import prepareEscrowExecution from '../transaction/escrow-execution' } from "ripple-address-codec";
import prepareEscrowCancellation from '../transaction/escrow-cancellation'
import preparePaymentChannelCreate from '../transaction/payment-channel-create' import { constants, errors, txFlags, ensureClassicAddress } from "../common";
import preparePaymentChannelFund from '../transaction/payment-channel-fund' import { ValidationError } from "../common/errors";
import preparePaymentChannelClaim from '../transaction/payment-channel-claim' import { getFee } from "../common/fee";
import prepareCheckCreate from '../transaction/check-create' import * as schemaValidator from "../common/schema-validator";
import prepareCheckCancel from '../transaction/check-cancel' import getBalances from "../ledger/balances";
import prepareCheckCash from '../transaction/check-cash' import { getOrderbook, formatBidsAndAsks } from "../ledger/orderbook";
import prepareSettings from '../transaction/settings' import getPaths from "../ledger/pathfind";
import prepareTicketCreate from '../transaction/ticket' import getTrustlines from "../ledger/trustlines";
import {sign} from '../transaction/sign' import { clamp } from "../ledger/utils";
import combine from '../transaction/combine'
import {deriveAddress, deriveXAddress} from '../utils/derive'
import { import {
Request, Request,
Response, Response,
@@ -94,162 +93,172 @@ import {
PingRequest, PingRequest,
PingResponse, PingResponse,
RandomRequest, RandomRequest,
RandomResponse RandomResponse,
} from '../models/methods' } from "../models/methods";
import prepareCheckCancel from "../transaction/check-cancel";
import prepareCheckCash from "../transaction/check-cash";
import prepareCheckCreate from "../transaction/check-create";
import combine from "../transaction/combine";
import prepareEscrowCancellation from "../transaction/escrow-cancellation";
import prepareEscrowCreation from "../transaction/escrow-creation";
import prepareEscrowExecution from "../transaction/escrow-execution";
import prepareOrder from "../transaction/order";
import prepareOrderCancellation from "../transaction/ordercancellation";
import preparePayment from "../transaction/payment";
import preparePaymentChannelClaim from "../transaction/payment-channel-claim";
import preparePaymentChannelCreate from "../transaction/payment-channel-create";
import preparePaymentChannelFund from "../transaction/payment-channel-fund";
import prepareSettings from "../transaction/settings";
import { sign } from "../transaction/sign";
import prepareTicketCreate from "../transaction/ticket";
import prepareTrustline from "../transaction/trustline";
import { TransactionJSON, Instructions, Prepare } from "../transaction/types";
import * as transactionUtils from "../transaction/utils";
import { deriveAddress, deriveXAddress } from "../utils/derive";
import generateFaucetWallet from "../wallet/wallet-generation";
import * as transactionUtils from '../transaction/utils' import { Connection, ConnectionUserOptions } from "./connection";
import * as schemaValidator from '../common/schema-validator'
import {getFee} from '../common/fee'
import {ensureClassicAddress} from '../common'
import {clamp} from '../ledger/utils'
import {TransactionJSON, Instructions, Prepare} from '../transaction/types'
import {
classicAddressToXAddress,
xAddressToClassicAddress,
isValidXAddress,
isValidClassicAddress,
encodeSeed,
decodeSeed,
encodeAccountID,
decodeAccountID,
encodeNodePublic,
decodeNodePublic,
encodeAccountPublic,
decodeAccountPublic,
encodeXAddress,
decodeXAddress
} from 'ripple-address-codec'
import generateFaucetWallet from '../wallet/wallet-generation'
import { ValidationError } from '../common/errors'
export interface ClientOptions extends ConnectionUserOptions { export interface ClientOptions extends ConnectionUserOptions {
feeCushion?: number feeCushion?: number;
maxFeeXRP?: string maxFeeXRP?: string;
proxy?: string proxy?: string;
timeout?: number timeout?: number;
} }
/** /**
* Get the response key / property name that contains the listed data for a * Get the response key / property name that contains the listed data for a
* command. This varies from command to command, but we need to know it to * command. This varies from command to command, but we need to know it to
* properly count across many requests. * properly count across many requests.
*
* @param command
*/ */
function getCollectKeyFromCommand(command: string): string | null { function getCollectKeyFromCommand(command: string): string | null {
switch (command) { switch (command) {
case 'account_channels': case "account_channels":
return 'channels' return "channels";
case 'account_lines': case "account_lines":
return 'lines' return "lines";
case 'account_objects': case "account_objects":
return 'account_objects' return "account_objects";
case 'account_tx': case "account_tx":
return 'transactions' return "transactions";
case 'account_offers': case "account_offers":
case 'book_offers': case "book_offers":
return 'offers' return "offers";
case 'ledger_data': case "ledger_data":
return 'state' return "state";
default: default:
return null return null;
} }
} }
type MarkerRequest = AccountChannelsRequest type MarkerRequest =
| AccountLinesRequest | AccountChannelsRequest
| AccountObjectsRequest | AccountLinesRequest
| AccountOffersRequest | AccountObjectsRequest
| AccountTxRequest | AccountOffersRequest
| LedgerDataRequest | AccountTxRequest
| LedgerDataRequest;
type MarkerResponse = AccountChannelsResponse type MarkerResponse =
| AccountLinesResponse | AccountChannelsResponse
| AccountObjectsResponse | AccountLinesResponse
| AccountOffersResponse | AccountObjectsResponse
| AccountTxResponse | AccountOffersResponse
| LedgerDataResponse | AccountTxResponse
| LedgerDataResponse;
class Client extends EventEmitter { class Client extends EventEmitter {
// Factor to multiply estimated fee by to provide a cushion in case the // Factor to multiply estimated fee by to provide a cushion in case the
// required fee rises during submission of a transaction. Defaults to 1.2. // required fee rises during submission of a transaction. Defaults to 1.2.
_feeCushion: number _feeCushion: number;
// Maximum fee to use with transactions, in XRP. Must be a string-encoded // Maximum fee to use with transactions, in XRP. Must be a string-encoded
// number. Defaults to '2'. // number. Defaults to '2'.
_maxFeeXRP: string _maxFeeXRP: string;
// New in > 0.21.0 // New in > 0.21.0
// non-validated ledger versions are allowed, and passed to rippled as-is. // non-validated ledger versions are allowed, and passed to rippled as-is.
connection: Connection connection: Connection;
constructor(server: string, options: ClientOptions = {}) { constructor(server: string, options: ClientOptions = {}) {
super() super();
if (typeof server !== 'string' || !server.match("^(wss?|wss?\\+unix)://")) { if (typeof server !== "string" || !server.match("^(wss?|wss?\\+unix)://")) {
throw new ValidationError("server URI must start with `wss://`, `ws://`, `wss+unix://`, or `ws+unix://`.") throw new ValidationError(
"server URI must start with `wss://`, `ws://`, `wss+unix://`, or `ws+unix://`."
);
} }
this._feeCushion = options.feeCushion || 1.2 this._feeCushion = options.feeCushion || 1.2;
this._maxFeeXRP = options.maxFeeXRP || '2' this._maxFeeXRP = options.maxFeeXRP || "2";
this.connection = new Connection(server, options) this.connection = new Connection(server, options);
this.connection.on('error', (errorCode, errorMessage, data) => { this.connection.on("error", (errorCode, errorMessage, data) => {
this.emit('error', errorCode, errorMessage, data) this.emit("error", errorCode, errorMessage, data);
}) });
this.connection.on('connected', () => { this.connection.on("connected", () => {
this.emit('connected') this.emit("connected");
}) });
this.connection.on('disconnected', (code) => { this.connection.on("disconnected", (code) => {
let finalCode = code let finalCode = code;
// 4000: Connection uses a 4000 code internally to indicate a manual disconnect/close // 4000: Connection uses a 4000 code internally to indicate a manual disconnect/close
// Since 4000 is a normal disconnect reason, we convert this to the standard exit code 1000 // Since 4000 is a normal disconnect reason, we convert this to the standard exit code 1000
if (finalCode === 4000) { if (finalCode === 4000) {
finalCode = 1000 finalCode = 1000;
} }
this.emit('disconnected', finalCode) this.emit("disconnected", finalCode);
}) });
} }
/** /**
* Makes a request to the client with the given command and * Makes a request to the client with the given command and
* additional request body parameters. * additional request body parameters.
*/ */
public request(r: AccountChannelsRequest): Promise<AccountChannelsResponse> public request(r: AccountChannelsRequest): Promise<AccountChannelsResponse>;
public request(r: AccountCurrenciesRequest): Promise<AccountCurrenciesResponse> public request(
public request(r: AccountInfoRequest): Promise<AccountInfoResponse> r: AccountCurrenciesRequest
public request(r: AccountLinesRequest): Promise<AccountLinesResponse> ): Promise<AccountCurrenciesResponse>;
public request(r: AccountObjectsRequest): Promise<AccountObjectsResponse> public request(r: AccountInfoRequest): Promise<AccountInfoResponse>;
public request(r: AccountOffersRequest): Promise<AccountOffersResponse> public request(r: AccountLinesRequest): Promise<AccountLinesResponse>;
public request(r: AccountTxRequest): Promise<AccountTxResponse> public request(r: AccountObjectsRequest): Promise<AccountObjectsResponse>;
public request(r: BookOffersRequest): Promise<BookOffersResponse> public request(r: AccountOffersRequest): Promise<AccountOffersResponse>;
public request(r: ChannelVerifyRequest): Promise<ChannelVerifyResponse> public request(r: AccountTxRequest): Promise<AccountTxResponse>;
public request(r: DepositAuthorizedRequest): Promise<DepositAuthorizedResponse> public request(r: BookOffersRequest): Promise<BookOffersResponse>;
public request(r: FeeRequest): Promise<FeeResponse> public request(r: ChannelVerifyRequest): Promise<ChannelVerifyResponse>;
public request(r: GatewayBalancesRequest): Promise<GatewayBalancesResponse> public request(
public request(r: LedgerRequest): Promise<LedgerResponse> r: DepositAuthorizedRequest
public request(r: LedgerClosedRequest): Promise<LedgerClosedResponse> ): Promise<DepositAuthorizedResponse>;
public request(r: LedgerCurrentRequest): Promise<LedgerCurrentResponse> public request(r: FeeRequest): Promise<FeeResponse>;
public request(r: LedgerDataRequest): Promise<LedgerDataResponse> public request(r: GatewayBalancesRequest): Promise<GatewayBalancesResponse>;
public request(r: LedgerEntryRequest): Promise<LedgerEntryResponse> public request(r: LedgerRequest): Promise<LedgerResponse>;
public request(r: ManifestRequest): Promise<ManifestResponse> public request(r: LedgerClosedRequest): Promise<LedgerClosedResponse>;
public request(r: NoRippleCheckRequest): Promise<NoRippleCheckResponse> public request(r: LedgerCurrentRequest): Promise<LedgerCurrentResponse>;
public request(r: PathFindRequest): Promise<PathFindResponse> public request(r: LedgerDataRequest): Promise<LedgerDataResponse>;
public request(r: PingRequest): Promise<PingResponse> public request(r: LedgerEntryRequest): Promise<LedgerEntryResponse>;
public request(r: RandomRequest): Promise<RandomResponse> public request(r: ManifestRequest): Promise<ManifestResponse>;
public request(r: RipplePathFindRequest): Promise<RipplePathFindResponse> public request(r: NoRippleCheckRequest): Promise<NoRippleCheckResponse>;
public request(r: ServerInfoRequest): Promise<ServerInfoResponse> public request(r: PathFindRequest): Promise<PathFindResponse>;
public request(r: ServerStateRequest): Promise<ServerStateResponse> public request(r: PingRequest): Promise<PingResponse>;
public request(r: SubmitRequest): Promise<SubmitResponse> public request(r: RandomRequest): Promise<RandomResponse>;
public request(r: SubmitMultisignedRequest): Promise<SubmitMultisignedResponse> public request(r: RipplePathFindRequest): Promise<RipplePathFindResponse>;
public request(r: TransactionEntryRequest): Promise<TransactionEntryResponse> public request(r: ServerInfoRequest): Promise<ServerInfoResponse>;
public request(r: TxRequest): Promise<TxResponse> public request(r: ServerStateRequest): Promise<ServerStateResponse>;
public request(r: SubmitRequest): Promise<SubmitResponse>;
public request(
r: SubmitMultisignedRequest
): Promise<SubmitMultisignedResponse>;
public request(r: TransactionEntryRequest): Promise<TransactionEntryResponse>;
public request(r: TxRequest): Promise<TxResponse>;
public request<R extends Request, T extends Response>(r: R): Promise<T> { public request<R extends Request, T extends Response>(r: R): Promise<T> {
// TODO: should this be typed with `extends BaseRequest/BaseResponse`? // TODO: should this be typed with `extends BaseRequest/BaseResponse`?
return this.connection.request({ return this.connection.request({
...r, ...r,
// @ts-ignore // @ts-expect-error
account: r.account ? ensureClassicAddress(r.account) : undefined, account: r.account ? ensureClassicAddress(r.account) : undefined,
}) });
} }
/** /**
@@ -258,38 +267,64 @@ class Client extends EventEmitter {
* When there are more results than contained in the response, the response * When there are more results than contained in the response, the response
* includes a `marker` field. * includes a `marker` field.
* *
* See https://ripple.com/build/rippled-apis/#markers-and-pagination * See https://ripple.com/build/rippled-apis/#markers-and-pagination.
*
* @param response
*/ */
hasNextPage(response: MarkerResponse): boolean { hasNextPage(response: MarkerResponse): boolean {
return !!response.result.marker return Boolean(response.result.marker);
} }
async requestNextPage(req: AccountChannelsRequest, resp: AccountChannelsResponse): Promise<AccountChannelsResponse> async requestNextPage(
async requestNextPage(req: AccountLinesRequest, resp: AccountLinesResponse): Promise<AccountLinesResponse> req: AccountChannelsRequest,
async requestNextPage(req: AccountObjectsRequest, resp: AccountObjectsResponse): Promise<AccountObjectsResponse> resp: AccountChannelsResponse
async requestNextPage(req: AccountOffersRequest, resp: AccountOffersResponse): Promise<AccountOffersResponse> ): Promise<AccountChannelsResponse>;
async requestNextPage(req: AccountTxRequest, resp: AccountTxResponse): Promise<AccountTxResponse> async requestNextPage(
async requestNextPage(req: LedgerDataRequest, resp: LedgerDataResponse): Promise<LedgerDataResponse> req: AccountLinesRequest,
async requestNextPage<T extends MarkerRequest, U extends MarkerResponse>(req: T, resp: U): Promise<U> { resp: AccountLinesResponse
): Promise<AccountLinesResponse>;
async requestNextPage(
req: AccountObjectsRequest,
resp: AccountObjectsResponse
): Promise<AccountObjectsResponse>;
async requestNextPage(
req: AccountOffersRequest,
resp: AccountOffersResponse
): Promise<AccountOffersResponse>;
async requestNextPage(
req: AccountTxRequest,
resp: AccountTxResponse
): Promise<AccountTxResponse>;
async requestNextPage(
req: LedgerDataRequest,
resp: LedgerDataResponse
): Promise<LedgerDataResponse>;
async requestNextPage<T extends MarkerRequest, U extends MarkerResponse>(
req: T,
resp: U
): Promise<U> {
if (!resp.result.marker) { if (!resp.result.marker) {
return Promise.reject( return Promise.reject(
new errors.NotFoundError('response does not have a next page') new errors.NotFoundError("response does not have a next page")
) );
} }
const nextPageRequest = {...req, marker: resp.result.marker} const nextPageRequest = { ...req, marker: resp.result.marker };
return this.connection.request(nextPageRequest) return this.connection.request(nextPageRequest);
} }
/** /**
* Prepare a transaction. * Prepare a transaction.
* *
* You can later submit the transaction with a `submit` request. * You can later submit the transaction with a `submit` request.
*
* @param txJSON
* @param instructions
*/ */
async prepareTransaction( async prepareTransaction(
txJSON: TransactionJSON, txJSON: TransactionJSON,
instructions: Instructions = {} instructions: Instructions = {}
): Promise<Prepare> { ): Promise<Prepare> {
return transactionUtils.prepareTransaction(txJSON, this, instructions) return transactionUtils.prepareTransaction(txJSON, this, instructions);
} }
/** /**
@@ -297,15 +332,15 @@ class Client extends EventEmitter {
* *
* This can be used to generate `MemoData`, `MemoType`, and `MemoFormat`. * This can be used to generate `MemoData`, `MemoType`, and `MemoFormat`.
* *
* @param string string to convert to hex * @param string - String to convert to hex.
*/ */
convertStringToHex(string: string): string { convertStringToHex(string: string): string {
return transactionUtils.convertStringToHex(string) return transactionUtils.convertStringToHex(string);
} }
/** /**
* Makes multiple paged requests to the client to return a given number of * Makes multiple paged requests to the client to return a given number of
* resources. requestAll() will make multiple requests until the `limit` * resources. Multiple paged requests will be made until the `limit`
* number of resources is reached (if no `limit` is provided, a single request * number of resources is reached (if no `limit` is provided, a single request
* will be made). * will be made).
* *
@@ -316,129 +351,136 @@ class Client extends EventEmitter {
* general use. Instead, use rippled's built-in pagination and make multiple * general use. Instead, use rippled's built-in pagination and make multiple
* requests as needed. * requests as needed.
*/ */
async requestAll(req: AccountChannelsRequest): Promise<AccountChannelsResponse[]> async requestAll(
async requestAll(req: AccountLinesRequest): Promise<AccountLinesResponse[]> req: AccountChannelsRequest
async requestAll(req: AccountObjectsRequest): Promise<AccountObjectsResponse[]> ): Promise<AccountChannelsResponse[]>;
async requestAll(req: AccountOffersRequest): Promise<AccountOffersResponse[]> async requestAll(req: AccountLinesRequest): Promise<AccountLinesResponse[]>;
async requestAll(req: AccountTxRequest): Promise<AccountTxResponse[]> async requestAll(
async requestAll(req: BookOffersRequest): Promise<BookOffersResponse[]> req: AccountObjectsRequest
async requestAll(req: LedgerDataRequest): Promise<LedgerDataResponse[]> ): Promise<AccountObjectsResponse[]>;
async requestAll<T extends MarkerRequest, U extends MarkerResponse>(request: T, options: {collect?: string} = {}): Promise<U[]> { async requestAll(req: AccountOffersRequest): Promise<AccountOffersResponse[]>;
async requestAll(req: AccountTxRequest): Promise<AccountTxResponse[]>;
async requestAll(req: BookOffersRequest): Promise<BookOffersResponse[]>;
async requestAll(req: LedgerDataRequest): Promise<LedgerDataResponse[]>;
async requestAll<T extends MarkerRequest, U extends MarkerResponse>(
request: T,
options: { collect?: string } = {}
): Promise<U[]> {
// The data under collection is keyed based on the command. Fail if command // The data under collection is keyed based on the command. Fail if command
// not recognized and collection key not provided. // not recognized and collection key not provided.
const collectKey = options.collect || getCollectKeyFromCommand(request.command) const collectKey =
options.collect || getCollectKeyFromCommand(request.command);
if (!collectKey) { if (!collectKey) {
throw new errors.ValidationError(`no collect key for command ${request.command}`) throw new errors.ValidationError(
`no collect key for command ${request.command}`
);
} }
// If limit is not provided, fetches all data over multiple requests. // If limit is not provided, fetches all data over multiple requests.
// NOTE: This may return much more than needed. Set limit when possible. // NOTE: This may return much more than needed. Set limit when possible.
const countTo: number = request.limit != null ? request.limit : Infinity const countTo: number = request.limit != null ? request.limit : Infinity;
let count: number = 0 let count = 0;
let marker: string = request.marker let marker: string = request.marker;
let lastBatchLength: number let lastBatchLength: number;
const results = [] const results: any[] = [];
do { do {
const countRemaining = clamp(countTo - count, 10, 400) const countRemaining = clamp(countTo - count, 10, 400);
const repeatProps = { const repeatProps = {
...request, ...request,
limit: countRemaining, limit: countRemaining,
marker marker,
} };
const singleResponse = await this.connection.request(repeatProps) const singleResponse = await this.connection.request(repeatProps);
const singleResult = singleResponse.result const singleResult = singleResponse.result;
const collectedData = singleResult[collectKey] const collectedData = singleResult[collectKey];
marker = singleResult['marker'] marker = singleResult.marker;
results.push(singleResponse) results.push(singleResponse);
// Make sure we handle when no data (not even an empty array) is returned. // Make sure we handle when no data (not even an empty array) is returned.
const isExpectedFormat = Array.isArray(collectedData) const isExpectedFormat = Array.isArray(collectedData);
if (isExpectedFormat) { if (isExpectedFormat) {
count += collectedData.length count += collectedData.length;
lastBatchLength = collectedData.length lastBatchLength = collectedData.length;
} else { } else {
lastBatchLength = 0 lastBatchLength = 0;
} }
} while (!!marker && count < countTo && lastBatchLength !== 0) } while (Boolean(marker) && count < countTo && lastBatchLength !== 0);
return results return results;
} }
isConnected(): boolean { isConnected(): boolean {
return this.connection.isConnected() return this.connection.isConnected();
} }
async connect(): Promise<void> { async connect(): Promise<void> {
return this.connection.connect() return this.connection.connect();
} }
async disconnect(): Promise<void> { async disconnect(): Promise<void> {
// backwards compatibility: connection.disconnect() can return a number, but // backwards compatibility: connection.disconnect() can return a number, but
// this method returns nothing. SO we await but don't return any result. // this method returns nothing. SO we await but don't return any result.
await this.connection.disconnect() await this.connection.disconnect();
} }
getFee = getFee getFee = getFee;
getTrustlines = getTrustlines getTrustlines = getTrustlines;
getBalances = getBalances getBalances = getBalances;
getPaths = getPaths getPaths = getPaths;
getOrderbook = getOrderbook getOrderbook = getOrderbook;
preparePayment = preparePayment preparePayment = preparePayment;
prepareTrustline = prepareTrustline prepareTrustline = prepareTrustline;
prepareOrder = prepareOrder prepareOrder = prepareOrder;
prepareOrderCancellation = prepareOrderCancellation prepareOrderCancellation = prepareOrderCancellation;
prepareEscrowCreation = prepareEscrowCreation prepareEscrowCreation = prepareEscrowCreation;
prepareEscrowExecution = prepareEscrowExecution prepareEscrowExecution = prepareEscrowExecution;
prepareEscrowCancellation = prepareEscrowCancellation prepareEscrowCancellation = prepareEscrowCancellation;
preparePaymentChannelCreate = preparePaymentChannelCreate preparePaymentChannelCreate = preparePaymentChannelCreate;
preparePaymentChannelFund = preparePaymentChannelFund preparePaymentChannelFund = preparePaymentChannelFund;
preparePaymentChannelClaim = preparePaymentChannelClaim preparePaymentChannelClaim = preparePaymentChannelClaim;
prepareCheckCreate = prepareCheckCreate prepareCheckCreate = prepareCheckCreate;
prepareCheckCash = prepareCheckCash prepareCheckCash = prepareCheckCash;
prepareCheckCancel = prepareCheckCancel prepareCheckCancel = prepareCheckCancel;
prepareTicketCreate = prepareTicketCreate prepareTicketCreate = prepareTicketCreate;
prepareSettings = prepareSettings prepareSettings = prepareSettings;
sign = sign sign = sign;
combine = combine combine = combine;
generateFaucetWallet = generateFaucetWallet generateFaucetWallet = generateFaucetWallet;
errors = errors errors = errors;
static deriveXAddress = deriveXAddress static deriveXAddress = deriveXAddress;
// Client.deriveClassicAddress (static) is a new name for client.deriveAddress // Client.deriveClassicAddress (static) is a new name for client.deriveAddress
static deriveClassicAddress = deriveAddress static deriveClassicAddress = deriveAddress;
static formatBidsAndAsks = formatBidsAndAsks static formatBidsAndAsks = formatBidsAndAsks;
/** /**
* Static methods to expose ripple-address-codec methods * Static methods to expose ripple-address-codec methods.
*/ */
static classicAddressToXAddress = classicAddressToXAddress static classicAddressToXAddress = classicAddressToXAddress;
static xAddressToClassicAddress = xAddressToClassicAddress static xAddressToClassicAddress = xAddressToClassicAddress;
static isValidXAddress = isValidXAddress static isValidXAddress = isValidXAddress;
static isValidClassicAddress = isValidClassicAddress static isValidClassicAddress = isValidClassicAddress;
static encodeSeed = encodeSeed static encodeSeed = encodeSeed;
static decodeSeed = decodeSeed static decodeSeed = decodeSeed;
static encodeAccountID = encodeAccountID static encodeAccountID = encodeAccountID;
static decodeAccountID = decodeAccountID static decodeAccountID = decodeAccountID;
static encodeNodePublic = encodeNodePublic static encodeNodePublic = encodeNodePublic;
static decodeNodePublic = decodeNodePublic static decodeNodePublic = decodeNodePublic;
static encodeAccountPublic = encodeAccountPublic static encodeAccountPublic = encodeAccountPublic;
static decodeAccountPublic = decodeAccountPublic static decodeAccountPublic = decodeAccountPublic;
static encodeXAddress = encodeXAddress static encodeXAddress = encodeXAddress;
static decodeXAddress = decodeXAddress static decodeXAddress = decodeXAddress;
txFlags = txFlags txFlags = txFlags;
static txFlags = txFlags static txFlags = txFlags;
accountSetFlags = constants.AccountSetFlags accountSetFlags = constants.AccountSetFlags;
static accountSetFlags = constants.AccountSetFlags static accountSetFlags = constants.AccountSetFlags;
isValidAddress = schemaValidator.isValidAddress isValidAddress = schemaValidator.isValidAddress;
isValidSecret = schemaValidator.isValidSecret isValidSecret = schemaValidator.isValidSecret;
} }
export { export { Client, Connection };
Client,
Connection
}

View File

@@ -1,63 +1,64 @@
import * as _ from 'lodash' import * as assert from "assert";
import * as assert from 'assert'
type Interval = [number, number] import * as _ from "lodash";
type Interval = [number, number];
function mergeIntervals(intervals: Interval[]): Interval[] { function mergeIntervals(intervals: Interval[]): Interval[] {
const stack: Interval[] = [[-Infinity, -Infinity]] const stack: Interval[] = [[-Infinity, -Infinity]];
_.sortBy(intervals, (x) => x[0]).forEach((interval) => { _.sortBy(intervals, (x) => x[0]).forEach((interval) => {
const lastInterval: Interval = stack.pop()! const lastInterval: Interval = stack.pop()!;
if (interval[0] <= lastInterval[1] + 1) { if (interval[0] <= lastInterval[1] + 1) {
stack.push([lastInterval[0], Math.max(interval[1], lastInterval[1])]) stack.push([lastInterval[0], Math.max(interval[1], lastInterval[1])]);
} else { } else {
stack.push(lastInterval) stack.push(lastInterval);
stack.push(interval) stack.push(interval);
} }
}) });
return stack.slice(1) return stack.slice(1);
} }
class RangeSet { class RangeSet {
ranges: Array<[number, number]> ranges: Array<[number, number]> = [];
constructor() { constructor() {
this.reset() this.reset();
} }
reset() { reset() {
this.ranges = [] this.ranges = [];
} }
serialize() { serialize() {
return this.ranges return this.ranges
.map((range) => range[0].toString() + '-' + range[1].toString()) .map((range) => `${range[0].toString()}-${range[1].toString()}`)
.join(',') .join(",");
} }
addRange(start: number, end: number) { addRange(start: number, end: number) {
assert.ok(start <= end, `invalid range ${start} <= ${end}`) assert.ok(start <= end, `invalid range ${start} <= ${end}`);
this.ranges = mergeIntervals(this.ranges.concat([[start, end]])) this.ranges = mergeIntervals(this.ranges.concat([[start, end]]));
} }
addValue(value: number) { addValue(value: number) {
this.addRange(value, value) this.addRange(value, value);
} }
parseAndAddRanges(rangesString: string) { parseAndAddRanges(rangesString: string) {
const rangeStrings = rangesString.split(',') const rangeStrings = rangesString.split(",");
rangeStrings.forEach((rangeString) => { rangeStrings.forEach((rangeString) => {
const range = rangeString.split('-').map(Number) const range = rangeString.split("-").map(Number);
this.addRange(range[0], range.length === 1 ? range[0] : range[1]) this.addRange(range[0], range.length === 1 ? range[0] : range[1]);
}) });
} }
containsRange(start: number, end: number) { containsRange(start: number, end: number) {
return this.ranges.some((range) => range[0] <= start && range[1] >= end) return this.ranges.some((range) => range[0] <= start && range[1] >= end);
} }
containsValue(value: number) { containsValue(value: number) {
return this.containsRange(value, value) return this.containsRange(value, value);
} }
} }
export default RangeSet export default RangeSet;

View File

@@ -1,15 +1,15 @@
import {EventEmitter} from 'events' import { EventEmitter } from "events";
// Define the global WebSocket class found on the native browser // Define the global WebSocket class found on the native browser
declare class WebSocket { declare class WebSocket {
onclose?: Function onclose?: Function;
onopen?: Function onopen?: Function;
onerror?: Function onerror?: Function;
onmessage?: Function onmessage?: Function;
readyState: number readyState: number;
constructor(url: string) constructor(url: string);
close() close();
send(message: string) send(message: string);
} }
/** /**
@@ -17,46 +17,46 @@ declare class WebSocket {
* same, as `ws` package provides. * same, as `ws` package provides.
*/ */
export default class WSWrapper extends EventEmitter { export default class WSWrapper extends EventEmitter {
private _ws: WebSocket private readonly _ws: WebSocket;
static CONNECTING = 0 static CONNECTING = 0;
static OPEN = 1 static OPEN = 1;
static CLOSING = 2 static CLOSING = 2;
static CLOSED = 3 static CLOSED = 3;
constructor(url, _protocols: any, _websocketOptions: any) { constructor(url, _protocols: any, _websocketOptions: any) {
super() super();
this.setMaxListeners(Infinity) this.setMaxListeners(Infinity);
this._ws = new WebSocket(url) this._ws = new WebSocket(url);
this._ws.onclose = () => { this._ws.onclose = () => {
this.emit('close') this.emit("close");
} };
this._ws.onopen = () => { this._ws.onopen = () => {
this.emit('open') this.emit("open");
} };
this._ws.onerror = (error) => { this._ws.onerror = (error) => {
this.emit('error', error) this.emit("error", error);
} };
this._ws.onmessage = (message) => { this._ws.onmessage = (message) => {
this.emit('message', message.data) this.emit("message", message.data);
} };
} }
close() { close() {
if (this.readyState === 1) { if (this.readyState === 1) {
this._ws.close() this._ws.close();
} }
} }
send(message) { send(message) {
this._ws.send(message) this._ws.send(message);
} }
get readyState() { get readyState() {
return this._ws.readyState return this._ws.readyState;
} }
} }

View File

@@ -1,4 +1,4 @@
import {txFlagIndices} from './txflags' import { txFlagIndices } from "./txflags";
// Ordering from https://developers.ripple.com/accountroot.html // Ordering from https://developers.ripple.com/accountroot.html
const accountRootFlags = { const accountRootFlags = {
@@ -43,8 +43,8 @@ const accountRootFlags = {
// lsfRequireDestTag: // lsfRequireDestTag:
// Require a DestinationTag for incoming payments. // Require a DestinationTag for incoming payments.
RequireDestTag: 0x00020000 RequireDestTag: 0x00020000,
} };
const AccountFlags = { const AccountFlags = {
passwordSpent: accountRootFlags.PasswordSpent, passwordSpent: accountRootFlags.PasswordSpent,
@@ -55,19 +55,19 @@ const AccountFlags = {
disableMasterKey: accountRootFlags.DisableMaster, disableMasterKey: accountRootFlags.DisableMaster,
noFreeze: accountRootFlags.NoFreeze, noFreeze: accountRootFlags.NoFreeze,
globalFreeze: accountRootFlags.GlobalFreeze, globalFreeze: accountRootFlags.GlobalFreeze,
defaultRipple: accountRootFlags.DefaultRipple defaultRipple: accountRootFlags.DefaultRipple,
} };
export interface Settings { export interface Settings {
passwordSpent?: boolean passwordSpent?: boolean;
requireDestinationTag?: boolean requireDestinationTag?: boolean;
requireAuthorization?: boolean requireAuthorization?: boolean;
depositAuth?: boolean depositAuth?: boolean;
disallowIncomingXRP?: boolean disallowIncomingXRP?: boolean;
disableMasterKey?: boolean disableMasterKey?: boolean;
noFreeze?: boolean noFreeze?: boolean;
globalFreeze?: boolean globalFreeze?: boolean;
defaultRipple?: boolean defaultRipple?: boolean;
} }
const AccountSetFlags = { const AccountSetFlags = {
@@ -79,21 +79,21 @@ const AccountSetFlags = {
enableTransactionIDTracking: txFlagIndices.AccountSet.asfAccountTxnID, enableTransactionIDTracking: txFlagIndices.AccountSet.asfAccountTxnID,
noFreeze: txFlagIndices.AccountSet.asfNoFreeze, noFreeze: txFlagIndices.AccountSet.asfNoFreeze,
globalFreeze: txFlagIndices.AccountSet.asfGlobalFreeze, globalFreeze: txFlagIndices.AccountSet.asfGlobalFreeze,
defaultRipple: txFlagIndices.AccountSet.asfDefaultRipple defaultRipple: txFlagIndices.AccountSet.asfDefaultRipple,
} };
const AccountFields = { const AccountFields = {
EmailHash: { EmailHash: {
name: 'emailHash', name: "emailHash",
encoding: 'hex', encoding: "hex",
length: 32, length: 32,
defaults: '00000000000000000000000000000000' defaults: "00000000000000000000000000000000",
}, },
WalletLocator: {name: 'walletLocator'}, WalletLocator: { name: "walletLocator" },
MessageKey: {name: 'messageKey'}, MessageKey: { name: "messageKey" },
Domain: {name: 'domain', encoding: 'hex'}, Domain: { name: "domain", encoding: "hex" },
TransferRate: {name: 'transferRate', defaults: 0, shift: 9}, TransferRate: { name: "transferRate", defaults: 0, shift: 9 },
TickSize: {name: 'tickSize', defaults: 0} TickSize: { name: "tickSize", defaults: 0 },
} };
export {AccountFields, AccountSetFlags, AccountFlags} export { AccountFields, AccountSetFlags, AccountFlags };

View File

@@ -1,6 +1,6 @@
enum ECDSA { enum ECDSA {
ed25519 = 'ed25519', ed25519 = "ed25519",
secp256k1 = 'ecdsa-secp256k1', secp256k1 = "ecdsa-secp256k1",
} }
export default ECDSA export default ECDSA;

View File

@@ -1,35 +1,35 @@
import {inspect} from 'util' import { inspect } from "util";
class RippleError extends Error { class RippleError extends Error {
name: string name: string;
message: string message: string;
data?: any data?: any;
constructor(message = '', data?: any) { constructor(message = "", data?: any) {
super(message) super(message);
this.name = this.constructor.name this.name = this.constructor.name;
this.message = message this.message = message;
this.data = data this.data = data;
if (Error.captureStackTrace) { if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor) Error.captureStackTrace(this, this.constructor);
} }
} }
toString() { toString() {
let result = '[' + this.name + '(' + this.message let result = `[${this.name}(${this.message}`;
if (this.data) { if (this.data) {
result += ', ' + inspect(this.data) result += `, ${inspect(this.data)}`;
} }
result += ')]' result += ")]";
return result return result;
} }
/* console.log in node uses util.inspect on object, and util.inspect allows // console.log in node uses util.inspect on object, and util.inspect allows
us to customize its output: // us to customize its output:
https://nodejs.org/api/util.html#util_custom_inspect_function_on_objects */ // https://nodejs.org/api/util.html#util_custom_inspect_function_on_objects
inspect() { inspect() {
return this.toString() return this.toString();
} }
} }
@@ -56,14 +56,14 @@ class ValidationError extends RippleError {}
class XRPLFaucetError extends RippleError {} class XRPLFaucetError extends RippleError {}
class NotFoundError extends RippleError { class NotFoundError extends RippleError {
constructor(message = 'Not found') { constructor(message = "Not found") {
super(message) super(message);
} }
} }
class MissingLedgerHistoryError extends RippleError { class MissingLedgerHistoryError extends RippleError {
constructor(message?: string) { constructor(message?: string) {
super(message || 'Server is missing ledger history in the specified range') super(message || "Server is missing ledger history in the specified range");
} }
} }
@@ -72,8 +72,8 @@ class PendingLedgerVersionError extends RippleError {
super( super(
message || message ||
"maxLedgerVersion is greater than server's most recent" + "maxLedgerVersion is greater than server's most recent" +
' validated ledger' " validated ledger"
) );
} }
} }
@@ -92,5 +92,5 @@ export {
PendingLedgerVersionError, PendingLedgerVersionError,
MissingLedgerHistoryError, MissingLedgerHistoryError,
LedgerVersionError, LedgerVersionError,
XRPLFaucetError XRPLFaucetError,
} };

View File

@@ -1,29 +1,38 @@
import _ from 'lodash' import BigNumber from "bignumber.js";
import BigNumber from 'bignumber.js' import _ from "lodash";
import {Client} from '..'
import { Client } from "..";
// This is a public API that can be called directly. // This is a public API that can be called directly.
// This is not used by the `prepare*` methods. See `src/transaction/utils.ts` // This is not used by the `prepare*` methods. See `src/transaction/utils.ts`
async function getFee(this: Client, cushion?: number): Promise<string> { async function getFee(this: Client, cushion?: number): Promise<string> {
if (cushion == null) { if (cushion == null) {
cushion = this._feeCushion cushion = this._feeCushion;
} }
if (cushion == null) { if (cushion == null) {
cushion = 1.2 cushion = 1.2;
} }
const serverInfo = (await this.request({command: "server_info"})).result.info const serverInfo = (await this.request({ command: "server_info" })).result
const baseFeeXrp = new BigNumber(serverInfo.validated_ledger.base_fee_xrp) .info;
const baseFee = serverInfo.validated_ledger?.base_fee_xrp;
if (baseFee == null) {
throw new Error("getFee: Could not get base_fee_xrp from server_info");
}
const baseFeeXrp = new BigNumber(baseFee);
if (serverInfo.load_factor == null) { if (serverInfo.load_factor == null) {
// https://github.com/ripple/rippled/issues/3812#issuecomment-816871100 // https://github.com/ripple/rippled/issues/3812#issuecomment-816871100
serverInfo.load_factor = 1 serverInfo.load_factor = 1;
} }
let fee = baseFeeXrp.times(serverInfo.load_factor).times(cushion) let fee = baseFeeXrp.times(serverInfo.load_factor).times(cushion);
// Cap fee to `this._maxFeeXRP` // Cap fee to `this._maxFeeXRP`
fee = BigNumber.min(fee, this._maxFeeXRP) fee = BigNumber.min(fee, this._maxFeeXRP);
// Round fee to 6 decimal places // Round fee to 6 decimal places
return new BigNumber(fee.toFixed(6)).toString(10) return new BigNumber(fee.toFixed(6)).toString(10);
} }
export {getFee} export { getFee };

View File

@@ -1,27 +1,30 @@
import * as constants from './constants' import {
import * as errors from './errors' xAddressToClassicAddress,
import * as validate from './validate' isValidXAddress,
import {xAddressToClassicAddress, isValidXAddress} from 'ripple-address-codec' } from "ripple-address-codec";
import * as constants from "./constants";
import * as errors from "./errors";
import * as validate from "./validate";
export function ensureClassicAddress(account: string): string { export function ensureClassicAddress(account: string): string {
if (isValidXAddress(account)) { if (isValidXAddress(account)) {
const {classicAddress, tag} = xAddressToClassicAddress(account) const { classicAddress, tag } = xAddressToClassicAddress(account);
// Except for special cases, X-addresses used for requests // Except for special cases, X-addresses used for requests
// must not have an embedded tag. In other words, // must not have an embedded tag. In other words,
// `tag` should be `false`. // `tag` should be `false`.
if (tag !== false) { if (tag !== false) {
throw new Error( throw new Error(
'This command does not support the use of a tag. Use an address without a tag.' "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. // For rippled requests that use an account, always use a classic address.
return classicAddress return classicAddress;
} else {
return account
} }
return account;
} }
export {constants, errors, validate} export { constants, errors, validate };
export {txFlags} from './txflags' export { txFlags } from "./txflags";

View File

@@ -1,134 +1,140 @@
import _ from 'lodash' import * as assert from "assert";
import * as assert from 'assert'
const {Validator} = require('jsonschema') import _ from "lodash";
import {ValidationError} from './errors' import { isValidClassicAddress, isValidXAddress } from "ripple-address-codec";
import {isValidClassicAddress, isValidXAddress} from 'ripple-address-codec'
import {isValidSecret} from '../utils' import { isValidSecret } from "../utils";
import { ValidationError } from "./errors";
const { Validator } = require("jsonschema");
function loadSchemas() { function loadSchemas() {
// listed explicitly for webpack (instead of scanning schemas directory) // listed explicitly for webpack (instead of scanning schemas directory)
const schemas = [ const schemas = [
require('./schemas/objects/tx-json.json'), require("./schemas/objects/tx-json.json"),
require('./schemas/objects/transaction-type.json'), require("./schemas/objects/transaction-type.json"),
require('./schemas/objects/hash128.json'), require("./schemas/objects/hash128.json"),
require('./schemas/objects/hash256.json'), require("./schemas/objects/hash256.json"),
require('./schemas/objects/sequence.json'), require("./schemas/objects/sequence.json"),
require('./schemas/objects/ticket-sequence.json'), require("./schemas/objects/ticket-sequence.json"),
require('./schemas/objects/signature.json'), require("./schemas/objects/signature.json"),
require('./schemas/objects/issue.json'), require("./schemas/objects/issue.json"),
require('./schemas/objects/ledger-version.json'), require("./schemas/objects/ledger-version.json"),
require('./schemas/objects/max-adjustment.json'), require("./schemas/objects/max-adjustment.json"),
require('./schemas/objects/memo.json'), require("./schemas/objects/memo.json"),
require('./schemas/objects/memos.json'), require("./schemas/objects/memos.json"),
require('./schemas/objects/public-key.json'), require("./schemas/objects/public-key.json"),
require('./schemas/objects/private-key.json'), require("./schemas/objects/private-key.json"),
require('./schemas/objects/uint32.json'), require("./schemas/objects/uint32.json"),
require('./schemas/objects/value.json'), require("./schemas/objects/value.json"),
require('./schemas/objects/source-adjustment.json'), require("./schemas/objects/source-adjustment.json"),
require('./schemas/objects/destination-adjustment.json'), require("./schemas/objects/destination-adjustment.json"),
require('./schemas/objects/tag.json'), require("./schemas/objects/tag.json"),
require('./schemas/objects/lax-amount.json'), require("./schemas/objects/lax-amount.json"),
require('./schemas/objects/lax-lax-amount.json'), require("./schemas/objects/lax-lax-amount.json"),
require('./schemas/objects/min-adjustment.json'), require("./schemas/objects/min-adjustment.json"),
require('./schemas/objects/source-exact-adjustment.json'), require("./schemas/objects/source-exact-adjustment.json"),
require('./schemas/objects/destination-exact-adjustment.json'), require("./schemas/objects/destination-exact-adjustment.json"),
require('./schemas/objects/destination-address-tag.json'), require("./schemas/objects/destination-address-tag.json"),
require('./schemas/objects/transaction-hash.json'), require("./schemas/objects/transaction-hash.json"),
require('./schemas/objects/address.json'), require("./schemas/objects/address.json"),
require('./schemas/objects/x-address.json'), require("./schemas/objects/x-address.json"),
require('./schemas/objects/classic-address.json'), require("./schemas/objects/classic-address.json"),
require('./schemas/objects/adjustment.json'), require("./schemas/objects/adjustment.json"),
require('./schemas/objects/quality.json'), require("./schemas/objects/quality.json"),
require('./schemas/objects/amount.json'), require("./schemas/objects/amount.json"),
require('./schemas/objects/amountbase.json'), require("./schemas/objects/amountbase.json"),
require('./schemas/objects/balance.json'), require("./schemas/objects/balance.json"),
require('./schemas/objects/blob.json'), require("./schemas/objects/blob.json"),
require('./schemas/objects/currency.json'), require("./schemas/objects/currency.json"),
require('./schemas/objects/signed-value.json'), require("./schemas/objects/signed-value.json"),
require('./schemas/objects/orderbook.json'), require("./schemas/objects/orderbook.json"),
require('./schemas/objects/instructions.json'), require("./schemas/objects/instructions.json"),
require('./schemas/objects/settings-plus-memos.json'), require("./schemas/objects/settings-plus-memos.json"),
require('./schemas/specifications/settings.json'), require("./schemas/specifications/settings.json"),
require('./schemas/specifications/payment.json'), require("./schemas/specifications/payment.json"),
require('./schemas/specifications/get-payment.json'), require("./schemas/specifications/get-payment.json"),
require('./schemas/specifications/escrow-cancellation.json'), require("./schemas/specifications/escrow-cancellation.json"),
require('./schemas/specifications/order-cancellation.json'), require("./schemas/specifications/order-cancellation.json"),
require('./schemas/specifications/order.json'), require("./schemas/specifications/order.json"),
require('./schemas/specifications/escrow-execution.json'), require("./schemas/specifications/escrow-execution.json"),
require('./schemas/specifications/escrow-creation.json'), require("./schemas/specifications/escrow-creation.json"),
require('./schemas/specifications/payment-channel-create.json'), require("./schemas/specifications/payment-channel-create.json"),
require('./schemas/specifications/payment-channel-fund.json'), require("./schemas/specifications/payment-channel-fund.json"),
require('./schemas/specifications/payment-channel-claim.json'), require("./schemas/specifications/payment-channel-claim.json"),
require('./schemas/specifications/check-create.json'), require("./schemas/specifications/check-create.json"),
require('./schemas/specifications/check-cash.json'), require("./schemas/specifications/check-cash.json"),
require('./schemas/specifications/check-cancel.json'), require("./schemas/specifications/check-cancel.json"),
require('./schemas/specifications/trustline.json'), require("./schemas/specifications/trustline.json"),
require('./schemas/specifications/deposit-preauth.json'), require("./schemas/specifications/deposit-preauth.json"),
require('./schemas/specifications/account-delete.json'), require("./schemas/specifications/account-delete.json"),
require('./schemas/output/sign.json'), require("./schemas/output/sign.json"),
require('./schemas/output/submit.json'), require("./schemas/output/submit.json"),
require('./schemas/output/get-account-info.json'), require("./schemas/output/get-account-info.json"),
require('./schemas/output/get-account-objects.json'), require("./schemas/output/get-account-objects.json"),
require('./schemas/output/get-balances.json'), require("./schemas/output/get-balances.json"),
require('./schemas/output/get-balance-sheet.json'), require("./schemas/output/get-balance-sheet.json"),
require('./schemas/output/get-ledger.json'), require("./schemas/output/get-ledger.json"),
require('./schemas/output/get-orderbook.json'), require("./schemas/output/get-orderbook.json"),
require('./schemas/output/get-orders.json'), require("./schemas/output/get-orders.json"),
require('./schemas/output/order-change.json'), require("./schemas/output/order-change.json"),
require('./schemas/output/get-payment-channel.json'), require("./schemas/output/get-payment-channel.json"),
require('./schemas/output/prepare.json'), require("./schemas/output/prepare.json"),
require('./schemas/output/ledger-event.json'), require("./schemas/output/ledger-event.json"),
require('./schemas/output/get-paths.json'), require("./schemas/output/get-paths.json"),
require('./schemas/output/get-server-info.json'), require("./schemas/output/get-server-info.json"),
require('./schemas/output/get-settings.json'), require("./schemas/output/get-settings.json"),
require('./schemas/output/orderbook-orders.json'), require("./schemas/output/orderbook-orders.json"),
require('./schemas/output/outcome.json'), require("./schemas/output/outcome.json"),
require('./schemas/output/get-transaction.json'), require("./schemas/output/get-transaction.json"),
require('./schemas/output/get-transactions.json'), require("./schemas/output/get-transactions.json"),
require('./schemas/output/get-trustlines.json'), require("./schemas/output/get-trustlines.json"),
require('./schemas/output/sign-payment-channel-claim.json'), require("./schemas/output/sign-payment-channel-claim.json"),
require('./schemas/output/verify-payment-channel-claim.json'), require("./schemas/output/verify-payment-channel-claim.json"),
require('./schemas/input/get-balances.json'), require("./schemas/input/get-balances.json"),
require('./schemas/input/get-balance-sheet.json'), require("./schemas/input/get-balance-sheet.json"),
require('./schemas/input/get-ledger.json'), require("./schemas/input/get-ledger.json"),
require('./schemas/input/get-orders.json'), require("./schemas/input/get-orders.json"),
require('./schemas/input/get-orderbook.json'), require("./schemas/input/get-orderbook.json"),
require('./schemas/input/get-paths.json'), require("./schemas/input/get-paths.json"),
require('./schemas/input/get-payment-channel.json'), require("./schemas/input/get-payment-channel.json"),
require('./schemas/input/api-options.json'), require("./schemas/input/api-options.json"),
require('./schemas/input/get-settings.json'), require("./schemas/input/get-settings.json"),
require('./schemas/input/get-account-info.json'), require("./schemas/input/get-account-info.json"),
require('./schemas/input/get-account-objects.json'), require("./schemas/input/get-account-objects.json"),
require('./schemas/input/get-transaction.json'), require("./schemas/input/get-transaction.json"),
require('./schemas/input/get-transactions.json'), require("./schemas/input/get-transactions.json"),
require('./schemas/input/get-trustlines.json'), require("./schemas/input/get-trustlines.json"),
require('./schemas/input/prepare-payment.json'), require("./schemas/input/prepare-payment.json"),
require('./schemas/input/prepare-order.json'), require("./schemas/input/prepare-order.json"),
require('./schemas/input/prepare-trustline.json'), require("./schemas/input/prepare-trustline.json"),
require('./schemas/input/prepare-order-cancellation.json'), require("./schemas/input/prepare-order-cancellation.json"),
require('./schemas/input/prepare-settings.json'), require("./schemas/input/prepare-settings.json"),
require('./schemas/input/prepare-escrow-creation.json'), require("./schemas/input/prepare-escrow-creation.json"),
require('./schemas/input/prepare-escrow-cancellation.json'), require("./schemas/input/prepare-escrow-cancellation.json"),
require('./schemas/input/prepare-escrow-execution.json'), require("./schemas/input/prepare-escrow-execution.json"),
require('./schemas/input/prepare-payment-channel-create.json'), require("./schemas/input/prepare-payment-channel-create.json"),
require('./schemas/input/prepare-payment-channel-fund.json'), require("./schemas/input/prepare-payment-channel-fund.json"),
require('./schemas/input/prepare-payment-channel-claim.json'), require("./schemas/input/prepare-payment-channel-claim.json"),
require('./schemas/input/prepare-check-create.json'), require("./schemas/input/prepare-check-create.json"),
require('./schemas/input/prepare-check-cash.json'), require("./schemas/input/prepare-check-cash.json"),
require('./schemas/input/prepare-check-cancel.json'), require("./schemas/input/prepare-check-cancel.json"),
require('./schemas/input/prepare-ticket-create.json'), require("./schemas/input/prepare-ticket-create.json"),
require('./schemas/input/compute-ledger-hash.json'), require("./schemas/input/compute-ledger-hash.json"),
require('./schemas/input/sign.json'), require("./schemas/input/sign.json"),
require('./schemas/input/submit.json'), require("./schemas/input/submit.json"),
require('./schemas/input/generate-address.json'), require("./schemas/input/generate-address.json"),
require('./schemas/input/sign-payment-channel-claim.json'), require("./schemas/input/sign-payment-channel-claim.json"),
require('./schemas/input/verify-payment-channel-claim.json'), require("./schemas/input/verify-payment-channel-claim.json"),
require('./schemas/input/combine.json') require("./schemas/input/combine.json"),
] ];
const titles = schemas.map((schema) => schema.title) const titles = schemas.map((schema) => schema.title);
const duplicates = Object.keys(_.pickBy(_.countBy(titles), (count) => count > 1)) const duplicates = Object.keys(
assert.ok(duplicates.length === 0, 'Duplicate schemas for: ' + duplicates) _.pickBy(_.countBy(titles), (count) => count > 1)
const validator = new Validator() );
assert.ok(duplicates.length === 0, `Duplicate schemas for: ${duplicates}`);
const validator = new Validator();
// Register custom format validators that ignore undefined instances // Register custom format validators that ignore undefined instances
// since jsonschema will still call the format validator on a missing // since jsonschema will still call the format validator on a missing
// (optional) property // (optional) property
@@ -136,49 +142,47 @@ function loadSchemas() {
// This relies on "format": "xAddress" in `x-address.json`! // This relies on "format": "xAddress" in `x-address.json`!
validator.customFormats.xAddress = function (instance) { validator.customFormats.xAddress = function (instance) {
if (instance == null) { if (instance == null) {
return true return true;
} }
return isValidXAddress(instance) return isValidXAddress(instance);
} };
// This relies on "format": "classicAddress" in `classic-address.json`! // This relies on "format": "classicAddress" in `classic-address.json`!
validator.customFormats.classicAddress = function (instance) { validator.customFormats.classicAddress = function (instance) {
if (instance == null) { if (instance == null) {
return true return true;
} }
return isValidAddress(instance) return isValidAddress(instance);
} };
validator.customFormats.secret = function (instance) { validator.customFormats.secret = function (instance) {
if (instance == null) { if (instance == null) {
return true return true;
} }
return isValidSecret(instance) return isValidSecret(instance);
} };
// Register under the root URI '/' // Register under the root URI '/'
schemas.forEach((schema) => schemas.forEach((schema) => validator.addSchema(schema, `/${schema.title}`));
validator.addSchema(schema, '/' + schema.title) return validator;
)
return validator
} }
const schemaValidator = loadSchemas() const schemaValidator = loadSchemas();
function schemaValidate(schemaName: string, object: any): void { function schemaValidate(schemaName: string, object: any): void {
// Lookup under the root URI '/' // Lookup under the root URI '/'
const schema = schemaValidator.getSchema('/' + schemaName) const schema = schemaValidator.getSchema(`/${schemaName}`);
if (schema == null) { if (schema == null) {
throw new ValidationError('no schema for ' + schemaName) throw new ValidationError(`no schema for ${schemaName}`);
} }
const result = schemaValidator.validate(object, schema) const result = schemaValidator.validate(object, schema);
if (!result.valid) { if (!result.valid) {
throw new ValidationError(result.errors.join()) throw new ValidationError(result.errors.join());
} }
} }
function isValidAddress(address: string): boolean { function isValidAddress(address: string): boolean {
return isValidXAddress(address) || isValidClassicAddress(address) return isValidXAddress(address) || isValidClassicAddress(address);
} }
export {schemaValidate, isValidSecret, isValidAddress} export { schemaValidate, isValidSecret, isValidAddress };

View File

@@ -1,7 +1,7 @@
const txFlags = { const txFlags = {
// Universal flags can apply to any transaction type // Universal flags can apply to any transaction type
Universal: { Universal: {
FullyCanonicalSig: 0x80000000 FullyCanonicalSig: 0x80000000,
}, },
AccountSet: { AccountSet: {
@@ -10,7 +10,7 @@ const txFlags = {
RequireAuth: 0x00040000, RequireAuth: 0x00040000,
OptionalAuth: 0x00080000, OptionalAuth: 0x00080000,
DisallowXRP: 0x00100000, DisallowXRP: 0x00100000,
AllowXRP: 0x00200000 AllowXRP: 0x00200000,
}, },
TrustSet: { TrustSet: {
@@ -19,27 +19,27 @@ const txFlags = {
SetNoRipple: 0x00020000, SetNoRipple: 0x00020000,
ClearNoRipple: 0x00040000, ClearNoRipple: 0x00040000,
SetFreeze: 0x00100000, SetFreeze: 0x00100000,
ClearFreeze: 0x00200000 ClearFreeze: 0x00200000,
}, },
OfferCreate: { OfferCreate: {
Passive: 0x00010000, Passive: 0x00010000,
ImmediateOrCancel: 0x00020000, ImmediateOrCancel: 0x00020000,
FillOrKill: 0x00040000, FillOrKill: 0x00040000,
Sell: 0x00080000 Sell: 0x00080000,
}, },
Payment: { Payment: {
NoRippleDirect: 0x00010000, NoRippleDirect: 0x00010000,
PartialPayment: 0x00020000, PartialPayment: 0x00020000,
LimitQuality: 0x00040000 LimitQuality: 0x00040000,
}, },
PaymentChannelClaim: { PaymentChannelClaim: {
Renew: 0x00010000, Renew: 0x00010000,
Close: 0x00020000 Close: 0x00020000,
} },
} };
// The following are integer (as opposed to bit) flags // The following are integer (as opposed to bit) flags
// that can be set for particular transactions in the // that can be set for particular transactions in the
@@ -54,8 +54,8 @@ const txFlagIndices = {
asfNoFreeze: 6, asfNoFreeze: 6,
asfGlobalFreeze: 7, asfGlobalFreeze: 7,
asfDefaultRipple: 8, asfDefaultRipple: 8,
asfDepositAuth: 9 asfDepositAuth: 9,
} },
} };
export {txFlags, txFlagIndices} export { txFlags, txFlagIndices };

View File

@@ -1,23 +1,23 @@
import { import {
AccountRootLedgerEntry, AccountRootLedgerEntry,
SignerListLedgerEntry, SignerListLedgerEntry,
QueueData QueueData,
} from '../objects' } from "../objects";
export interface AccountInfoRequest { export interface AccountInfoRequest {
account: string account: string;
strict?: boolean strict?: boolean;
queue?: boolean queue?: boolean;
ledger_hash?: string ledger_hash?: string;
ledger_index?: number | ('validated' | 'closed' | 'current') ledger_index?: number | ("validated" | "closed" | "current");
signer_lists?: boolean signer_lists?: boolean;
} }
export interface AccountInfoResponse { export interface AccountInfoResponse {
account_data: AccountRootLedgerEntry account_data: AccountRootLedgerEntry;
signer_lists?: SignerListLedgerEntry[] signer_lists?: SignerListLedgerEntry[];
ledger_current_index?: number ledger_current_index?: number;
ledger_index?: number ledger_index?: number;
queue_data?: QueueData queue_data?: QueueData;
validated?: boolean validated?: boolean;
} }

View File

@@ -1,19 +1,19 @@
import {Trustline} from '../objects' import { Trustline } from "../objects";
export interface AccountLinesRequest { export interface AccountLinesRequest {
account: string account: string;
ledger_hash?: string ledger_hash?: string;
ledger_index?: number | ('validated' | 'closed' | 'current') ledger_index?: number | ("validated" | "closed" | "current");
peer?: string peer?: string;
limit?: number limit?: number;
marker?: any marker?: any;
} }
export interface AccountLinesResponse { export interface AccountLinesResponse {
account: string account: string;
lines: Trustline[] lines: Trustline[];
ledger_current_index?: number ledger_current_index?: number;
ledger_index?: number ledger_index?: number;
ledger_hash?: string ledger_hash?: string;
marker?: any marker?: any;
} }

View File

@@ -1,4 +1,4 @@
import { AccountObjectType } from '../../../models/common'; import { AccountObjectType } from "../../../models/common";
import { import {
CheckLedgerEntry, CheckLedgerEntry,
RippleStateLedgerEntry, RippleStateLedgerEntry,
@@ -6,46 +6,46 @@ import {
SignerListLedgerEntry, SignerListLedgerEntry,
EscrowLedgerEntry, EscrowLedgerEntry,
PayChannelLedgerEntry, PayChannelLedgerEntry,
DepositPreauthLedgerEntry DepositPreauthLedgerEntry,
} from '../objects' } from "../objects";
export interface GetAccountObjectsOptions { export interface GetAccountObjectsOptions {
type?: AccountObjectType type?: AccountObjectType;
ledgerHash?: string ledgerHash?: string;
ledgerIndex?: number | ('validated' | 'closed' | 'current') ledgerIndex?: number | ("validated" | "closed" | "current");
limit?: number limit?: number;
marker?: string marker?: string;
} }
export interface AccountObjectsRequest { export interface AccountObjectsRequest {
account: string account: string;
// (Optional) Filter results to include only this type of ledger object. // (Optional) Filter results to include only this type of ledger object.
type?: type?:
| string | string
| ( | (
| 'check' | "check"
| 'escrow' | "escrow"
| 'offer' | "offer"
| 'payment_channel' | "payment_channel"
| 'signer_list' | "signer_list"
| 'state' | "state"
) );
// (Optional) A 20-byte hex string for the ledger version to use. // (Optional) A 20-byte hex string for the ledger version to use.
ledger_hash?: string ledger_hash?: string;
// (Optional) The sequence number of the ledger to use, // (Optional) The sequence number of the ledger to use,
// or a shortcut string to choose a ledger automatically. // or a shortcut string to choose a ledger automatically.
ledger_index?: number | ('validated' | 'closed' | 'current') ledger_index?: number | ("validated" | "closed" | "current");
limit?: number limit?: number;
marker?: string marker?: string;
} }
export interface AccountObjectsResponse { export interface AccountObjectsResponse {
account: string account: string;
// Array of objects owned by this account. // Array of objects owned by this account.
// from the getAccountObjects section of the dev center // from the getAccountObjects section of the dev center
@@ -57,29 +57,29 @@ export interface AccountObjectsResponse {
| EscrowLedgerEntry | EscrowLedgerEntry
| PayChannelLedgerEntry | PayChannelLedgerEntry
| DepositPreauthLedgerEntry | DepositPreauthLedgerEntry
> >;
// (May be omitted) The identifying hash of the ledger // (May be omitted) The identifying hash of the ledger
// that was used to generate this response. // that was used to generate this response.
ledger_hash?: string ledger_hash?: string;
// (May be omitted) The sequence number of the ledger version // (May be omitted) The sequence number of the ledger version
// that was used to generate this response. // that was used to generate this response.
ledger_index?: number ledger_index?: number;
// (May be omitted) The sequence number of the current in-progress ledger // (May be omitted) The sequence number of the current in-progress ledger
// version that was used to generate this response. // version that was used to generate this response.
ledger_current_index?: number ledger_current_index?: number;
// The limit that was used in this request, if any. // The limit that was used in this request, if any.
limit?: number limit?: number;
// Server-defined value indicating the response is paginated. Pass this // Server-defined value indicating the response is paginated. Pass this
// to the next call to resume where this call left off. Omitted when there // to the next call to resume where this call left off. Omitted when there
// are no additional pages after this one. // are no additional pages after this one.
marker?: string marker?: string;
// If true, this information comes from a ledger version // If true, this information comes from a ledger version
// that has been validated by consensus. // that has been validated by consensus.
validated?: boolean validated?: boolean;
} }

View File

@@ -1,27 +1,27 @@
import {RippledAmount} from '../objects' import { RippledAmount } from "../objects";
export interface AccountOffersRequest { export interface AccountOffersRequest {
account: string account: string;
ledger_hash?: string ledger_hash?: string;
ledger_index?: number | ('validated' | 'closed' | 'current') ledger_index?: number | ("validated" | "closed" | "current");
limit?: number limit?: number;
marker?: any marker?: any;
} }
export interface AccountOffersResponse { export interface AccountOffersResponse {
account: string account: string;
ledger_hash?: string ledger_hash?: string;
ledger_current_index?: number ledger_current_index?: number;
ledger_index?: number ledger_index?: number;
marker?: any marker?: any;
offers?: AccountOffer[] offers?: AccountOffer[];
} }
export interface AccountOffer { export interface AccountOffer {
seq: number seq: number;
flags: number flags: number;
taker_gets: RippledAmount taker_gets: RippledAmount;
taker_pays: RippledAmount taker_pays: RippledAmount;
quality: string quality: string;
expiration?: number expiration?: number;
} }

View File

@@ -1,26 +1,30 @@
import {TakerRequestAmount, RippledAmount, OfferLedgerEntry} from '../objects' import {
TakerRequestAmount,
RippledAmount,
OfferLedgerEntry,
} from "../objects";
export interface BookOffersRequest { export interface BookOffersRequest {
taker?: string taker?: string;
taker_gets: TakerRequestAmount taker_gets: TakerRequestAmount;
taker_pays: TakerRequestAmount taker_pays: TakerRequestAmount;
ledger_hash?: string ledger_hash?: string;
ledger_index?: number | ('validated' | 'closed' | 'current') ledger_index?: number | ("validated" | "closed" | "current");
limit?: number limit?: number;
marker?: any marker?: any;
} }
export interface BookOffersResponse { export interface BookOffersResponse {
offers: BookOffer[] offers: BookOffer[];
ledger_hash?: string ledger_hash?: string;
ledger_current_index?: number ledger_current_index?: number;
ledger_index?: number ledger_index?: number;
marker?: any marker?: any;
} }
export interface BookOffer extends OfferLedgerEntry { export interface BookOffer extends OfferLedgerEntry {
quality?: string quality?: string;
owner_funds?: string owner_funds?: string;
taker_gets_funded?: RippledAmount taker_gets_funded?: RippledAmount;
taker_pays_funded?: RippledAmount taker_pays_funded?: RippledAmount;
} }

View File

@@ -1,19 +1,19 @@
import {Amount} from '../objects' import { Amount } from "../objects";
export interface GatewayBalancesRequest { export interface GatewayBalancesRequest {
account: string account: string;
strict?: boolean strict?: boolean;
hotwallet: string | Array<string> hotwallet: string | string[];
ledger_hash?: string ledger_hash?: string;
ledger_index?: number | ('validated' | 'closed' | 'current') ledger_index?: number | ("validated" | "closed" | "current");
} }
export interface GatewayBalancesResponse { export interface GatewayBalancesResponse {
account: string account: string;
obligations?: {[currency: string]: string} obligations?: { [currency: string]: string };
balances?: {[address: string]: Amount[]} balances?: { [address: string]: Amount[] };
assets?: {[address: string]: Amount[]} assets?: { [address: string]: Amount[] };
ledger_hash?: string ledger_hash?: string;
ledger_current_index?: number ledger_current_index?: number;
ledger_index?: number ledger_index?: number;
} }

View File

@@ -1,10 +1,10 @@
export * from './account_info' export * from "./account_info";
export * from './account_lines' export * from "./account_lines";
export * from './account_objects' export * from "./account_objects";
export * from './account_offers' export * from "./account_offers";
export * from './book_offers' export * from "./book_offers";
export * from './gateway_balances' export * from "./gateway_balances";
export * from './ledger' export * from "./ledger";
export * from './ledger_data' export * from "./ledger_data";
export * from './ledger_entry' export * from "./ledger_entry";
export * from './server_info' export * from "./server_info";

View File

@@ -1,20 +1,20 @@
import {Ledger, QueueData} from '../objects' import { Ledger, QueueData } from "../objects";
export interface LedgerRequest { export interface LedgerRequest {
ledger_hash?: string ledger_hash?: string;
ledger_index?: number | ('validated' | 'closed' | 'current') ledger_index?: number | ("validated" | "closed" | "current");
full?: boolean full?: boolean;
accounts?: boolean accounts?: boolean;
transactions?: boolean transactions?: boolean;
expand?: boolean expand?: boolean;
owner_funds?: boolean owner_funds?: boolean;
binary?: boolean binary?: boolean;
queue?: boolean queue?: boolean;
} }
export interface LedgerResponse { export interface LedgerResponse {
ledger_index: number ledger_index: number;
ledger_hash: string ledger_hash: string;
ledger: Ledger ledger: Ledger;
queue_data?: QueueData queue_data?: QueueData;
} }

View File

@@ -1,12 +1,12 @@
import {LedgerData} from '../objects' import { LedgerData } from "../objects";
export interface LedgerDataRequest { export interface LedgerDataRequest {
id?: any id?: any;
ledger_hash?: string ledger_hash?: string;
ledger_index?: string ledger_index?: string;
binary?: boolean binary?: boolean;
limit?: number limit?: number;
marker?: string marker?: string;
} }
export type LedgerDataResponse = LedgerData export type LedgerDataResponse = LedgerData;

View File

@@ -1,36 +1,36 @@
import {LedgerEntry} from '../objects' import { LedgerEntry } from "../objects";
export interface LedgerEntryRequest { export interface LedgerEntryRequest {
ledger_hash?: string ledger_hash?: string;
ledger_index?: number | ('validated' | 'closed' | 'current') ledger_index?: number | ("validated" | "closed" | "current");
index?: string index?: string;
account_root?: string account_root?: string;
directory?: directory?:
| string | string
| { | {
sub_index?: number sub_index?: number;
dir_root: string dir_root: string;
} }
| { | {
sub_index?: number sub_index?: number;
owner: string owner: string;
} };
offer?: offer?:
| string | string
| { | {
account: string account: string;
seq: number seq: number;
} };
ripple_state?: { ripple_state?: {
accounts: [string, string] accounts: [string, string];
currency: string currency: string;
} };
binary?: boolean binary?: boolean;
} }
export interface LedgerEntryResponse { export interface LedgerEntryResponse {
index: string index: string;
ledger_index: number ledger_index: number;
node_binary?: string node_binary?: string;
node?: LedgerEntry node?: LedgerEntry;
} }

View File

@@ -1,51 +1,51 @@
export interface ServerInfoRequest { export interface ServerInfoRequest {
id?: number id?: number;
} }
export interface ServerInfoResponse { export interface ServerInfoResponse {
info: { info: {
amendment_blocked?: boolean amendment_blocked?: boolean;
build_version: string build_version: string;
closed_ledger?: LedgerInfo closed_ledger?: LedgerInfo;
complete_ledgers: string complete_ledgers: string;
hostid: string hostid: string;
io_latency_ms: number io_latency_ms: number;
last_close: { last_close: {
converge_time_s: number converge_time_s: number;
proposers: number proposers: number;
} };
load?: { load?: {
job_types: { job_types: Array<{
job_type: string job_type: string;
per_second: number per_second: number;
in_progress: number in_progress: number;
}[] }>;
threads: number threads: number;
} };
load_factor: number load_factor: number;
load_factor_local?: number load_factor_local?: number;
load_factor_net?: number load_factor_net?: number;
load_factor_cluster?: number load_factor_cluster?: number;
load_factor_fee_escalation?: number load_factor_fee_escalation?: number;
load_factor_fee_queue?: number load_factor_fee_queue?: number;
load_factor_server?: number load_factor_server?: number;
peers: number peers: number;
pubkey_node: string pubkey_node: string;
pubkey_validator: string pubkey_validator: string;
server_state: string server_state: string;
state_accounting: any state_accounting: any;
uptime: number uptime: number;
validated_ledger?: LedgerInfo validated_ledger?: LedgerInfo;
validation_quorum: number validation_quorum: number;
validator_list_expires: string validator_list_expires: string;
} };
} }
export interface LedgerInfo { export interface LedgerInfo {
age: number age: number;
base_fee_xrp: number base_fee_xrp: number;
hash: string hash: string;
reserve_base_xrp: number reserve_base_xrp: number;
reserve_inc_xrp: number reserve_inc_xrp: number;
seq: number seq: number;
} }

View File

@@ -1,19 +1,19 @@
import {Amount} from './amounts' import { Amount } from "./amounts";
export type Adjustment = { export interface Adjustment {
address: string address: string;
amount: Amount amount: Amount;
tag?: number tag?: number;
} }
export type MaxAdjustment = { export interface MaxAdjustment {
address: string address: string;
maxAmount: Amount maxAmount: Amount;
tag?: number tag?: number;
} }
export type MinAdjustment = { export interface MinAdjustment {
address: string address: string;
minAmount: Amount minAmount: Amount;
tag?: number tag?: number;
} }

View File

@@ -1,8 +1,8 @@
export interface Amount extends Issue { export interface Amount extends Issue {
value: string value: string;
} }
export type RippledAmount = string | Amount export type RippledAmount = string | Amount;
/** /**
* Specification of which currency the account taking the offer would pay/ * Specification of which currency the account taking the offer would pay/
@@ -10,15 +10,15 @@ export type RippledAmount = string | Amount
* Similar to currency amounts. * Similar to currency amounts.
*/ */
export interface TakerRequestAmount { export interface TakerRequestAmount {
currency: string currency: string;
issuer?: string issuer?: string;
} }
/** /**
* A currency-counterparty pair, or just currency if it's XRP. * A currency-counterparty pair, or just currency if it's XRP.
*/ */
export interface Issue { export interface Issue {
currency: string currency: string;
issuer?: string issuer?: string;
counterparty?: string counterparty?: string;
} }

View File

@@ -1,12 +1,12 @@
export * from './adjustments' export * from "./adjustments";
export * from './amounts' export * from "./amounts";
export * from './ledger' export * from "./ledger";
export * from './ledger_data' export * from "./ledger_data";
export * from './ledger_entries' export * from "./ledger_entries";
export * from './memos' export * from "./memos";
export * from './orders' export * from "./orders";
export * from './queue_data' export * from "./queue_data";
export * from './settings' export * from "./settings";
export * from './signers' export * from "./signers";
export * from './transactions' export * from "./transactions";
export * from './trustlines' export * from "./trustlines";

View File

@@ -1,31 +1,31 @@
export interface Ledger { export interface Ledger {
account_hash: string account_hash: string;
close_time: number close_time: number;
close_time_human: string close_time_human: string;
close_time_resolution: number close_time_resolution: number;
closed: boolean closed: boolean;
ledger_hash: string ledger_hash: string;
ledger_index: string ledger_index: string;
parent_hash: string parent_hash: string;
total_coins: string total_coins: string;
transaction_hash: string transaction_hash: string;
transactions: string[] | object[] transactions: string[] | object[];
close_flags?: number close_flags?: number;
parent_close_time?: number parent_close_time?: number;
accountState?: any[] accountState?: any[];
validated?: boolean validated?: boolean;
} }
// https://xrpl.org/subscribe.html#ledger-stream // https://xrpl.org/subscribe.html#ledger-stream
export type LedgerClosedEvent = { export interface LedgerClosedEvent {
type: 'ledgerClosed' type: "ledgerClosed";
fee_base: number fee_base: number;
fee_ref: number fee_ref: number;
ledger_hash: string ledger_hash: string;
ledger_index: number ledger_index: number;
ledger_time: number ledger_time: number;
reserve_base: number reserve_base: number;
reserve_inc: number reserve_inc: number;
txn_count: number txn_count: number;
validated_ledgers: string validated_ledgers: string;
} }

View File

@@ -1,6 +1,8 @@
export interface LedgerData { export interface LedgerData {
ledger_index: string ledger_index: string;
ledger_hash: string ledger_hash: string;
marker: string marker: string;
state: ({data?: string; LedgerEntryType?: string; index: string} & any)[] state: Array<
{ data?: string; LedgerEntryType?: string; index: string } & any
>;
} }

View File

@@ -1,175 +1,176 @@
import {SignerEntry} from './index' import { Amount, RippledAmount } from "./amounts";
import {Amount, RippledAmount} from './amounts'
import { SignerEntry } from "./index";
export interface AccountRootLedgerEntry { export interface AccountRootLedgerEntry {
LedgerEntryType: 'AccountRoot' LedgerEntryType: "AccountRoot";
Account: string Account: string;
Balance: string Balance: string;
Flags: number Flags: number;
OwnerCount: number OwnerCount: number;
PreviousTxnID: string PreviousTxnID: string;
PreviousTxnLgrSeq: number PreviousTxnLgrSeq: number;
Sequence: number Sequence: number;
AccountTxnID?: string AccountTxnID?: string;
Domain?: string Domain?: string;
EmailHash?: string EmailHash?: string;
MessageKey?: string MessageKey?: string;
RegularKey?: string RegularKey?: string;
TickSize?: number TickSize?: number;
TransferRate?: number TransferRate?: number;
WalletLocator?: string WalletLocator?: string;
WalletSize?: number // DEPRECATED WalletSize?: number; // DEPRECATED
} }
export interface AmendmentsLedgerEntry { export interface AmendmentsLedgerEntry {
LedgerEntryType: 'Amendments' LedgerEntryType: "Amendments";
Amendments?: string[] Amendments?: string[];
Majorities?: any[] Majorities?: any[];
Flags: 0 Flags: 0;
} }
export interface CheckLedgerEntry { export interface CheckLedgerEntry {
LedgerEntryType: 'Check' LedgerEntryType: "Check";
Account: string Account: string;
Destination Destination;
string string;
Flags: 0 Flags: 0;
OwnerNode: string OwnerNode: string;
PreviousTxnID: string PreviousTxnID: string;
PreviousTxnLgrSeq: number PreviousTxnLgrSeq: number;
SendMax: string | object SendMax: string | object;
Sequence: number Sequence: number;
DestinationNode: string DestinationNode: string;
DestinationTag: number DestinationTag: number;
Expiration: number Expiration: number;
InvoiceID: string InvoiceID: string;
SourceTag: number SourceTag: number;
} }
export interface DepositPreauthLedgerEntry { export interface DepositPreauthLedgerEntry {
LedgerEntryType: 'DepositPreauth' LedgerEntryType: "DepositPreauth";
Account: string Account: string;
Authorize: string Authorize: string;
OwnerNode: string OwnerNode: string;
PreviousTxnID: string PreviousTxnID: string;
PreviousTxnLgrSeq: number PreviousTxnLgrSeq: number;
} }
export interface DirectoryNodeLedgerEntry { export interface DirectoryNodeLedgerEntry {
LedgerEntryType: 'DirectoryNode' LedgerEntryType: "DirectoryNode";
Flags: number Flags: number;
RootIndex: string RootIndex: string;
Indexes: string[] Indexes: string[];
IndexNext?: number IndexNext?: number;
IndexPrevious?: number IndexPrevious?: number;
} }
export interface OfferDirectoryNodeLedgerEntry export interface OfferDirectoryNodeLedgerEntry
extends DirectoryNodeLedgerEntry { extends DirectoryNodeLedgerEntry {
TakerPaysCurrency: string TakerPaysCurrency: string;
TakerPaysIssuer: string TakerPaysIssuer: string;
TakerGetsCurrency: string TakerGetsCurrency: string;
TakerGetsIssuer: string TakerGetsIssuer: string;
ExchangeRate?: number // DEPRECATED ExchangeRate?: number; // DEPRECATED
} }
export interface OwnerDirectoryNodeLedgerEntry export interface OwnerDirectoryNodeLedgerEntry
extends DirectoryNodeLedgerEntry { extends DirectoryNodeLedgerEntry {
Owner: string Owner: string;
} }
export interface EscrowLedgerEntry { export interface EscrowLedgerEntry {
LedgerEntryType: 'Escrow' LedgerEntryType: "Escrow";
Account: string Account: string;
Destination: string Destination: string;
Amount: string Amount: string;
Condition?: string Condition?: string;
CancelAfter?: number CancelAfter?: number;
FinishAfter?: number FinishAfter?: number;
Flags: number Flags: number;
SourceTag?: number SourceTag?: number;
DestinationTag?: number DestinationTag?: number;
OwnerNode: string OwnerNode: string;
DestinationNode?: string DestinationNode?: string;
PreviousTxnID: string PreviousTxnID: string;
PreviousTxnLgrSeq: number PreviousTxnLgrSeq: number;
} }
export interface FeeSettingsLedgerEntry { export interface FeeSettingsLedgerEntry {
LedgerEntryType: 'FeeSettings' LedgerEntryType: "FeeSettings";
BaseFee: string BaseFee: string;
ReferenceFeeUnits: number ReferenceFeeUnits: number;
ReserveBase: number ReserveBase: number;
ReserveIncrement: number ReserveIncrement: number;
Flags: number Flags: number;
} }
export interface LedgerHashesLedgerEntry { export interface LedgerHashesLedgerEntry {
LedgerEntryType: 'LedgerHashes' LedgerEntryType: "LedgerHashes";
Hashes: string[] Hashes: string[];
Flags: number Flags: number;
FirstLedgerSequence?: number // DEPRECATED FirstLedgerSequence?: number; // DEPRECATED
LastLedgerSequence?: number LastLedgerSequence?: number;
} }
export interface OfferLedgerEntry { export interface OfferLedgerEntry {
LedgerEntryType: 'Offer' LedgerEntryType: "Offer";
Flags: number Flags: number;
Account: string Account: string;
Sequence: number Sequence: number;
TakerPays: RippledAmount TakerPays: RippledAmount;
TakerGets: RippledAmount TakerGets: RippledAmount;
BookDirectory: string BookDirectory: string;
BookNode: string BookNode: string;
OwnerNode: string OwnerNode: string;
PreviousTxnID: string PreviousTxnID: string;
PreviousTxnLgrSeq: number PreviousTxnLgrSeq: number;
Expiration?: number Expiration?: number;
} }
export interface PayChannelLedgerEntry { export interface PayChannelLedgerEntry {
LedgerEntryType: 'PayChannel' LedgerEntryType: "PayChannel";
Sequence: number Sequence: number;
Account: string Account: string;
Amount: string Amount: string;
Balance: string Balance: string;
PublicKey: string PublicKey: string;
Destination: string Destination: string;
SettleDelay: number SettleDelay: number;
Expiration?: number Expiration?: number;
CancelAfter?: number CancelAfter?: number;
SourceTag?: number SourceTag?: number;
DestinationTag?: number DestinationTag?: number;
OwnerNode: string OwnerNode: string;
PreviousTxnID: string PreviousTxnID: string;
PreviousTxnLgrSeq: number PreviousTxnLgrSeq: number;
index: string index: string;
} }
export interface RippleStateLedgerEntry { export interface RippleStateLedgerEntry {
LedgerEntryType: 'RippleState' LedgerEntryType: "RippleState";
Flags: number Flags: number;
Balance: Amount Balance: Amount;
LowLimit: Amount LowLimit: Amount;
HighLimit: Amount HighLimit: Amount;
PreviousTxnID: string PreviousTxnID: string;
PreviousTxnLgrSeq: number PreviousTxnLgrSeq: number;
LowNode?: string LowNode?: string;
HighNode?: string HighNode?: string;
LowQualityIn?: number LowQualityIn?: number;
LowQualityOut?: number LowQualityOut?: number;
HighQualityIn?: number HighQualityIn?: number;
HighQualityOut?: number HighQualityOut?: number;
} }
export interface SignerListLedgerEntry { export interface SignerListLedgerEntry {
LedgerEntryType: 'SignerList' LedgerEntryType: "SignerList";
OwnerNode: string OwnerNode: string;
SignerQuorum: number SignerQuorum: number;
SignerEntries: SignerEntry[] SignerEntries: SignerEntry[];
SignerListID: number SignerListID: number;
PreviousTxnID: string PreviousTxnID: string;
PreviousTxnLgrSeq: number PreviousTxnLgrSeq: number;
} }
// see https://ripple.com/build/ledger-format/#ledger-object-types // see https://ripple.com/build/ledger-format/#ledger-object-types
@@ -187,4 +188,4 @@ export type LedgerEntry =
| OfferLedgerEntry | OfferLedgerEntry
| PayChannelLedgerEntry | PayChannelLedgerEntry
| RippleStateLedgerEntry | RippleStateLedgerEntry
| SignerListLedgerEntry | SignerListLedgerEntry;

View File

@@ -1,5 +1,5 @@
export type Memo = { export interface Memo {
type?: string type?: string;
format?: string format?: string;
data?: string data?: string;
} }

View File

@@ -1,17 +1,17 @@
import {Amount} from './amounts' import { Amount } from "./amounts";
import {Memo} from './memos' import { Memo } from "./memos";
export type FormattedOrderSpecification = { export interface FormattedOrderSpecification {
direction: string direction: string;
quantity: Amount quantity: Amount;
totalPrice: Amount totalPrice: Amount;
immediateOrCancel?: boolean immediateOrCancel?: boolean;
fillOrKill?: boolean fillOrKill?: boolean;
expirationTime?: string expirationTime?: string;
orderToReplace?: number orderToReplace?: number;
memos?: Memo[] memos?: Memo[];
// If enabled, the offer will not consume offers that exactly match it, and // If enabled, the offer will not consume offers that exactly match it, and
// instead becomes an Offer node in the ledger. It will still consume offers // instead becomes an Offer node in the ledger. It will still consume offers
// that cross it. // that cross it.
passive?: boolean passive?: boolean;
} }

View File

@@ -1,16 +1,16 @@
export interface QueueTransaction { export interface QueueTransaction {
auth_change: boolean auth_change: boolean;
fee: string fee: string;
fee_level: string fee_level: string;
max_spend_drops: string max_spend_drops: string;
seq: number seq: number;
} }
export interface QueueData { export interface QueueData {
txn_count: number txn_count: number;
auth_change_queued?: boolean auth_change_queued?: boolean;
lowest_sequence?: number lowest_sequence?: number;
highest_sequence?: number highest_sequence?: number;
max_spend_drops_total?: string max_spend_drops_total?: string;
transactions?: QueueTransaction[] transactions?: QueueTransaction[];
} }

View File

@@ -1,33 +1,33 @@
import {Memo} from './memos' import { Memo } from "./memos";
export type WeightedSigner = { export interface WeightedSigner {
address: string address: string;
weight: number weight: number;
} }
export type Signers = { export interface Signers {
threshold?: number threshold?: number;
weights: WeightedSigner[] weights: WeightedSigner[];
} }
export type FormattedSettings = { export interface FormattedSettings {
defaultRipple?: boolean defaultRipple?: boolean;
depositAuth?: boolean depositAuth?: boolean;
disableMasterKey?: boolean disableMasterKey?: boolean;
disallowIncomingXRP?: boolean disallowIncomingXRP?: boolean;
domain?: string domain?: string;
emailHash?: string | null emailHash?: string | null;
walletLocator?: string | null walletLocator?: string | null;
enableTransactionIDTracking?: boolean enableTransactionIDTracking?: boolean;
globalFreeze?: boolean globalFreeze?: boolean;
memos?: Memo[] memos?: Memo[];
messageKey?: string messageKey?: string;
noFreeze?: boolean noFreeze?: boolean;
passwordSpent?: boolean passwordSpent?: boolean;
regularKey?: string regularKey?: string;
requireAuthorization?: boolean requireAuthorization?: boolean;
requireDestinationTag?: boolean requireDestinationTag?: boolean;
signers?: Signers signers?: Signers;
transferRate?: number | null transferRate?: number | null;
tickSize?: number tickSize?: number;
} }

View File

@@ -1,6 +1,6 @@
export interface SignerEntry { export interface SignerEntry {
SignerEntry: { SignerEntry: {
Account: string Account: string;
SignerWeight: number SignerWeight: number;
} };
} }

View File

@@ -1,27 +1,27 @@
import {RippledAmount} from './amounts' import { RippledAmount } from "./amounts";
import {Memo} from './memos' import { Memo } from "./memos";
export interface OfferCreateTransaction { export interface OfferCreateTransaction {
TransactionType: 'OfferCreate' TransactionType: "OfferCreate";
Account: string Account: string;
AccountTxnID?: string AccountTxnID?: string;
Fee: string Fee: string;
Field: any Field: any;
Flags: number Flags: number;
LastLedgerSequence?: number LastLedgerSequence?: number;
Sequence: number Sequence: number;
Signers: any[] Signers: any[];
SigningPubKey: string SigningPubKey: string;
SourceTag?: number SourceTag?: number;
TakerGets: RippledAmount TakerGets: RippledAmount;
TakerPays: RippledAmount TakerPays: RippledAmount;
TxnSignature: string TxnSignature: string;
Expiration?: number Expiration?: number;
Memos?: Memo[] Memos?: Memo[];
OfferSequence?: number OfferSequence?: number;
} }
export interface SignedTransaction { export interface SignedTransaction {
signedTransaction: string signedTransaction: string;
id: string id: string;
} }

View File

@@ -1,42 +1,42 @@
import {Memo} from './memos' import { Memo } from "./memos";
export interface Trustline { export interface Trustline {
account: string account: string;
balance: string balance: string;
currency: string currency: string;
limit: string limit: string;
limit_peer: string limit_peer: string;
quality_in: number quality_in: number;
quality_out: number quality_out: number;
no_ripple?: boolean no_ripple?: boolean;
no_ripple_peer?: boolean no_ripple_peer?: boolean;
freeze?: boolean freeze?: boolean;
freeze_peer?: boolean freeze_peer?: boolean;
authorized?: boolean authorized?: boolean;
peer_authorized?: boolean peer_authorized?: boolean;
} }
export type FormattedTrustlineSpecification = { export interface FormattedTrustlineSpecification {
currency: string currency: string;
counterparty: string counterparty: string;
limit: string limit: string;
qualityIn?: number qualityIn?: number;
qualityOut?: number qualityOut?: number;
ripplingDisabled?: boolean ripplingDisabled?: boolean;
authorized?: boolean authorized?: boolean;
frozen?: boolean frozen?: boolean;
memos?: Memo[] memos?: Memo[];
} }
export type FormattedTrustline = { export interface FormattedTrustline {
specification: FormattedTrustlineSpecification specification: FormattedTrustlineSpecification;
counterparty: { counterparty: {
limit: string limit: string;
ripplingDisabled?: boolean ripplingDisabled?: boolean;
frozen?: boolean frozen?: boolean;
authorized?: boolean authorized?: boolean;
} };
state: { state: {
balance: string balance: string;
} };
} }

View File

@@ -1,9 +1,10 @@
import _ from 'lodash' import _ from "lodash";
import {ValidationError} from './errors'
import {schemaValidate} from './schema-validator' import { ValidationError } from "./errors";
import { schemaValidate } from "./schema-validator";
function error(text) { function error(text) {
return new ValidationError(text) return new ValidationError(text);
} }
function validateLedgerRange(options) { function validateLedgerRange(options) {
@@ -13,158 +14,161 @@ function validateLedgerRange(options) {
options.maxLedgerVersion != null options.maxLedgerVersion != null
) { ) {
if (Number(options.minLedgerVersion) > Number(options.maxLedgerVersion)) { if (Number(options.minLedgerVersion) > Number(options.maxLedgerVersion)) {
throw error('minLedgerVersion must not be greater than maxLedgerVersion') throw error("minLedgerVersion must not be greater than maxLedgerVersion");
} }
} }
} }
function validateOptions(schema, instance) { function validateOptions(schema, instance) {
schemaValidate(schema, instance) schemaValidate(schema, instance);
validateLedgerRange(instance.options) validateLedgerRange(instance.options);
} }
export const getPaths = _.partial(schemaValidate, 'getPathsParameters') export const getPaths = _.partial(schemaValidate, "getPathsParameters");
export const getTransactions = _.partial( export const getTransactions = _.partial(
validateOptions, validateOptions,
'getTransactionsParameters' "getTransactionsParameters"
) );
export const getSettings = _.partial(validateOptions, 'getSettingsParameters') export const getSettings = _.partial(validateOptions, "getSettingsParameters");
export const getAccountInfo = _.partial( export const getAccountInfo = _.partial(
validateOptions, validateOptions,
'getAccountInfoParameters' "getAccountInfoParameters"
) );
export const getTrustlines = _.partial( export const getTrustlines = _.partial(
validateOptions, validateOptions,
'getTrustlinesParameters' "getTrustlinesParameters"
) );
export const getBalances = _.partial(validateOptions, 'getBalancesParameters') export const getBalances = _.partial(validateOptions, "getBalancesParameters");
export const getBalanceSheet = _.partial( export const getBalanceSheet = _.partial(
validateOptions, validateOptions,
'getBalanceSheetParameters' "getBalanceSheetParameters"
) );
export const getOrders = _.partial(validateOptions, 'getOrdersParameters') export const getOrders = _.partial(validateOptions, "getOrdersParameters");
export const getOrderbook = _.partial(validateOptions, 'getOrderbookParameters') export const getOrderbook = _.partial(
validateOptions,
"getOrderbookParameters"
);
export const getTransaction = _.partial( export const getTransaction = _.partial(
validateOptions, validateOptions,
'getTransactionParameters' "getTransactionParameters"
) );
export const getPaymentChannel = _.partial( export const getPaymentChannel = _.partial(
validateOptions, validateOptions,
'getPaymentChannelParameters' "getPaymentChannelParameters"
) );
export const getLedger = _.partial(validateOptions, 'getLedgerParameters') export const getLedger = _.partial(validateOptions, "getLedgerParameters");
export const preparePayment = _.partial( export const preparePayment = _.partial(
schemaValidate, schemaValidate,
'preparePaymentParameters' "preparePaymentParameters"
) );
export const prepareOrder = _.partial(schemaValidate, 'prepareOrderParameters') export const prepareOrder = _.partial(schemaValidate, "prepareOrderParameters");
export const prepareOrderCancellation = _.partial( export const prepareOrderCancellation = _.partial(
schemaValidate, schemaValidate,
'prepareOrderCancellationParameters' "prepareOrderCancellationParameters"
) );
export const prepareTrustline = _.partial( export const prepareTrustline = _.partial(
schemaValidate, schemaValidate,
'prepareTrustlineParameters' "prepareTrustlineParameters"
) );
export const prepareSettings = _.partial( export const prepareSettings = _.partial(
schemaValidate, schemaValidate,
'prepareSettingsParameters' "prepareSettingsParameters"
) );
export const prepareEscrowCreation = _.partial( export const prepareEscrowCreation = _.partial(
schemaValidate, schemaValidate,
'prepareEscrowCreationParameters' "prepareEscrowCreationParameters"
) );
export const prepareEscrowCancellation = _.partial( export const prepareEscrowCancellation = _.partial(
schemaValidate, schemaValidate,
'prepareEscrowCancellationParameters' "prepareEscrowCancellationParameters"
) );
export const prepareEscrowExecution = _.partial( export const prepareEscrowExecution = _.partial(
schemaValidate, schemaValidate,
'prepareEscrowExecutionParameters' "prepareEscrowExecutionParameters"
) );
export const preparePaymentChannelCreate = _.partial( export const preparePaymentChannelCreate = _.partial(
schemaValidate, schemaValidate,
'preparePaymentChannelCreateParameters' "preparePaymentChannelCreateParameters"
) );
export const preparePaymentChannelFund = _.partial( export const preparePaymentChannelFund = _.partial(
schemaValidate, schemaValidate,
'preparePaymentChannelFundParameters' "preparePaymentChannelFundParameters"
) );
export const preparePaymentChannelClaim = _.partial( export const preparePaymentChannelClaim = _.partial(
schemaValidate, schemaValidate,
'preparePaymentChannelClaimParameters' "preparePaymentChannelClaimParameters"
) );
export const prepareCheckCreate = _.partial( export const prepareCheckCreate = _.partial(
schemaValidate, schemaValidate,
'prepareCheckCreateParameters' "prepareCheckCreateParameters"
) );
export const prepareCheckCash = _.partial( export const prepareCheckCash = _.partial(
schemaValidate, schemaValidate,
'prepareCheckCashParameters' "prepareCheckCashParameters"
) );
export const prepareCheckCancel = _.partial( export const prepareCheckCancel = _.partial(
schemaValidate, schemaValidate,
'prepareCheckCancelParameters' "prepareCheckCancelParameters"
) );
export const prepareTicketCreate = _.partial( export const prepareTicketCreate = _.partial(
schemaValidate, schemaValidate,
'prepareTicketParameters' "prepareTicketParameters"
) );
export const sign = _.partial(schemaValidate, 'signParameters') export const sign = _.partial(schemaValidate, "signParameters");
export const combine = _.partial(schemaValidate, 'combineParameters') export const combine = _.partial(schemaValidate, "combineParameters");
export const submit = _.partial(schemaValidate, 'submitParameters') export const submit = _.partial(schemaValidate, "submitParameters");
export const computeLedgerHash = _.partial( export const computeLedgerHash = _.partial(
schemaValidate, schemaValidate,
'computeLedgerHashParameters' "computeLedgerHashParameters"
) );
export const generateAddress = _.partial( export const generateAddress = _.partial(
schemaValidate, schemaValidate,
'generateAddressParameters' "generateAddressParameters"
) );
export const signPaymentChannelClaim = _.partial( export const signPaymentChannelClaim = _.partial(
schemaValidate, schemaValidate,
'signPaymentChannelClaimParameters' "signPaymentChannelClaimParameters"
) );
export const verifyPaymentChannelClaim = _.partial( export const verifyPaymentChannelClaim = _.partial(
schemaValidate, schemaValidate,
'verifyPaymentChannelClaimParameters' "verifyPaymentChannelClaimParameters"
) );
export const apiOptions = _.partial(schemaValidate, 'api-options') export const apiOptions = _.partial(schemaValidate, "api-options");
export const instructions = _.partial(schemaValidate, 'instructions') export const instructions = _.partial(schemaValidate, "instructions");
export const tx_json = _.partial(schemaValidate, 'tx-json') export const tx_json = _.partial(schemaValidate, "tx-json");

View File

@@ -1,14 +1,14 @@
export {Client} from './client' export { Client } from "./client";
export * from './transaction/types' export * from "./transaction/types";
export * from './common/types/objects/ledger' export * from "./common/types/objects/ledger";
export * from './models/methods' export * from "./models/methods";
export * from './utils' export * from "./utils";
// Broadcast client is experimental // Broadcast client is experimental
export {BroadcastClient} from './client/broadcastClient' export { BroadcastClient } from "./client/broadcastClient";
export * from './Wallet' export * from "./Wallet";

View File

@@ -1,42 +1,46 @@
import * as utils from './utils' import { Client } from "..";
import {validate, ensureClassicAddress} from '../common' import { Connection } from "../client";
import {Connection} from '../client' import { validate, ensureClassicAddress } from "../common";
import {GetTrustlinesOptions} from './trustlines' import { FormattedTrustline } from "../common/types/objects/trustlines";
import {FormattedTrustline} from '../common/types/objects/trustlines'
import {Client} from '..'
export type Balance = { import { GetTrustlinesOptions } from "./trustlines";
value: string import * as utils from "./utils";
currency: string
counterparty?: string export interface Balance {
value: string;
currency: string;
counterparty?: string;
} }
export type GetBalances = Array<Balance> export type GetBalances = Balance[];
function getTrustlineBalanceAmount(trustline: FormattedTrustline): Balance { function getTrustlineBalanceAmount(trustline: FormattedTrustline): Balance {
return { return {
currency: trustline.specification.currency, currency: trustline.specification.currency,
counterparty: trustline.specification.counterparty, counterparty: trustline.specification.counterparty,
value: trustline.state.balance value: trustline.state.balance,
} };
} }
function formatBalances(options: GetTrustlinesOptions, balances: {xrp: string, trustlines: FormattedTrustline[]}) { function formatBalances(
const result = balances.trustlines.map(getTrustlineBalanceAmount) options: GetTrustlinesOptions,
balances: { xrp: string; trustlines: FormattedTrustline[] }
) {
const result = balances.trustlines.map(getTrustlineBalanceAmount);
if ( if (
!(options.counterparty || (options.currency && options.currency !== 'XRP')) !(options.counterparty || (options.currency && options.currency !== "XRP"))
) { ) {
const xrpBalance = { const xrpBalance = {
currency: 'XRP', currency: "XRP",
value: balances.xrp value: balances.xrp,
} };
result.unshift(xrpBalance) result.unshift(xrpBalance);
} }
if (options.limit && result.length > options.limit) { if (options.limit && result.length > options.limit) {
const toRemove = result.length - options.limit const toRemove = result.length - options.limit;
result.splice(-toRemove, toRemove) result.splice(-toRemove, toRemove);
} }
return result return result;
} }
function getLedgerVersionHelper( function getLedgerVersionHelper(
@@ -44,12 +48,14 @@ function getLedgerVersionHelper(
optionValue?: number optionValue?: number
): Promise<number> { ): Promise<number> {
if (optionValue != null && optionValue !== null) { if (optionValue != null && optionValue !== null) {
return Promise.resolve(optionValue) return Promise.resolve(optionValue);
} }
return connection.request({ return connection
command: 'ledger', .request({
ledger_index: 'validated' command: "ledger",
}).then(response => response.result.ledger_index); ledger_index: "validated",
})
.then((response) => response.result.ledger_index);
} }
function getBalances( function getBalances(
@@ -57,26 +63,23 @@ function getBalances(
address: string, address: string,
options: GetTrustlinesOptions = {} options: GetTrustlinesOptions = {}
): Promise<GetBalances> { ): Promise<GetBalances> {
validate.getTrustlines({address, options}) validate.getTrustlines({ address, options });
// Only support retrieving balances without a tag, // Only support retrieving balances without a tag,
// since we currently do not calculate balances // since we currently do not calculate balances
// on a per-tag basis. Apps must interpret and // on a per-tag basis. Apps must interpret and
// use tags independent of the XRP Ledger, comparing // use tags independent of the XRP Ledger, comparing
// with the XRP Ledger's balance as an accounting check. // with the XRP Ledger's balance as an accounting check.
address = ensureClassicAddress(address) address = ensureClassicAddress(address);
return Promise.all([ return Promise.all([
getLedgerVersionHelper( getLedgerVersionHelper(this.connection, options.ledgerVersion).then(
this.connection, (ledgerVersion) => utils.getXRPBalance(this, address, ledgerVersion)
options.ledgerVersion
).then((ledgerVersion) =>
utils.getXRPBalance(this, address, ledgerVersion)
), ),
this.getTrustlines(address, options) this.getTrustlines(address, options),
]).then((results) => ]).then((results) =>
formatBalances(options, {xrp: results[0], trustlines: results[1]}) formatBalances(options, { xrp: results[0], trustlines: results[1] })
) );
} }
export default getBalances export default getBalances;

View File

@@ -1,45 +1,46 @@
import _ from 'lodash' import BigNumber from "bignumber.js";
import * as utils from './utils' import _ from "lodash";
import type { Client } from "../client";
import type { BookOffer } from "../common/types/commands";
import type { Issue } from "../common/types/objects";
import { import {
parseOrderbookOrder, parseOrderbookOrder,
FormattedOrderbookOrder FormattedOrderbookOrder,
} from './parse/orderbook-order' } from "./parse/orderbook-order";
import {validate} from '../common' import * as utils from "./utils";
import {Issue} from '../common/types/objects'
import {BookOffer} from '../common/types/commands'
import {Client} from '..'
import BigNumber from 'bignumber.js'
export type FormattedOrderbook = { export interface FormattedOrderbook {
bids: FormattedOrderbookOrder[] bids: FormattedOrderbookOrder[];
asks: FormattedOrderbookOrder[] asks: FormattedOrderbookOrder[];
} }
function isSameIssue(a: Issue, b: Issue) { function isSameIssue(a: Issue, b: Issue) {
return a.currency === b.currency && a.counterparty === b.counterparty return a.currency === b.currency && a.counterparty === b.counterparty;
} }
function directionFilter(direction: string, order: FormattedOrderbookOrder) { function directionFilter(direction: string, order: FormattedOrderbookOrder) {
return order.specification.direction === direction return order.specification.direction === direction;
} }
function flipOrder(order: FormattedOrderbookOrder) { function flipOrder(order: FormattedOrderbookOrder) {
const specification = order.specification const specification = order.specification;
const flippedSpecification = { const flippedSpecification = {
quantity: specification.totalPrice, quantity: specification.totalPrice,
totalPrice: specification.quantity, totalPrice: specification.quantity,
direction: specification.direction === 'buy' ? 'sell' : 'buy' direction: specification.direction === "buy" ? "sell" : "buy",
} };
const newSpecification = _.merge({}, specification, flippedSpecification) const newSpecification = _.merge({}, specification, flippedSpecification);
return _.merge({}, order, {specification: newSpecification}) return _.merge({}, order, { specification: newSpecification });
} }
function alignOrder( function alignOrder(
base: Issue, base: Issue,
order: FormattedOrderbookOrder order: FormattedOrderbookOrder
): FormattedOrderbookOrder { ): FormattedOrderbookOrder {
const quantity = order.specification.quantity const quantity = order.specification.quantity;
return isSameIssue(quantity, base) ? order : flipOrder(order) return isSameIssue(quantity, base) ? order : flipOrder(order);
} }
export function formatBidsAndAsks( export function formatBidsAndAsks(
@@ -58,14 +59,17 @@ export function formatBidsAndAsks(
// we sort the orders so that earlier orders are closer to mid-market // we sort the orders so that earlier orders are closer to mid-market
const orders = offers const orders = offers
.sort((a, b) => { .sort((a, b) => {
return new BigNumber(a.quality).comparedTo(b.quality) const qualityA = a.quality ?? 0;
}) const qualityB = b.quality ?? 0;
.map(parseOrderbookOrder)
const alignedOrders = orders.map(_.partial(alignOrder, orderbook.base)) return new BigNumber(qualityA).comparedTo(qualityB);
const bids = alignedOrders.filter(_.partial(directionFilter, 'buy')) })
const asks = alignedOrders.filter(_.partial(directionFilter, 'sell')) .map(parseOrderbookOrder);
return {bids, asks}
const alignedOrders = orders.map(_.partial(alignOrder, orderbook.base));
const bids = alignedOrders.filter(_.partial(directionFilter, "buy"));
const asks = alignedOrders.filter(_.partial(directionFilter, "sell"));
return { bids, asks };
} }
// account is to specify a "perspective", which affects which unfunded offers // account is to specify a "perspective", which affects which unfunded offers
@@ -79,25 +83,26 @@ async function makeRequest(
) { ) {
const orderData = utils.renameCounterpartyToIssuerInOrder({ const orderData = utils.renameCounterpartyToIssuerInOrder({
taker_gets: takerGets, taker_gets: takerGets,
taker_pays: takerPays taker_pays: takerPays,
}) });
return client.requestAll({command: 'book_offers', return client.requestAll({
command: "book_offers",
taker_gets: orderData.taker_gets, taker_gets: orderData.taker_gets,
taker_pays: orderData.taker_pays, taker_pays: orderData.taker_pays,
ledger_index: options.ledgerVersion || 'validated', ledger_index: options.ledgerVersion || "validated",
limit: options.limit, limit: options.limit,
taker taker,
}) });
} }
export type GetOrderbookOptions = { export interface GetOrderbookOptions {
limit?: number limit?: number;
ledgerVersion?: number ledgerVersion?: number;
} }
export type OrderbookInfo = { export interface OrderbookInfo {
base: Issue base: Issue;
counter: Issue counter: Issue;
} }
export async function getOrderbook( export async function getOrderbook(
@@ -106,21 +111,19 @@ export async function getOrderbook(
orderbook: OrderbookInfo, orderbook: OrderbookInfo,
options: GetOrderbookOptions = {} options: GetOrderbookOptions = {}
): Promise<FormattedOrderbook> { ): Promise<FormattedOrderbook> {
// 1. Validate
validate.getOrderbook({address, orderbook, options})
// 2. Make Request // 2. Make Request
const [directOfferResults, reverseOfferResults] = await Promise.all([ const [directOfferResults, reverseOfferResults] = await Promise.all([
makeRequest(this, address, options, orderbook.base, orderbook.counter), makeRequest(this, address, options, orderbook.base, orderbook.counter),
makeRequest(this, address, options, orderbook.counter, orderbook.base) makeRequest(this, address, options, orderbook.counter, orderbook.base),
]) ]);
// 3. Return Formatted Response // 3. Return Formatted Response
const directOffers = _.flatMap( const directOffers = _.flatMap(
directOfferResults, directOfferResults,
(directOfferResult) => directOfferResult.result.offers (directOfferResult) => directOfferResult.result.offers
) );
const reverseOffers = _.flatMap( const reverseOffers = _.flatMap(
reverseOfferResults, reverseOfferResults,
(reverseOfferResult) => reverseOfferResult.result.offers (reverseOfferResult) => reverseOfferResult.result.offers
) );
return formatBidsAndAsks(orderbook, [...directOffers, ...reverseOffers]) return formatBidsAndAsks(orderbook, [...directOffers, ...reverseOffers]);
} }

View File

@@ -1,25 +1,28 @@
import * as assert from 'assert' import * as assert from "assert";
import {removeUndefined} from '../../utils'
import {classicAddressToXAddress} from 'ripple-address-codec'
import {parseMemos} from './utils'
export type FormattedAccountDelete = { import { classicAddressToXAddress } from "ripple-address-codec";
import { removeUndefined } from "../../utils";
import { parseMemos } from "./utils";
export interface FormattedAccountDelete {
// account (address) of an account to receive any leftover XRP after deleting the sending account. // account (address) of an account to receive any leftover XRP after deleting the sending account.
// Must be a funded account in the ledger, and must not be the sending account. // Must be a funded account in the ledger, and must not be the sending account.
destination: string destination: string;
// (Optional) Arbitrary destination tag that identifies a hosted recipient or other information // (Optional) Arbitrary destination tag that identifies a hosted recipient or other information
// for the recipient of the deleted account's leftover XRP. NB: Ensure that the hosted recipient is // for the recipient of the deleted account's leftover XRP. NB: Ensure that the hosted recipient is
// able to account for AccountDelete transactions; if not, your balance may not be properly credited. // able to account for AccountDelete transactions; if not, your balance may not be properly credited.
destinationTag?: number destinationTag?: number;
// X-address of an account to receive any leftover XRP after deleting the sending account. // X-address of an account to receive any leftover XRP after deleting the sending account.
// Must be a funded account in the ledger, and must not be the sending account. // Must be a funded account in the ledger, and must not be the sending account.
destinationXAddress: string destinationXAddress: string;
} }
function parseAccountDelete(tx: any): FormattedAccountDelete { function parseAccountDelete(tx: any): FormattedAccountDelete {
assert.ok(tx.TransactionType === 'AccountDelete') assert.ok(tx.TransactionType === "AccountDelete");
return removeUndefined({ return removeUndefined({
memos: parseMemos(tx), memos: parseMemos(tx),
@@ -29,8 +32,8 @@ function parseAccountDelete(tx: any): FormattedAccountDelete {
tx.Destination, tx.Destination,
tx.DestinationTag == null ? false : tx.DestinationTag, tx.DestinationTag == null ? false : tx.DestinationTag,
false false
) ),
}) });
} }
export default parseAccountDelete export default parseAccountDelete;

View File

@@ -1,23 +1,25 @@
import BigNumber from 'bignumber.js' import BigNumber from "bignumber.js";
import parseAmount from './amount'
import {parseTimestamp, adjustQualityForXRP} from './utils'
import {removeUndefined} from '../../utils'
import {orderFlags} from './flags'
import {FormattedOrderSpecification} from '../../common/types/objects'
export type FormattedAccountOrder = { import { FormattedOrderSpecification } from "../../common/types/objects";
specification: FormattedOrderSpecification import { removeUndefined } from "../../utils";
import parseAmount from "./amount";
import { orderFlags } from "./flags";
import { parseTimestamp, adjustQualityForXRP } from "./utils";
export interface FormattedAccountOrder {
specification: FormattedOrderSpecification;
properties: { properties: {
maker: string maker: string;
sequence: number sequence: number;
makerExchangeRate: string makerExchangeRate: string;
} };
} }
// TODO: remove this function once rippled provides quality directly // TODO: remove this function once rippled provides quality directly
function computeQuality(takerGets, takerPays) { function computeQuality(takerGets, takerPays) {
const quotient = new BigNumber(takerPays.value).dividedBy(takerGets.value) const quotient = new BigNumber(takerPays.value).dividedBy(takerGets.value);
return quotient.precision(16, BigNumber.ROUND_HALF_UP).toString() return quotient.precision(16, BigNumber.ROUND_HALF_UP).toString();
} }
// rippled 'account_offers' returns a different format for orders than 'tx' // rippled 'account_offers' returns a different format for orders than 'tx'
@@ -26,22 +28,22 @@ export function parseAccountOrder(
address: string, address: string,
order: any order: any
): FormattedAccountOrder { ): FormattedAccountOrder {
const direction = (order.flags & orderFlags.Sell) === 0 ? 'buy' : 'sell' const direction = (order.flags & orderFlags.Sell) === 0 ? "buy" : "sell";
const takerGetsAmount = parseAmount(order.taker_gets) const takerGetsAmount = parseAmount(order.taker_gets);
const takerPaysAmount = parseAmount(order.taker_pays) const takerPaysAmount = parseAmount(order.taker_pays);
const quantity = direction === 'buy' ? takerPaysAmount : takerGetsAmount const quantity = direction === "buy" ? takerPaysAmount : takerGetsAmount;
const totalPrice = direction === 'buy' ? takerGetsAmount : takerPaysAmount const totalPrice = direction === "buy" ? takerGetsAmount : takerPaysAmount;
// note: immediateOrCancel and fillOrKill orders cannot enter the order book // note: immediateOrCancel and fillOrKill orders cannot enter the order book
// so we can omit those flags here // so we can omit those flags here
const specification = removeUndefined({ const specification = removeUndefined({
direction: direction, direction,
quantity: quantity, quantity,
totalPrice: totalPrice, totalPrice,
passive: (order.flags & orderFlags.Passive) !== 0 || undefined, passive: (order.flags & orderFlags.Passive) !== 0 || undefined,
// rippled currently does not provide "expiration" in account_offers // rippled currently does not provide "expiration" in account_offers
expirationTime: parseTimestamp(order.expiration) expirationTime: parseTimestamp(order.expiration),
}) });
const makerExchangeRate = order.quality const makerExchangeRate = order.quality
? adjustQualityForXRP( ? adjustQualityForXRP(
@@ -49,12 +51,12 @@ export function parseAccountOrder(
takerGetsAmount.currency, takerGetsAmount.currency,
takerPaysAmount.currency takerPaysAmount.currency
) )
: computeQuality(takerGetsAmount, takerPaysAmount) : computeQuality(takerGetsAmount, takerPaysAmount);
const properties = { const properties = {
maker: address, maker: address,
sequence: order.seq, sequence: order.seq,
makerExchangeRate: makerExchangeRate makerExchangeRate,
} };
return {specification, properties} return { specification, properties };
} }

View File

@@ -1,9 +1,10 @@
import {parseQuality} from './utils'
import {removeUndefined} from '../../utils'
import { import {
Trustline, Trustline,
FormattedTrustline FormattedTrustline,
} from '../../common/types/objects/trustlines' } from "../../common/types/objects/trustlines";
import { removeUndefined } from "../../utils";
import { parseQuality } from "./utils";
// rippled 'account_lines' returns a different format for // rippled 'account_lines' returns a different format for
// trustlines than 'tx' // trustlines than 'tx'
@@ -16,19 +17,19 @@ function parseAccountTrustline(trustline: Trustline): FormattedTrustline {
qualityOut: parseQuality(trustline.quality_out) || undefined, qualityOut: parseQuality(trustline.quality_out) || undefined,
ripplingDisabled: trustline.no_ripple, ripplingDisabled: trustline.no_ripple,
frozen: trustline.freeze, frozen: trustline.freeze,
authorized: trustline.authorized authorized: trustline.authorized,
}) });
// rippled doesn't provide the counterparty's qualities // rippled doesn't provide the counterparty's qualities
const counterparty = removeUndefined({ const counterparty = removeUndefined({
limit: trustline.limit_peer, limit: trustline.limit_peer,
ripplingDisabled: trustline.no_ripple_peer, ripplingDisabled: trustline.no_ripple_peer,
frozen: trustline.freeze_peer, frozen: trustline.freeze_peer,
authorized: trustline.peer_authorized authorized: trustline.peer_authorized,
}) });
const state = { const state = {
balance: trustline.balance balance: trustline.balance,
} };
return {specification, counterparty, state} return { specification, counterparty, state };
} }
export default parseAccountTrustline export default parseAccountTrustline;

View File

@@ -1,7 +1,7 @@
function parseAmendment(tx: any) { function parseAmendment(tx: any) {
return { return {
amendment: tx.Amendment amendment: tx.Amendment,
} };
} }
export default parseAmendment export default parseAmendment;

View File

@@ -1,18 +1,18 @@
import {Amount, RippledAmount} from '../../common/types/objects' import { Amount, RippledAmount } from "../../common/types/objects";
import {dropsToXrp} from '../../utils' import { dropsToXrp } from "../../utils";
function parseAmount(amount: RippledAmount): Amount { function parseAmount(amount: RippledAmount): Amount {
if (typeof amount === 'string') { if (typeof amount === "string") {
return { return {
currency: 'XRP', currency: "XRP",
value: dropsToXrp(amount) value: dropsToXrp(amount),
} };
} }
return { return {
currency: amount.currency, currency: amount.currency,
value: amount.value, value: amount.value,
counterparty: amount.issuer counterparty: amount.issuer,
} };
} }
export default parseAmount export default parseAmount;

View File

@@ -1,12 +1,13 @@
import * as assert from 'assert' import * as assert from "assert";
import {parseMemos} from './utils'
import { parseMemos } from "./utils";
function parseOrderCancellation(tx: any): object { function parseOrderCancellation(tx: any): object {
assert.ok(tx.TransactionType === 'OfferCancel') assert.ok(tx.TransactionType === "OfferCancel");
return { return {
memos: parseMemos(tx), memos: parseMemos(tx),
orderSequence: tx.OfferSequence orderSequence: tx.OfferSequence,
} };
} }
export default parseOrderCancellation export default parseOrderCancellation;

View File

@@ -1,19 +1,21 @@
import * as assert from 'assert' import * as assert from "assert";
import {removeUndefined} from '../../utils'
import {parseMemos} from './utils'
export type FormattedCheckCancel = { import { removeUndefined } from "../../utils";
import { parseMemos } from "./utils";
export interface FormattedCheckCancel {
// ID of the Check ledger object to cancel. // ID of the Check ledger object to cancel.
checkID: string checkID: string;
} }
function parseCheckCancel(tx: any): FormattedCheckCancel { function parseCheckCancel(tx: any): FormattedCheckCancel {
assert.ok(tx.TransactionType === 'CheckCancel') assert.ok(tx.TransactionType === "CheckCancel");
return removeUndefined({ return removeUndefined({
memos: parseMemos(tx), memos: parseMemos(tx),
checkID: tx.CheckID checkID: tx.CheckID,
}) });
} }
export default parseCheckCancel export default parseCheckCancel;

View File

@@ -1,36 +1,38 @@
import * as assert from 'assert' import * as assert from "assert";
import {removeUndefined} from '../../utils'
import parseAmount from './amount'
import {Amount} from '../../common/types/objects'
import {parseMemos} from './utils'
export type FormattedCheckCash = { import { Amount } from "../../common/types/objects";
import { removeUndefined } from "../../utils";
import parseAmount from "./amount";
import { parseMemos } from "./utils";
export interface FormattedCheckCash {
// ID of the Check ledger object to cash. // ID of the Check ledger object to cash.
checkID: string checkID: string;
// (Optional) redeem the Check for exactly this amount, if possible. // (Optional) redeem the Check for exactly this amount, if possible.
// The currency must match that of the `SendMax` of the corresponding // The currency must match that of the `SendMax` of the corresponding
// `CheckCreate` transaction. // `CheckCreate` transaction.
amount: Amount amount: Amount;
// (Optional) redeem the Check for at least this amount and // (Optional) redeem the Check for at least this amount and
// for as much as possible. // for as much as possible.
// The currency must match that of the `SendMax` of the corresponding // The currency must match that of the `SendMax` of the corresponding
// `CheckCreate` transaction. // `CheckCreate` transaction.
deliverMin: Amount deliverMin: Amount;
// *must* include either Amount or DeliverMin, but not both. // *must* include either Amount or DeliverMin, but not both.
} }
function parseCheckCash(tx: any): FormattedCheckCash { function parseCheckCash(tx: any): FormattedCheckCash {
assert.ok(tx.TransactionType === 'CheckCash') assert.ok(tx.TransactionType === "CheckCash");
return removeUndefined({ return removeUndefined({
memos: parseMemos(tx), memos: parseMemos(tx),
checkID: tx.CheckID, checkID: tx.CheckID,
amount: tx.Amount && parseAmount(tx.Amount), amount: tx.Amount && parseAmount(tx.Amount),
deliverMin: tx.DeliverMin && parseAmount(tx.DeliverMin) deliverMin: tx.DeliverMin && parseAmount(tx.DeliverMin),
}) });
} }
export default parseCheckCash export default parseCheckCash;

View File

@@ -1,30 +1,31 @@
import * as assert from 'assert' import * as assert from "assert";
import {parseTimestamp} from './utils'
import {removeUndefined} from '../../utils'
import parseAmount from './amount'
import {Amount} from '../../common/types/objects'
import {parseMemos} from './utils'
export type FormattedCheckCreate = { import { Amount } from "../../common/types/objects";
import { removeUndefined } from "../../utils";
import parseAmount from "./amount";
import { parseTimestamp, parseMemos } from "./utils";
export interface FormattedCheckCreate {
// account that can cash the check. // account that can cash the check.
destination: string destination: string;
// amount the check is allowed to debit the sender, // amount the check is allowed to debit the sender,
// including transfer fees on non-XRP currencies. // including transfer fees on non-XRP currencies.
sendMax: Amount sendMax: Amount;
// (Optional) identifies the reason for the check, or a hosted recipient. // (Optional) identifies the reason for the check, or a hosted recipient.
destinationTag?: string destinationTag?: string;
// (Optional) time in seconds since the Ripple Epoch. // (Optional) time in seconds since the Ripple Epoch.
expiration?: string expiration?: string;
// (Optional) 256-bit hash representing a specific reason or identifier. // (Optional) 256-bit hash representing a specific reason or identifier.
invoiceID?: string invoiceID?: string;
} }
function parseCheckCreate(tx: any): FormattedCheckCreate { function parseCheckCreate(tx: any): FormattedCheckCreate {
assert.ok(tx.TransactionType === 'CheckCreate') assert.ok(tx.TransactionType === "CheckCreate");
return removeUndefined({ return removeUndefined({
memos: parseMemos(tx), memos: parseMemos(tx),
@@ -32,8 +33,8 @@ function parseCheckCreate(tx: any): FormattedCheckCreate {
sendMax: parseAmount(tx.SendMax), sendMax: parseAmount(tx.SendMax),
destinationTag: tx.DestinationTag, destinationTag: tx.DestinationTag,
expiration: tx.Expiration && parseTimestamp(tx.Expiration), expiration: tx.Expiration && parseTimestamp(tx.Expiration),
invoiceID: tx.InvoiceID invoiceID: tx.InvoiceID,
}) });
} }
export default parseCheckCreate export default parseCheckCreate;

View File

@@ -1,23 +1,25 @@
import * as assert from 'assert' import * as assert from "assert";
import {removeUndefined} from '../../utils'
import {parseMemos} from './utils'
export type FormattedDepositPreauth = { import { removeUndefined } from "../../utils";
import { parseMemos } from "./utils";
export interface FormattedDepositPreauth {
// account (address) of the sender to preauthorize // account (address) of the sender to preauthorize
authorize: string authorize: string;
// account (address) of the sender whose preauthorization should be revoked // account (address) of the sender whose preauthorization should be revoked
unauthorize: string unauthorize: string;
} }
function parseDepositPreauth(tx: any): FormattedDepositPreauth { function parseDepositPreauth(tx: any): FormattedDepositPreauth {
assert.ok(tx.TransactionType === 'DepositPreauth') assert.ok(tx.TransactionType === "DepositPreauth");
return removeUndefined({ return removeUndefined({
memos: parseMemos(tx), memos: parseMemos(tx),
authorize: tx.Authorize, authorize: tx.Authorize,
unauthorize: tx.Unauthorize unauthorize: tx.Unauthorize,
}) });
} }
export default parseDepositPreauth export default parseDepositPreauth;

View File

@@ -1,15 +1,17 @@
import * as assert from 'assert' import * as assert from "assert";
import {parseMemos} from './utils'
import {removeUndefined} from '../../utils' import { removeUndefined } from "../../utils";
import { parseMemos } from "./utils";
function parseEscrowCancellation(tx: any): object { function parseEscrowCancellation(tx: any): object {
assert.ok(tx.TransactionType === 'EscrowCancel') assert.ok(tx.TransactionType === "EscrowCancel");
return removeUndefined({ return removeUndefined({
memos: parseMemos(tx), memos: parseMemos(tx),
owner: tx.Owner, owner: tx.Owner,
escrowSequence: tx.OfferSequence escrowSequence: tx.OfferSequence,
}) });
} }
export default parseEscrowCancellation export default parseEscrowCancellation;

View File

@@ -1,10 +1,12 @@
import * as assert from 'assert' import * as assert from "assert";
import parseAmount from './amount'
import {parseTimestamp, parseMemos} from './utils' import { removeUndefined } from "../../utils";
import {removeUndefined} from '../../utils'
import parseAmount from "./amount";
import { parseTimestamp, parseMemos } from "./utils";
function parseEscrowCreation(tx: any): object { function parseEscrowCreation(tx: any): object {
assert.ok(tx.TransactionType === 'EscrowCreate') assert.ok(tx.TransactionType === "EscrowCreate");
return removeUndefined({ return removeUndefined({
amount: parseAmount(tx.Amount).value, amount: parseAmount(tx.Amount).value,
@@ -14,8 +16,8 @@ function parseEscrowCreation(tx: any): object {
allowCancelAfter: parseTimestamp(tx.CancelAfter), allowCancelAfter: parseTimestamp(tx.CancelAfter),
allowExecuteAfter: parseTimestamp(tx.FinishAfter), allowExecuteAfter: parseTimestamp(tx.FinishAfter),
sourceTag: tx.SourceTag, sourceTag: tx.SourceTag,
destinationTag: tx.DestinationTag destinationTag: tx.DestinationTag,
}) });
} }
export default parseEscrowCreation export default parseEscrowCreation;

View File

@@ -1,17 +1,19 @@
import * as assert from 'assert' import * as assert from "assert";
import {parseMemos} from './utils'
import {removeUndefined} from '../../utils' import { removeUndefined } from "../../utils";
import { parseMemos } from "./utils";
function parseEscrowExecution(tx: any): object { function parseEscrowExecution(tx: any): object {
assert.ok(tx.TransactionType === 'EscrowFinish') assert.ok(tx.TransactionType === "EscrowFinish");
return removeUndefined({ return removeUndefined({
memos: parseMemos(tx), memos: parseMemos(tx),
owner: tx.Owner, owner: tx.Owner,
escrowSequence: tx.OfferSequence, escrowSequence: tx.OfferSequence,
condition: tx.Condition, condition: tx.Condition,
fulfillment: tx.Fulfillment fulfillment: tx.Fulfillment,
}) });
} }
export default parseEscrowExecution export default parseEscrowExecution;

View File

@@ -1,16 +1,18 @@
import BigNumber from 'bignumber.js' import BigNumber from "bignumber.js";
import {dropsToXrp} from '../../utils'
import {parseMemos} from './utils' import { dropsToXrp } from "../../utils";
import { parseMemos } from "./utils";
function parseFeeUpdate(tx: any) { function parseFeeUpdate(tx: any) {
const baseFeeDrops = new BigNumber(tx.BaseFee, 16).toString() const baseFeeDrops = new BigNumber(tx.BaseFee, 16).toString();
return { return {
memos: parseMemos(tx), memos: parseMemos(tx),
baseFeeXRP: dropsToXrp(baseFeeDrops), baseFeeXRP: dropsToXrp(baseFeeDrops),
referenceFeeUnits: tx.ReferenceFeeUnits, referenceFeeUnits: tx.ReferenceFeeUnits,
reserveBaseXRP: dropsToXrp(tx.ReserveBase), reserveBaseXRP: dropsToXrp(tx.ReserveBase),
reserveIncrementXRP: dropsToXrp(tx.ReserveIncrement) reserveIncrementXRP: dropsToXrp(tx.ReserveIncrement),
} };
} }
export default parseFeeUpdate export default parseFeeUpdate;

View File

@@ -1,52 +1,54 @@
import _ from 'lodash' import BigNumber from "bignumber.js";
import BigNumber from 'bignumber.js' import _ from "lodash";
import {constants} from '../../common'
const AccountFields = constants.AccountFields import { constants } from "../../common";
const AccountFields = constants.AccountFields;
function parseField(info, value) { function parseField(info, value) {
if (info.encoding === 'hex' && !info.length) { if (info.encoding === "hex" && !info.length) {
// e.g. "domain" // e.g. "domain"
return Buffer.from(value, 'hex').toString('ascii') return Buffer.from(value, "hex").toString("ascii");
} }
if (info.shift) { if (info.shift) {
return new BigNumber(value).shiftedBy(-info.shift).toNumber() return new BigNumber(value).shiftedBy(-info.shift).toNumber();
} }
return value return value;
} }
function parseFields(data: any): object { function parseFields(data: any): object {
const settings: any = {} const settings: any = {};
for (const fieldName in AccountFields) { for (const fieldName in AccountFields) {
const fieldValue = data[fieldName] const fieldValue = data[fieldName];
if (fieldValue != null) { if (fieldValue != null) {
const info = AccountFields[fieldName] const info = AccountFields[fieldName];
settings[info.name] = parseField(info, fieldValue) settings[info.name] = parseField(info, fieldValue);
} }
} }
if (data.RegularKey) { if (data.RegularKey) {
settings.regularKey = data.RegularKey settings.regularKey = data.RegularKey;
} }
// Since an account can own at most one SignerList, // Since an account can own at most one SignerList,
// this array must have exactly one member if it is present. // this array must have exactly one member if it is present.
if (data.signer_lists && data.signer_lists.length === 1) { if (data.signer_lists && data.signer_lists.length === 1) {
settings.signers = {} settings.signers = {};
if (data.signer_lists[0].SignerQuorum) { if (data.signer_lists[0].SignerQuorum) {
settings.signers.threshold = data.signer_lists[0].SignerQuorum settings.signers.threshold = data.signer_lists[0].SignerQuorum;
} }
if (data.signer_lists[0].SignerEntries) { if (data.signer_lists[0].SignerEntries) {
settings.signers.weights = data.signer_lists[0].SignerEntries.map( settings.signers.weights = data.signer_lists[0].SignerEntries.map(
(entry: any) => { (entry: any) => {
return { return {
address: entry.SignerEntry.Account, address: entry.SignerEntry.Account,
weight: entry.SignerEntry.SignerWeight weight: entry.SignerEntry.SignerWeight,
} };
} }
) );
} }
} }
return settings return settings;
} }
export default parseFields export default parseFields;

View File

@@ -1,7 +1,7 @@
const orderFlags = { const orderFlags = {
Passive: 0x00010000, Passive: 0x00010000,
Sell: 0x00020000 // offer was placed as a sell Sell: 0x00020000, // offer was placed as a sell
} };
const trustlineFlags = { const trustlineFlags = {
LowReserve: 0x00010000, // entry counts toward reserve LowReserve: 0x00010000, // entry counts toward reserve
@@ -11,7 +11,7 @@ const trustlineFlags = {
LowNoRipple: 0x00100000, LowNoRipple: 0x00100000,
HighNoRipple: 0x00200000, HighNoRipple: 0x00200000,
LowFreeze: 0x00400000, LowFreeze: 0x00400000,
HighFreeze: 0x00800000 HighFreeze: 0x00800000,
} };
export {orderFlags, trustlineFlags} export { orderFlags, trustlineFlags };

View File

@@ -1,87 +1,92 @@
import _ from 'lodash' import _ from "lodash";
import {removeUndefined, rippleTimeToISOTime} from '../../utils'
import parseTransaction from './transaction'
import { TransactionAndMetadata } from '../../models/transactions'
export type FormattedLedger = { import { TransactionAndMetadata } from "../../models/transactions";
import { removeUndefined, rippleTimeToISOTime } from "../../utils";
import parseTransaction from "./transaction";
export interface FormattedLedger {
// TODO: properties in type don't match response object. Fix! // TODO: properties in type don't match response object. Fix!
// closed: boolean, // closed: boolean,
stateHash: string stateHash: string;
closeTime: string closeTime: string;
closeTimeResolution: number closeTimeResolution: number;
closeFlags: number closeFlags: number;
ledgerHash: string ledgerHash: string;
ledgerVersion: number ledgerVersion: number;
parentLedgerHash: string parentLedgerHash: string;
parentCloseTime: string parentCloseTime: string;
totalDrops: string totalDrops: string;
transactionHash: string transactionHash: string;
transactions?: Array<object> transactions?: object[];
transactionHashes?: Array<string> transactionHashes?: string[];
rawState?: string rawState?: string;
stateHashes?: Array<string> stateHashes?: string[];
} }
function parseTransactionWrapper(ledgerVersion: number, tx: TransactionAndMetadata) { function parseTransactionWrapper(
ledgerVersion: number,
tx: TransactionAndMetadata
) {
// renames metaData to meta and adds ledger_index // renames metaData to meta and adds ledger_index
const transaction = Object.assign({}, _.omit(tx, 'metadata'), { const transaction = {
..._.omit(tx, "metadata"),
meta: tx.metadata, meta: tx.metadata,
ledger_index: ledgerVersion ledger_index: ledgerVersion,
}) };
const result = parseTransaction(transaction, true) const result = parseTransaction(transaction, true);
if (!result.outcome.ledgerVersion) { if (!result.outcome.ledgerVersion) {
result.outcome.ledgerVersion = ledgerVersion result.outcome.ledgerVersion = ledgerVersion;
} }
return result return result;
} }
function parseTransactions(transactions: string[] | TransactionAndMetadata[], ledgerVersion: number) { function parseTransactions(
transactions: string[] | TransactionAndMetadata[],
ledgerVersion: number
) {
if (_.isEmpty(transactions)) { if (_.isEmpty(transactions)) {
return {} return {};
} }
if (typeof transactions[0] === 'string') { if (typeof transactions[0] === "string") {
return {transactionHashes: transactions as unknown as string[]} return { transactionHashes: transactions as unknown as string[] };
} }
return { return {
transactions: (transactions as unknown as TransactionAndMetadata[]).map( transactions: (transactions as unknown as TransactionAndMetadata[]).map(
_.partial(parseTransactionWrapper, ledgerVersion) _.partial(parseTransactionWrapper, ledgerVersion)
) ),
} };
} }
function parseState(state) { function parseState(state) {
if (_.isEmpty(state)) { if (_.isEmpty(state)) {
return {} return {};
} }
if (typeof state[0] === 'string') { if (typeof state[0] === "string") {
return {stateHashes: state} return { stateHashes: state };
} }
return {rawState: JSON.stringify(state)} return { rawState: JSON.stringify(state) };
} }
/** /**
* @param {Ledger} ledger must be a *closed* ledger with valid `close_time` and `parent_close_time` * @param ledger - Must be a *closed* ledger with valid `close_time` and `parent_close_time`.
* @returns {FormattedLedger} formatted ledger * @returns Formatted ledger.
* @throws RangeError: Invalid time value (rippleTimeToISOTime) * @throws RangeError: Invalid time value (rippleTimeToISOTime).
*/ */
export function parseLedger(ledger): FormattedLedger { export function parseLedger(ledger): FormattedLedger {
const ledgerVersion = parseInt(ledger.ledger_index, 10) const ledgerVersion = parseInt(ledger.ledger_index, 10);
return removeUndefined( return removeUndefined({
Object.assign( stateHash: ledger.account_hash,
{ closeTime: rippleTimeToISOTime(ledger.close_time),
stateHash: ledger.account_hash, closeTimeResolution: ledger.close_time_resolution,
closeTime: rippleTimeToISOTime(ledger.close_time), closeFlags: ledger.close_flags,
closeTimeResolution: ledger.close_time_resolution, ledgerHash: ledger.ledger_hash,
closeFlags: ledger.close_flags, ledgerVersion,
ledgerHash: ledger.ledger_hash, parentLedgerHash: ledger.parent_hash,
ledgerVersion: ledgerVersion, parentCloseTime: rippleTimeToISOTime(ledger.parent_close_time),
parentLedgerHash: ledger.parent_hash, totalDrops: ledger.total_coins,
parentCloseTime: rippleTimeToISOTime(ledger.parent_close_time), transactionHash: ledger.transaction_hash,
totalDrops: ledger.total_coins, ...parseTransactions(ledger.transactions, ledgerVersion),
transactionHash: ledger.transaction_hash ...parseState(ledger.accountState),
}, });
parseTransactions(ledger.transactions, ledgerVersion),
parseState(ledger.accountState)
)
)
} }

View File

@@ -1,35 +1,36 @@
import * as assert from 'assert' import * as assert from "assert";
import {parseTimestamp} from './utils'
import {parseMemos} from './utils' import { txFlags } from "../../common";
import parseAmount from './amount'
import {removeUndefined} from '../../utils'
import {txFlags} from '../../common'
import { import {
FormattedOrderSpecification, FormattedOrderSpecification,
OfferCreateTransaction OfferCreateTransaction,
} from '../../common/types/objects/index' } from "../../common/types/objects/index";
import { removeUndefined } from "../../utils";
const flags = txFlags.OfferCreate import parseAmount from "./amount";
import { parseTimestamp, parseMemos } from "./utils";
const flags = txFlags.OfferCreate;
function parseOrder(tx: OfferCreateTransaction): FormattedOrderSpecification { function parseOrder(tx: OfferCreateTransaction): FormattedOrderSpecification {
assert.ok(tx.TransactionType === 'OfferCreate') assert.ok(tx.TransactionType === "OfferCreate");
const direction = (tx.Flags & flags.Sell) === 0 ? 'buy' : 'sell' const direction = (tx.Flags & flags.Sell) === 0 ? "buy" : "sell";
const takerGetsAmount = parseAmount(tx.TakerGets) const takerGetsAmount = parseAmount(tx.TakerGets);
const takerPaysAmount = parseAmount(tx.TakerPays) const takerPaysAmount = parseAmount(tx.TakerPays);
const quantity = direction === 'buy' ? takerPaysAmount : takerGetsAmount const quantity = direction === "buy" ? takerPaysAmount : takerGetsAmount;
const totalPrice = direction === 'buy' ? takerGetsAmount : takerPaysAmount const totalPrice = direction === "buy" ? takerGetsAmount : takerPaysAmount;
return removeUndefined({ return removeUndefined({
memos: parseMemos(tx), memos: parseMemos(tx),
direction: direction, direction,
quantity: quantity, quantity,
totalPrice: totalPrice, totalPrice,
passive: (tx.Flags & flags.Passive) !== 0 || undefined, passive: (tx.Flags & flags.Passive) !== 0 || undefined,
immediateOrCancel: (tx.Flags & flags.ImmediateOrCancel) !== 0 || undefined, immediateOrCancel: (tx.Flags & flags.ImmediateOrCancel) !== 0 || undefined,
fillOrKill: (tx.Flags & flags.FillOrKill) !== 0 || undefined, fillOrKill: (tx.Flags & flags.FillOrKill) !== 0 || undefined,
expirationTime: parseTimestamp(tx.Expiration) expirationTime: parseTimestamp(tx.Expiration),
}) });
} }
export default parseOrder export default parseOrder;

View File

@@ -1,42 +1,50 @@
import _ from 'lodash' import _ from "lodash";
import {parseTimestamp, adjustQualityForXRP} from './utils'
import {removeUndefined} from '../../utils'
import {orderFlags} from './flags' import { BookOffer } from "../../common/types/commands";
import parseAmount from './amount' import {
import {BookOffer} from '../../common/types/commands' Amount,
import {Amount, FormattedOrderSpecification} from '../../common/types/objects' FormattedOrderSpecification,
} from "../../common/types/objects";
import { removeUndefined } from "../../utils";
export type FormattedOrderbookOrder = { import parseAmount from "./amount";
specification: FormattedOrderSpecification import { orderFlags } from "./flags";
import { parseTimestamp, adjustQualityForXRP } from "./utils";
export interface FormattedOrderbookOrder {
specification: FormattedOrderSpecification;
properties: { properties: {
maker: string maker: string;
sequence: number sequence: number;
makerExchangeRate: string makerExchangeRate: string;
} };
state?: { state?: {
fundedAmount: Amount fundedAmount: Amount;
priceOfFundedAmount: Amount priceOfFundedAmount: Amount;
} };
data: BookOffer data: BookOffer;
} }
export function parseOrderbookOrder(data: BookOffer): FormattedOrderbookOrder { export function parseOrderbookOrder(data: BookOffer): FormattedOrderbookOrder {
const direction = (data.Flags & orderFlags.Sell) === 0 ? 'buy' : 'sell' const direction = (data.Flags & orderFlags.Sell) === 0 ? "buy" : "sell";
const takerGetsAmount = parseAmount(data.TakerGets) const takerGetsAmount = parseAmount(data.TakerGets);
const takerPaysAmount = parseAmount(data.TakerPays) const takerPaysAmount = parseAmount(data.TakerPays);
const quantity = direction === 'buy' ? takerPaysAmount : takerGetsAmount const quantity = direction === "buy" ? takerPaysAmount : takerGetsAmount;
const totalPrice = direction === 'buy' ? takerGetsAmount : takerPaysAmount const totalPrice = direction === "buy" ? takerGetsAmount : takerPaysAmount;
// note: immediateOrCancel and fillOrKill orders cannot enter the order book // note: immediateOrCancel and fillOrKill orders cannot enter the order book
// so we can omit those flags here // so we can omit those flags here
const specification: FormattedOrderSpecification = removeUndefined({ const specification: FormattedOrderSpecification = removeUndefined({
direction: direction, direction,
quantity: quantity, quantity,
totalPrice: totalPrice, totalPrice,
passive: (data.Flags & orderFlags.Passive) !== 0 || undefined, passive: (data.Flags & orderFlags.Passive) !== 0 || undefined,
expirationTime: parseTimestamp(data.Expiration) expirationTime: parseTimestamp(data.Expiration),
}) });
if (data.quality == null) {
throw new Error("parseOrderBookOrder: Could not find quality");
}
const properties = { const properties = {
maker: data.Account, maker: data.Account,
@@ -45,19 +53,25 @@ export function parseOrderbookOrder(data: BookOffer): FormattedOrderbookOrder {
data.quality, data.quality,
takerGetsAmount.currency, takerGetsAmount.currency,
takerPaysAmount.currency takerPaysAmount.currency
) ),
} };
const takerGetsFunded = data.taker_gets_funded const takerGetsFunded = data.taker_gets_funded
? parseAmount(data.taker_gets_funded) ? parseAmount(data.taker_gets_funded)
: undefined : undefined;
const takerPaysFunded = data.taker_pays_funded const takerPaysFunded = data.taker_pays_funded
? parseAmount(data.taker_pays_funded) ? parseAmount(data.taker_pays_funded)
: undefined : undefined;
const available = removeUndefined({ const available = removeUndefined({
fundedAmount: takerGetsFunded, fundedAmount: takerGetsFunded,
priceOfFundedAmount: takerPaysFunded priceOfFundedAmount: takerPaysFunded,
}) });
const state = _.isEmpty(available) ? undefined : available const state = _.isEmpty(available) ? undefined : available;
return removeUndefined({specification, properties, state, data})
return removeUndefined({
specification,
properties,
state,
data,
}) as FormattedOrderbookOrder;
} }

View File

@@ -1,31 +1,33 @@
import _ from 'lodash' import _ from "lodash";
import parseAmount from './amount'
import {Amount, RippledAmount} from '../../common/types/objects' import { Amount, RippledAmount } from "../../common/types/objects";
import {Path, GetPaths, RippledPathsResponse} from '../pathfind-types' import { Path, GetPaths, RippledPathsResponse } from "../pathfind-types";
import parseAmount from "./amount";
function parsePaths(paths) { function parsePaths(paths) {
return paths.map((steps) => return paths.map((steps) =>
steps.map((step) => _.omit(step, ['type', 'type_hex'])) steps.map((step) => _.omit(step, ["type", "type_hex"]))
) );
} }
function removeAnyCounterpartyEncoding(address: string, amount: Amount) { function removeAnyCounterpartyEncoding(address: string, amount: Amount) {
return amount.counterparty === address return amount.counterparty === address
? _.omit(amount, 'counterparty') ? _.omit(amount, "counterparty")
: amount : amount;
} }
function createAdjustment( function createAdjustment(
address: string, address: string,
adjustmentWithoutAddress: object adjustmentWithoutAddress: object
): any { ): any {
const amountKey = Object.keys(adjustmentWithoutAddress)[0] const amountKey = Object.keys(adjustmentWithoutAddress)[0];
const amount = adjustmentWithoutAddress[amountKey] const amount = adjustmentWithoutAddress[amountKey];
return _.set( return _.set(
{address: address}, { address },
amountKey, amountKey,
removeAnyCounterpartyEncoding(address, amount) removeAnyCounterpartyEncoding(address, amount)
) );
} }
function parseAlternative( function parseAlternative(
@@ -39,28 +41,30 @@ function parseAlternative(
const amounts = const amounts =
alternative.destination_amount != null alternative.destination_amount != null
? { ? {
source: {amount: parseAmount(alternative.source_amount)}, source: { amount: parseAmount(alternative.source_amount) },
destination: {minAmount: parseAmount(alternative.destination_amount)} destination: {
minAmount: parseAmount(alternative.destination_amount),
},
} }
: { : {
source: {maxAmount: parseAmount(alternative.source_amount)}, source: { maxAmount: parseAmount(alternative.source_amount) },
destination: {amount: parseAmount(destinationAmount)} destination: { amount: parseAmount(destinationAmount) },
} };
return { return {
source: createAdjustment(sourceAddress, amounts.source), source: createAdjustment(sourceAddress, amounts.source),
destination: createAdjustment(destinationAddress, amounts.destination), destination: createAdjustment(destinationAddress, amounts.destination),
paths: JSON.stringify(parsePaths(alternative.paths_computed)) paths: JSON.stringify(parsePaths(alternative.paths_computed)),
} };
} }
function parsePathfind(pathfindResult: RippledPathsResponse): GetPaths { function parsePathfind(pathfindResult: RippledPathsResponse): GetPaths {
const sourceAddress = pathfindResult.source_account const sourceAddress = pathfindResult.source_account;
const destinationAddress = pathfindResult.destination_account const destinationAddress = pathfindResult.destination_account;
const destinationAmount = pathfindResult.destination_amount const destinationAmount = pathfindResult.destination_amount;
return pathfindResult.alternatives.map((alt) => return pathfindResult.alternatives.map((alt) =>
parseAlternative(sourceAddress, destinationAddress, destinationAmount, alt) parseAlternative(sourceAddress, destinationAddress, destinationAmount, alt)
) );
} }
export default parsePathfind export default parsePathfind;

View File

@@ -1,12 +1,15 @@
import * as assert from 'assert' import * as assert from "assert";
import {removeUndefined} from '../../utils'
import {txFlags} from '../../common' import { txFlags } from "../../common";
import parseAmount from './amount' import { removeUndefined } from "../../utils";
import {parseMemos} from './utils'
const claimFlags = txFlags.PaymentChannelClaim import parseAmount from "./amount";
import { parseMemos } from "./utils";
const claimFlags = txFlags.PaymentChannelClaim;
function parsePaymentChannelClaim(tx: any): object { function parsePaymentChannelClaim(tx: any): object {
assert.ok(tx.TransactionType === 'PaymentChannelClaim') assert.ok(tx.TransactionType === "PaymentChannelClaim");
return removeUndefined({ return removeUndefined({
memos: parseMemos(tx), memos: parseMemos(tx),
@@ -16,8 +19,8 @@ function parsePaymentChannelClaim(tx: any): object {
signature: tx.Signature, signature: tx.Signature,
publicKey: tx.PublicKey, publicKey: tx.PublicKey,
renew: Boolean(tx.Flags & claimFlags.Renew) || undefined, renew: Boolean(tx.Flags & claimFlags.Renew) || undefined,
close: Boolean(tx.Flags & claimFlags.Close) || undefined close: Boolean(tx.Flags & claimFlags.Close) || undefined,
}) });
} }
export default parsePaymentChannelClaim export default parsePaymentChannelClaim;

View File

@@ -1,10 +1,12 @@
import * as assert from 'assert' import * as assert from "assert";
import {parseTimestamp,parseMemos} from './utils'
import {removeUndefined} from '../../utils' import { removeUndefined } from "../../utils";
import parseAmount from './amount'
import parseAmount from "./amount";
import { parseTimestamp, parseMemos } from "./utils";
function parsePaymentChannelCreate(tx: any): object { function parsePaymentChannelCreate(tx: any): object {
assert.ok(tx.TransactionType === 'PaymentChannelCreate') assert.ok(tx.TransactionType === "PaymentChannelCreate");
return removeUndefined({ return removeUndefined({
memos: parseMemos(tx), memos: parseMemos(tx),
@@ -14,8 +16,8 @@ function parsePaymentChannelCreate(tx: any): object {
publicKey: tx.PublicKey, publicKey: tx.PublicKey,
cancelAfter: tx.CancelAfter && parseTimestamp(tx.CancelAfter), cancelAfter: tx.CancelAfter && parseTimestamp(tx.CancelAfter),
sourceTag: tx.SourceTag, sourceTag: tx.SourceTag,
destinationTag: tx.DestinationTag destinationTag: tx.DestinationTag,
}) });
} }
export default parsePaymentChannelCreate export default parsePaymentChannelCreate;

View File

@@ -1,17 +1,19 @@
import * as assert from 'assert' import * as assert from "assert";
import {parseTimestamp,parseMemos} from './utils'
import {removeUndefined} from '../../utils' import { removeUndefined } from "../../utils";
import parseAmount from './amount'
import parseAmount from "./amount";
import { parseTimestamp, parseMemos } from "./utils";
function parsePaymentChannelFund(tx: any): object { function parsePaymentChannelFund(tx: any): object {
assert.ok(tx.TransactionType === 'PaymentChannelFund') assert.ok(tx.TransactionType === "PaymentChannelFund");
return removeUndefined({ return removeUndefined({
memos: parseMemos(tx), memos: parseMemos(tx),
channel: tx.Channel, channel: tx.Channel,
amount: parseAmount(tx.Amount).value, amount: parseAmount(tx.Amount).value,
expiration: tx.Expiration && parseTimestamp(tx.Expiration) expiration: tx.Expiration && parseTimestamp(tx.Expiration),
}) });
} }
export default parsePaymentChannelFund export default parsePaymentChannelFund;

View File

@@ -1,25 +1,24 @@
import {parseTimestamp, parseMemos} from './utils' import { PayChannel } from "../../models/ledger";
import {removeUndefined, dropsToXrp} from '../../utils' import { removeUndefined, dropsToXrp } from "../../utils";
import { PayChannel } from '../../models/ledger'
export type FormattedPaymentChannel = { import { parseTimestamp, parseMemos } from "./utils";
account: string
amount: string export interface FormattedPaymentChannel {
balance: string account: string;
publicKey: string amount: string;
destination: string balance: string;
settleDelay: number publicKey: string;
expiration?: string destination: string;
cancelAfter?: string settleDelay: number;
sourceTag?: number expiration?: string;
destinationTag?: number cancelAfter?: string;
previousAffectingTransactionID: string sourceTag?: number;
previousAffectingTransactionLedgerVersion: number destinationTag?: number;
previousAffectingTransactionID: string;
previousAffectingTransactionLedgerVersion: number;
} }
export function parsePaymentChannel( export function parsePaymentChannel(data: PayChannel): FormattedPaymentChannel {
data: PayChannel
): FormattedPaymentChannel {
return removeUndefined({ return removeUndefined({
memos: parseMemos(data), memos: parseMemos(data),
account: data.Account, account: data.Account,
@@ -33,6 +32,6 @@ export function parsePaymentChannel(
sourceTag: data.SourceTag, sourceTag: data.SourceTag,
destinationTag: data.DestinationTag, destinationTag: data.DestinationTag,
previousAffectingTransactionID: data.PreviousTxnID, previousAffectingTransactionID: data.PreviousTxnID,
previousAffectingTransactionLedgerVersion: data.PreviousTxnLgrSeq previousAffectingTransactionLedgerVersion: data.PreviousTxnLgrSeq,
}) });
} }

View File

@@ -1,27 +1,30 @@
import _ from 'lodash' import * as assert from "assert";
import * as assert from 'assert'
import * as utils from './utils' import _ from "lodash";
import {txFlags} from '../../common'
import {removeUndefined} from '../../utils' import { txFlags } from "../../common";
import parseAmount from './amount' import { removeUndefined } from "../../utils";
import parseAmount from "./amount";
import * as utils from "./utils";
function isNoDirectRipple(tx) { function isNoDirectRipple(tx) {
return (tx.Flags & txFlags.Payment.NoRippleDirect) !== 0 return (tx.Flags & txFlags.Payment.NoRippleDirect) !== 0;
} }
function isQualityLimited(tx) { function isQualityLimited(tx) {
return (tx.Flags & txFlags.Payment.LimitQuality) !== 0 return (tx.Flags & txFlags.Payment.LimitQuality) !== 0;
} }
function removeGenericCounterparty(amount, address) { function removeGenericCounterparty(amount, address) {
return amount.counterparty === address return amount.counterparty === address
? _.omit(amount, 'counterparty') ? _.omit(amount, "counterparty")
: amount : amount;
} }
// Payment specification // Payment specification
function parsePayment(tx: any): object { function parsePayment(tx: any): object {
assert.ok(tx.TransactionType === 'Payment') assert.ok(tx.TransactionType === "Payment");
const source = { const source = {
address: tx.Account, address: tx.Account,
@@ -29,17 +32,17 @@ function parsePayment(tx: any): object {
parseAmount(tx.SendMax || tx.Amount), parseAmount(tx.SendMax || tx.Amount),
tx.Account tx.Account
), ),
tag: tx.SourceTag tag: tx.SourceTag,
} };
const destination: { const destination: {
address: string address: string;
tag: number | undefined tag: number | undefined;
} = { } = {
address: tx.Destination, address: tx.Destination,
tag: tx.DestinationTag tag: tx.DestinationTag,
// Notice that `amount` is omitted to prevent misinterpretation // Notice that `amount` is omitted to prevent misinterpretation
} };
return removeUndefined({ return removeUndefined({
source: removeUndefined(source), source: removeUndefined(source),
@@ -49,8 +52,8 @@ function parsePayment(tx: any): object {
paths: tx.Paths ? JSON.stringify(tx.Paths) : undefined, paths: tx.Paths ? JSON.stringify(tx.Paths) : undefined,
allowPartialPayment: utils.isPartialPayment(tx) || undefined, allowPartialPayment: utils.isPartialPayment(tx) || undefined,
noDirectRipple: isNoDirectRipple(tx) || undefined, noDirectRipple: isNoDirectRipple(tx) || undefined,
limitQuality: isQualityLimited(tx) || undefined limitQuality: isQualityLimited(tx) || undefined,
}) });
} }
export default parsePayment export default parsePayment;

View File

@@ -1,65 +1,69 @@
import _ from 'lodash' import * as assert from "assert";
import * as assert from 'assert'
import {constants} from '../../common' import _ from "lodash";
const AccountFlags = constants.AccountFlags
import parseFields from './fields' import { constants } from "../../common";
import parseFields from "./fields";
const AccountFlags = constants.AccountFlags;
function getAccountRootModifiedNode(tx: any) { function getAccountRootModifiedNode(tx: any) {
const modifiedNodes = tx.meta.AffectedNodes.filter( const modifiedNodes = tx.meta.AffectedNodes.filter(
(node) => node.ModifiedNode.LedgerEntryType === 'AccountRoot' (node) => node.ModifiedNode.LedgerEntryType === "AccountRoot"
) );
assert.ok(modifiedNodes.length === 1) assert.ok(modifiedNodes.length === 1);
return modifiedNodes[0].ModifiedNode return modifiedNodes[0].ModifiedNode;
} }
function parseFlags(tx: any): any { function parseFlags(tx: any): any {
const settings: any = {} const settings: any = {};
if (tx.TransactionType !== 'AccountSet') { if (tx.TransactionType !== "AccountSet") {
return settings return settings;
} }
const node = getAccountRootModifiedNode(tx) const node = getAccountRootModifiedNode(tx);
const oldFlags = _.get(node.PreviousFields, 'Flags') const oldFlags = _.get(node.PreviousFields, "Flags");
const newFlags = _.get(node.FinalFields, 'Flags') const newFlags = _.get(node.FinalFields, "Flags");
if (oldFlags != null && newFlags != null) { if (oldFlags != null && newFlags != null) {
const changedFlags = oldFlags ^ newFlags const changedFlags = oldFlags ^ newFlags;
const setFlags = newFlags & changedFlags const setFlags = newFlags & changedFlags;
const clearedFlags = oldFlags & changedFlags const clearedFlags = oldFlags & changedFlags;
Object.entries(AccountFlags).forEach(entry => { Object.entries(AccountFlags).forEach((entry) => {
const [flagName, flagValue] = entry; const [flagName, flagValue] = entry;
if (setFlags & flagValue) { if (setFlags & flagValue) {
settings[flagName] = true settings[flagName] = true;
} else if (clearedFlags & flagValue) { } else if (clearedFlags & flagValue) {
settings[flagName] = false settings[flagName] = false;
} }
}) });
} }
// enableTransactionIDTracking requires a special case because it // enableTransactionIDTracking requires a special case because it
// does not affect the Flags field; instead it adds/removes a field called // does not affect the Flags field; instead it adds/removes a field called
// "AccountTxnID" to/from the account root. // "AccountTxnID" to/from the account root.
const oldField = _.get(node.PreviousFields, 'AccountTxnID') const oldField = _.get(node.PreviousFields, "AccountTxnID");
const newField = _.get(node.FinalFields, 'AccountTxnID') const newField = _.get(node.FinalFields, "AccountTxnID");
if (newField && !oldField) { if (newField && !oldField) {
settings.enableTransactionIDTracking = true settings.enableTransactionIDTracking = true;
} else if (oldField && !newField) { } else if (oldField && !newField) {
settings.enableTransactionIDTracking = false settings.enableTransactionIDTracking = false;
} }
return settings return settings;
} }
function parseSettings(tx: any) { function parseSettings(tx: any) {
const txType = tx.TransactionType const txType = tx.TransactionType;
assert.ok( assert.ok(
txType === 'AccountSet' || txType === "AccountSet" ||
txType === 'SetRegularKey' || txType === "SetRegularKey" ||
txType === 'SignerListSet' txType === "SignerListSet"
) );
return Object.assign({}, parseFlags(tx), parseFields(tx)) return { ...parseFlags(tx), ...parseFields(tx) };
} }
export default parseSettings export default parseSettings;

View File

@@ -1,13 +1,15 @@
import * as assert from 'assert' import * as assert from "assert";
import {removeUndefined} from '../../utils'
import {parseMemos} from './utils' import { removeUndefined } from "../../utils";
import { parseMemos } from "./utils";
function parseTicketCreate(tx: any): object { function parseTicketCreate(tx: any): object {
assert.ok(tx.TransactionType === 'TicketCreate') assert.ok(tx.TransactionType === "TicketCreate");
return removeUndefined({ return removeUndefined({
memos: parseMemos(tx), memos: parseMemos(tx),
ticketCount: tx.TicketCount ticketCount: tx.TicketCount,
}) });
} }
export default parseTicketCreate export default parseTicketCreate;

View File

@@ -1,59 +1,58 @@
import {parseOutcome} from './utils' import { removeUndefined } from "../../utils";
import {removeUndefined} from '../../utils'
import parseSettings from './settings' import parseAccountDelete from "./account-delete";
import parseAccountDelete from './account-delete' import parseAmendment from "./amendment"; // pseudo-transaction
import parseCheckCancel from './check-cancel' import parseOrderCancellation from "./cancellation";
import parseCheckCash from './check-cash' import parseCheckCancel from "./check-cancel";
import parseCheckCreate from './check-create' import parseCheckCash from "./check-cash";
import parseDepositPreauth from './deposit-preauth' import parseCheckCreate from "./check-create";
import parseEscrowCancellation from './escrow-cancellation' import parseDepositPreauth from "./deposit-preauth";
import parseEscrowCreation from './escrow-creation' import parseEscrowCancellation from "./escrow-cancellation";
import parseEscrowExecution from './escrow-execution' import parseEscrowCreation from "./escrow-creation";
import parseOrderCancellation from './cancellation' import parseEscrowExecution from "./escrow-execution";
import parseOrder from './order' import parseFeeUpdate from "./fee-update"; // pseudo-transaction
import parsePayment from './payment' import parseOrder from "./order";
import parsePaymentChannelClaim from './payment-channel-claim' import parsePayment from "./payment";
import parsePaymentChannelCreate from './payment-channel-create' import parsePaymentChannelClaim from "./payment-channel-claim";
import parsePaymentChannelFund from './payment-channel-fund' import parsePaymentChannelCreate from "./payment-channel-create";
import parseTicketCreate from './ticket-create' import parsePaymentChannelFund from "./payment-channel-fund";
import parseTrustline from './trustline' import parseSettings from "./settings";
import parseTicketCreate from "./ticket-create";
import parseAmendment from './amendment' // pseudo-transaction import parseTrustline from "./trustline";
import parseFeeUpdate from './fee-update' // pseudo-transaction import { parseOutcome } from "./utils";
function parseTransactionType(type) { function parseTransactionType(type) {
// Ordering matches https://developers.ripple.com/transaction-types.html // Ordering matches https://developers.ripple.com/transaction-types.html
const mapping = { const mapping = {
AccountSet: 'settings', AccountSet: "settings",
AccountDelete: 'accountDelete', AccountDelete: "accountDelete",
CheckCancel: 'checkCancel', CheckCancel: "checkCancel",
CheckCash: 'checkCash', CheckCash: "checkCash",
CheckCreate: 'checkCreate', CheckCreate: "checkCreate",
DepositPreauth: 'depositPreauth', DepositPreauth: "depositPreauth",
EscrowCancel: 'escrowCancellation', EscrowCancel: "escrowCancellation",
EscrowCreate: 'escrowCreation', EscrowCreate: "escrowCreation",
EscrowFinish: 'escrowExecution', EscrowFinish: "escrowExecution",
OfferCancel: 'orderCancellation', OfferCancel: "orderCancellation",
OfferCreate: 'order', OfferCreate: "order",
Payment: 'payment', Payment: "payment",
PaymentChannelClaim: 'paymentChannelClaim', PaymentChannelClaim: "paymentChannelClaim",
PaymentChannelCreate: 'paymentChannelCreate', PaymentChannelCreate: "paymentChannelCreate",
PaymentChannelFund: 'paymentChannelFund', PaymentChannelFund: "paymentChannelFund",
SetRegularKey: 'settings', SetRegularKey: "settings",
SignerListSet: 'settings', SignerListSet: "settings",
TicketCreate: 'ticketCreate', TicketCreate: "ticketCreate",
TrustSet: 'trustline', TrustSet: "trustline",
EnableAmendment: 'amendment', // pseudo-transaction EnableAmendment: "amendment", // pseudo-transaction
SetFee: 'feeUpdate' // pseudo-transaction SetFee: "feeUpdate", // pseudo-transaction
} };
return mapping[type] || null return mapping[type] || null;
} }
// includeRawTransaction: undefined by default (getTransaction) // includeRawTransaction: undefined by default (getTransaction)
function parseTransaction(tx: any, includeRawTransaction: boolean): any { function parseTransaction(tx: any, includeRawTransaction: boolean): any {
const type = parseTransactionType(tx.TransactionType) const type = parseTransactionType(tx.TransactionType);
const mapping = { const mapping = {
settings: parseSettings, settings: parseSettings,
accountDelete: parseAccountDelete, accountDelete: parseAccountDelete,
@@ -74,31 +73,31 @@ function parseTransaction(tx: any, includeRawTransaction: boolean): any {
trustline: parseTrustline, trustline: parseTrustline,
amendment: parseAmendment, // pseudo-transaction amendment: parseAmendment, // pseudo-transaction
feeUpdate: parseFeeUpdate // pseudo-transaction feeUpdate: parseFeeUpdate, // pseudo-transaction
} };
const parser: Function = mapping[type] const parser: Function = mapping[type];
const specification = parser const specification = parser
? parser(tx) ? parser(tx)
: { : {
UNAVAILABLE: 'Unrecognized transaction type.', UNAVAILABLE: "Unrecognized transaction type.",
SEE_RAW_TRANSACTION: SEE_RAW_TRANSACTION:
'Since this type is unrecognized, `rawTransaction` is included in this response.' "Since this type is unrecognized, `rawTransaction` is included in this response.",
} };
if (!parser) { if (!parser) {
includeRawTransaction = true includeRawTransaction = true;
} }
const outcome = parseOutcome(tx) const outcome = parseOutcome(tx);
return removeUndefined({ return removeUndefined({
type: type, type,
address: tx.Account, address: tx.Account,
sequence: tx.Sequence, sequence: tx.Sequence,
id: tx.hash, id: tx.hash,
specification: removeUndefined(specification), specification: removeUndefined(specification),
outcome: outcome ? removeUndefined(outcome) : undefined, outcome: outcome ? removeUndefined(outcome) : undefined,
rawTransaction: includeRawTransaction ? JSON.stringify(tx) : undefined rawTransaction: includeRawTransaction ? JSON.stringify(tx) : undefined,
}) });
} }
export default parseTransaction export default parseTransaction;

View File

@@ -1,21 +1,24 @@
import * as assert from 'assert' import * as assert from "assert";
import {parseQuality, parseMemos} from './utils'
import {txFlags} from '../../common' import { txFlags } from "../../common";
import {removeUndefined} from '../../utils' import { removeUndefined } from "../../utils";
const flags = txFlags.TrustSet
import { parseQuality, parseMemos } from "./utils";
const flags = txFlags.TrustSet;
function parseFlag(flagsValue, trueValue, falseValue) { function parseFlag(flagsValue, trueValue, falseValue) {
if (flagsValue & trueValue) { if (flagsValue & trueValue) {
return true return true;
} }
if (flagsValue & falseValue) { if (flagsValue & falseValue) {
return false return false;
} }
return undefined return undefined;
} }
function parseTrustline(tx: any): object { function parseTrustline(tx: any): object {
assert.ok(tx.TransactionType === 'TrustSet') assert.ok(tx.TransactionType === "TrustSet");
return removeUndefined({ return removeUndefined({
limit: tx.LimitAmount.value, limit: tx.LimitAmount.value,
@@ -30,8 +33,8 @@ function parseTrustline(tx: any): object {
flags.ClearNoRipple flags.ClearNoRipple
), ),
frozen: parseFlag(tx.Flags, flags.SetFreeze, flags.ClearFreeze), frozen: parseFlag(tx.Flags, flags.SetFreeze, flags.ClearFreeze),
authorized: parseFlag(tx.Flags, flags.SetAuth, 0) authorized: parseFlag(tx.Flags, flags.SetAuth, 0),
}) });
} }
export default parseTrustline export default parseTrustline;

View File

@@ -1,32 +1,33 @@
import transactionParser from 'ripple-lib-transactionparser' import BigNumber from "bignumber.js";
import BigNumber from 'bignumber.js' import transactionParser from "ripple-lib-transactionparser";
import parseAmount from './amount'
import {Amount, Memo} from '../../common/types/objects' import { txFlags } from "../../common";
import {txFlags} from '../../common' import { Amount, Memo } from "../../common/types/objects";
import {removeUndefined, dropsToXrp, rippleTimeToISOTime} from '../../utils' import { removeUndefined, dropsToXrp, rippleTimeToISOTime } from "../../utils";
type OfferDescription = { import parseAmount from "./amount";
direction: string,
quantity: any, interface OfferDescription {
totalPrice: any, direction: string;
sequence: number, quantity: any;
status: string, totalPrice: any;
makerExchangeRate: string sequence: number;
status: string;
makerExchangeRate: string;
} }
type Orderbook = { interface Orderbook {
[key: string]: OfferDescription[] [key: string]: OfferDescription[];
} }
type BalanceSheetItem = { interface BalanceSheetItem {
counterparty: string, counterparty: string;
currency: string, currency: string;
value: string value: string;
} }
type BalanceSheet = { interface BalanceSheet {
[key: string]: BalanceSheetItem[] [key: string]: BalanceSheetItem[];
} }
function adjustQualityForXRP( function adjustQualityForXRP(
@@ -36,77 +37,79 @@ function adjustQualityForXRP(
) { ) {
// quality = takerPays.value/takerGets.value // quality = takerPays.value/takerGets.value
// using drops (1e-6 XRP) for XRP values // using drops (1e-6 XRP) for XRP values
const numeratorShift = takerPaysCurrency === 'XRP' ? -6 : 0 const numeratorShift = takerPaysCurrency === "XRP" ? -6 : 0;
const denominatorShift = takerGetsCurrency === 'XRP' ? -6 : 0 const denominatorShift = takerGetsCurrency === "XRP" ? -6 : 0;
const shift = numeratorShift - denominatorShift const shift = numeratorShift - denominatorShift;
return shift === 0 return shift === 0
? quality ? quality
: new BigNumber(quality).shiftedBy(shift).toString() : new BigNumber(quality).shiftedBy(shift).toString();
} }
function parseQuality(quality?: number | null): number | undefined { function parseQuality(quality?: number | null): number | undefined {
if (typeof quality !== 'number') { if (typeof quality !== "number") {
return undefined return undefined;
} }
return new BigNumber(quality).shiftedBy(-9).toNumber() return new BigNumber(quality).shiftedBy(-9).toNumber();
} }
function parseTimestamp(rippleTime?: number | null): string | undefined { function parseTimestamp(rippleTime?: number | null): string | undefined {
if (typeof rippleTime !== 'number') { if (typeof rippleTime !== "number") {
return undefined return undefined;
} }
return rippleTimeToISOTime(rippleTime) return rippleTimeToISOTime(rippleTime);
} }
function removeEmptyCounterparty(amount) { function removeEmptyCounterparty(amount) {
if (amount.counterparty === '') { if (amount.counterparty === "") {
delete amount.counterparty delete amount.counterparty;
} }
} }
function removeEmptyCounterpartyInBalanceChanges(balanceChanges: BalanceSheet) { function removeEmptyCounterpartyInBalanceChanges(balanceChanges: BalanceSheet) {
Object.entries(balanceChanges).forEach(([_, changes]) => { Object.entries(balanceChanges).forEach(([_, changes]) => {
changes.forEach(removeEmptyCounterparty) changes.forEach(removeEmptyCounterparty);
}) });
} }
function removeEmptyCounterpartyInOrderbookChanges(orderbookChanges: Orderbook) { function removeEmptyCounterpartyInOrderbookChanges(
orderbookChanges: Orderbook
) {
Object.entries(orderbookChanges).forEach(([_, changes]) => { Object.entries(orderbookChanges).forEach(([_, changes]) => {
changes.forEach((change) => { changes.forEach((change) => {
Object.entries(change).forEach(removeEmptyCounterparty) Object.entries(change).forEach(removeEmptyCounterparty);
}) });
}) });
} }
function isPartialPayment(tx: any) { function isPartialPayment(tx: any) {
return (tx.Flags & txFlags.Payment.PartialPayment) !== 0 return (tx.Flags & txFlags.Payment.PartialPayment) !== 0;
} }
function parseDeliveredAmount(tx: any): Amount | void { function parseDeliveredAmount(tx: any): Amount | void {
if ( if (
tx.TransactionType !== 'Payment' || tx.TransactionType !== "Payment" ||
tx.meta.TransactionResult !== 'tesSUCCESS' tx.meta.TransactionResult !== "tesSUCCESS"
) { ) {
return undefined return undefined;
} }
if (tx.meta.delivered_amount && tx.meta.delivered_amount === 'unavailable') { if (tx.meta.delivered_amount && tx.meta.delivered_amount === "unavailable") {
return undefined return undefined;
} }
// parsable delivered_amount // parsable delivered_amount
if (tx.meta.delivered_amount) { if (tx.meta.delivered_amount) {
return parseAmount(tx.meta.delivered_amount) return parseAmount(tx.meta.delivered_amount);
} }
// DeliveredAmount only present on partial payments // DeliveredAmount only present on partial payments
if (tx.meta.DeliveredAmount) { if (tx.meta.DeliveredAmount) {
return parseAmount(tx.meta.DeliveredAmount) return parseAmount(tx.meta.DeliveredAmount);
} }
// no partial payment flag, use tx.Amount // no partial payment flag, use tx.Amount
if (tx.Amount && !isPartialPayment(tx)) { if (tx.Amount && !isPartialPayment(tx)) {
return parseAmount(tx.Amount) return parseAmount(tx.Amount);
} }
// DeliveredAmount field was introduced at // DeliveredAmount field was introduced at
@@ -116,52 +119,52 @@ function parseDeliveredAmount(tx: any): Amount | void {
// transferred with a partial payment before // transferred with a partial payment before
// that date must be derived from metadata. // that date must be derived from metadata.
if (tx.Amount && tx.ledger_index > 4594094) { if (tx.Amount && tx.ledger_index > 4594094) {
return parseAmount(tx.Amount) return parseAmount(tx.Amount);
} }
return undefined return undefined;
} }
function parseOutcome(tx: any): any | undefined { function parseOutcome(tx: any): any | undefined {
const metadata = tx.meta || tx.metaData const metadata = tx.meta || tx.metaData;
if (!metadata) { if (!metadata) {
return undefined return undefined;
} }
const balanceChanges = transactionParser.parseBalanceChanges(metadata) const balanceChanges = transactionParser.parseBalanceChanges(metadata);
const orderbookChanges = transactionParser.parseOrderbookChanges(metadata) const orderbookChanges = transactionParser.parseOrderbookChanges(metadata);
const channelChanges = transactionParser.parseChannelChanges(metadata) const channelChanges = transactionParser.parseChannelChanges(metadata);
removeEmptyCounterpartyInBalanceChanges(balanceChanges) removeEmptyCounterpartyInBalanceChanges(balanceChanges);
removeEmptyCounterpartyInOrderbookChanges(orderbookChanges) removeEmptyCounterpartyInOrderbookChanges(orderbookChanges);
return removeUndefined({ return removeUndefined({
result: tx.meta.TransactionResult, result: tx.meta.TransactionResult,
timestamp: parseTimestamp(tx.date), timestamp: parseTimestamp(tx.date),
fee: dropsToXrp(tx.Fee), fee: dropsToXrp(tx.Fee),
balanceChanges: balanceChanges, balanceChanges,
orderbookChanges: orderbookChanges, orderbookChanges,
channelChanges: channelChanges, channelChanges,
ledgerVersion: tx.ledger_index, ledgerVersion: tx.ledger_index,
indexInLedger: tx.meta.TransactionIndex, indexInLedger: tx.meta.TransactionIndex,
deliveredAmount: parseDeliveredAmount(tx) deliveredAmount: parseDeliveredAmount(tx),
}) });
} }
function hexToString(hex: string): string | undefined { function hexToString(hex: string): string | undefined {
return hex ? Buffer.from(hex, 'hex').toString('utf-8') : undefined return hex ? Buffer.from(hex, "hex").toString("utf-8") : undefined;
} }
function parseMemos(tx: any): Array<Memo> | undefined { function parseMemos(tx: any): Memo[] | undefined {
if (!Array.isArray(tx.Memos) || tx.Memos.length === 0) { if (!Array.isArray(tx.Memos) || tx.Memos.length === 0) {
return undefined return undefined;
} }
return tx.Memos.map((m) => { return tx.Memos.map((m) => {
return removeUndefined({ return removeUndefined({
type: m.Memo.parsed_memo_type || hexToString(m.Memo.MemoType), type: m.Memo.parsed_memo_type || hexToString(m.Memo.MemoType),
format: m.Memo.parsed_memo_format || hexToString(m.Memo.MemoFormat), format: m.Memo.parsed_memo_format || hexToString(m.Memo.MemoFormat),
data: m.Memo.parsed_memo_data || hexToString(m.Memo.MemoData) data: m.Memo.parsed_memo_data || hexToString(m.Memo.MemoData),
}) });
}) });
} }
export { export {
@@ -171,5 +174,5 @@ export {
hexToString, hexToString,
parseTimestamp, parseTimestamp,
adjustQualityForXRP, adjustQualityForXRP,
isPartialPayment isPartialPayment,
} };

View File

@@ -3,64 +3,64 @@ import {
RippledAmount, RippledAmount,
Adjustment, Adjustment,
MaxAdjustment, MaxAdjustment,
MinAdjustment MinAdjustment,
} from '../common/types/objects' } from "../common/types/objects";
// Amount where counterparty and value are optional // Amount where counterparty and value are optional
export type LaxLaxAmount = { export interface LaxLaxAmount {
currency: string currency: string;
value?: string value?: string;
issuer?: string issuer?: string;
counterparty?: string counterparty?: string;
} }
export type Path = { export interface Path {
source: Adjustment | MaxAdjustment source: Adjustment | MaxAdjustment;
destination: Adjustment | MinAdjustment destination: Adjustment | MinAdjustment;
paths: string paths: string;
} }
export type GetPaths = Array<Path> export type GetPaths = Path[];
export type PathFind = { export interface PathFind {
source: { source: {
address: string address: string;
amount?: Amount amount?: Amount;
currencies?: Array<{currency: string; counterparty?: string}> currencies?: Array<{ currency: string; counterparty?: string }>;
} };
destination: { destination: {
address: string address: string;
amount: LaxLaxAmount amount: LaxLaxAmount;
} };
} }
export type PathFindRequest = { export interface PathFindRequest {
command: string command: string;
source_account: string source_account: string;
destination_amount: RippledAmount destination_amount: RippledAmount;
destination_account: string destination_account: string;
source_currencies?: {currency: string; issuer?: string}[] source_currencies?: Array<{ currency: string; issuer?: string }>;
send_max?: RippledAmount send_max?: RippledAmount;
} }
export type RippledPathsResponse = { export interface RippledPathsResponse {
alternatives: Array<{ alternatives: Array<{
paths_computed: Array< paths_computed: Array<
Array<{ Array<{
type: number type: number;
type_hex: string type_hex: string;
account?: string account?: string;
issuer?: string issuer?: string;
currency?: string currency?: string;
}> }>
> >;
source_amount: RippledAmount source_amount: RippledAmount;
}> }>;
type: string type: string;
destination_account: string destination_account: string;
destination_amount: RippledAmount destination_amount: RippledAmount;
destination_currencies?: Array<string> destination_currencies?: string[];
source_account: string source_account: string;
source_currencies?: Array<{currency: string}> source_currencies?: Array<{ currency: string }>;
full_reply?: boolean full_reply?: boolean;
} }

View File

@@ -1,87 +1,90 @@
import _ from 'lodash' import BigNumber from "bignumber.js";
import BigNumber from 'bignumber.js' import _ from "lodash";
import {getXRPBalance, renameCounterpartyToIssuer} from './utils'
import { import type { Client } from "..";
validate, import { Connection } from "../client";
errors import { validate, errors } from "../common";
} from '../common' import { RippledAmount, Amount } from "../common/types/objects";
import {toRippledAmount, xrpToDrops, dropsToXrp} from '../utils' import { RipplePathFindRequest } from "../models/methods";
import {Connection} from '../client' import { toRippledAmount, xrpToDrops, dropsToXrp } from "../utils";
import parsePathfind from './parse/pathfind'
import {RippledAmount, Amount} from '../common/types/objects' import parsePathfind from "./parse/pathfind";
import { import {
GetPaths, GetPaths,
PathFind, PathFind,
RippledPathsResponse, RippledPathsResponse,
PathFindRequest PathFindRequest,
} from './pathfind-types' } from "./pathfind-types";
import {Client} from '..' import { getXRPBalance, renameCounterpartyToIssuer } from "./utils";
import { RipplePathFindRequest } from '../models/methods'
const NotFoundError = errors.NotFoundError const NotFoundError = errors.NotFoundError;
const ValidationError = errors.ValidationError const ValidationError = errors.ValidationError;
function addParams( function addParams(
request: PathFindRequest, request: PathFindRequest,
result: RippledPathsResponse result: RippledPathsResponse
): RippledPathsResponse { ): RippledPathsResponse {
return _.defaults( return _.defaults(
Object.assign({}, result, { {
...result,
source_account: request.source_account, source_account: request.source_account,
source_currencies: request.source_currencies source_currencies: request.source_currencies,
}), },
{destination_amount: request.destination_amount} { destination_amount: request.destination_amount }
) );
} }
function requestPathFind( function requestPathFind(
connection: Connection, connection: Connection,
pathfind: PathFind pathfind: PathFind
): Promise<RippledPathsResponse> { ): Promise<RippledPathsResponse> {
const destinationAmount: Amount = Object.assign( const destinationAmount: Amount = {
{ // This is converted back to drops by toRippledAmount()
// This is converted back to drops by toRippledAmount() value:
value: pathfind.destination.amount.currency === "XRP" ? dropsToXrp("-1") : "-1",
pathfind.destination.amount.currency === 'XRP' ? dropsToXrp('-1') : '-1' ...pathfind.destination.amount,
}, };
pathfind.destination.amount
)
const request: RipplePathFindRequest = { const request: RipplePathFindRequest = {
command: 'ripple_path_find', command: "ripple_path_find",
source_account: pathfind.source.address, source_account: pathfind.source.address,
destination_account: pathfind.destination.address, destination_account: pathfind.destination.address,
// @ts-ignore // @ts-expect-error
destination_amount: destinationAmount destination_amount: destinationAmount,
} };
if ( if (
typeof request.destination_amount === 'object' && typeof request.destination_amount === "object" &&
!request.destination_amount.issuer !request.destination_amount.issuer
) { ) {
// Convert blank issuer to sender's address // Convert blank issuer to sender's address
// (Ripple convention for 'any issuer') // (Ripple convention for 'any issuer')
// https://developers.ripple.com/payment.html#special-issuer-values-for-sendmax-and-amount // https://developers.ripple.com/payment.html#special-issuer-values-for-sendmax-and-amount
request.destination_amount.issuer = request.destination_account request.destination_amount.issuer = request.destination_account;
} }
if (pathfind.source.currencies && pathfind.source.currencies.length > 0) { if (pathfind.source.currencies && pathfind.source.currencies.length > 0) {
// @ts-ignore // @ts-expect-error
request.source_currencies = pathfind.source.currencies.map((amount) => request.source_currencies = pathfind.source.currencies.map((amount) =>
renameCounterpartyToIssuer(amount) renameCounterpartyToIssuer(amount)
) );
} }
if (pathfind.source.amount) { if (pathfind.source.amount) {
if (pathfind.destination.amount.value != null) { if (pathfind.destination.amount.value != null) {
throw new ValidationError( throw new ValidationError(
'Cannot specify both source.amount' + "Cannot specify both source.amount" +
' and destination.amount.value in getPaths' " and destination.amount.value in getPaths"
) );
} }
// @ts-ignore // @ts-expect-error
request.send_max = toRippledAmount(pathfind.source.amount) request.send_max = toRippledAmount(pathfind.source.amount);
if (typeof request.send_max !== 'string' && !request.send_max.issuer) { if (
request.send_max.issuer = pathfind.source.address request.send_max != null &&
typeof request.send_max !== "string" &&
!request.send_max.issuer
) {
request.send_max.issuer = pathfind.source.address;
} }
} }
// @ts-ignore // @ts-expect-error
return connection.request(request).then((paths) => addParams(request, paths)) return connection.request(request).then((paths) => addParams(request, paths));
} }
function addDirectXrpPath( function addDirectXrpPath(
@@ -89,22 +92,22 @@ function addDirectXrpPath(
xrpBalance: string xrpBalance: string
): RippledPathsResponse { ): RippledPathsResponse {
// Add XRP "path" only if the source acct has enough XRP to make the payment // Add XRP "path" only if the source acct has enough XRP to make the payment
const destinationAmount = paths.destination_amount const destinationAmount = paths.destination_amount;
// @ts-ignore: destinationAmount can be a currency amount object! Fix! // @ts-expect-error: destinationAmount can be a currency amount object! Fix!
if (new BigNumber(xrpBalance).isGreaterThanOrEqualTo(destinationAmount)) { if (new BigNumber(xrpBalance).isGreaterThanOrEqualTo(destinationAmount)) {
paths.alternatives.unshift({ paths.alternatives.unshift({
paths_computed: [], paths_computed: [],
source_amount: paths.destination_amount source_amount: paths.destination_amount,
}) });
} }
return paths return paths;
} }
function isRippledIOUAmount(amount: RippledAmount) { function isRippledIOUAmount(amount: RippledAmount) {
// rippled XRP amounts are specified as decimal strings // rippled XRP amounts are specified as decimal strings
return ( return (
typeof amount === 'object' && amount.currency && amount.currency !== 'XRP' typeof amount === "object" && amount.currency && amount.currency !== "XRP"
) );
} }
function conditionallyAddDirectXRPPath( function conditionallyAddDirectXRPPath(
@@ -114,13 +117,14 @@ function conditionallyAddDirectXRPPath(
): Promise<RippledPathsResponse> { ): Promise<RippledPathsResponse> {
if ( if (
isRippledIOUAmount(paths.destination_amount) || isRippledIOUAmount(paths.destination_amount) ||
!paths.destination_currencies.includes('XRP') (paths.destination_currencies &&
!paths.destination_currencies.includes("XRP"))
) { ) {
return Promise.resolve(paths) return Promise.resolve(paths);
} }
return getXRPBalance(client, address, undefined).then((xrpBalance) => return getXRPBalance(client, address, undefined).then((xrpBalance) =>
addDirectXrpPath(paths, xrpBalance) addDirectXrpPath(paths, xrpBalance)
) );
} }
function filterSourceFundsLowPaths( function filterSourceFundsLowPaths(
@@ -129,74 +133,72 @@ function filterSourceFundsLowPaths(
): RippledPathsResponse { ): RippledPathsResponse {
if ( if (
pathfind.source.amount && pathfind.source.amount &&
pathfind.destination.amount.value == null && paths.alternatives &&
paths.alternatives pathfind.destination.amount.value == null
) { ) {
paths.alternatives = paths.alternatives.filter((alt) => { paths.alternatives = paths.alternatives.filter((alt) => {
if (!alt.source_amount) { if (!alt.source_amount) {
return false return false;
} }
if (pathfind.source.amount === undefined) {
return false;
}
const pathfindSourceAmountValue = new BigNumber( const pathfindSourceAmountValue = new BigNumber(
pathfind.source.amount.currency === 'XRP' pathfind.source.amount.currency === "XRP"
? xrpToDrops(pathfind.source.amount.value) ? xrpToDrops(pathfind.source.amount.value)
: pathfind.source.amount.value : pathfind.source.amount.value
) );
const altSourceAmountValue = new BigNumber( const altSourceAmountValue = new BigNumber(
typeof alt.source_amount === 'string' typeof alt.source_amount === "string"
? alt.source_amount ? alt.source_amount
: alt.source_amount.value : alt.source_amount.value
) );
return altSourceAmountValue.eq(pathfindSourceAmountValue) return altSourceAmountValue.eq(pathfindSourceAmountValue);
}) });
} }
return paths return paths;
} }
function formatResponse(pathfind: PathFind, paths: RippledPathsResponse) { function formatResponse(pathfind: PathFind, paths: RippledPathsResponse) {
if (paths.alternatives && paths.alternatives.length > 0) { if (paths.alternatives && paths.alternatives.length > 0) {
return parsePathfind(paths) return parsePathfind(paths);
} }
if ( if (
paths.destination_currencies != null && paths.destination_currencies != null &&
!paths.destination_currencies.includes( !paths.destination_currencies.includes(pathfind.destination.amount.currency)
pathfind.destination.amount.currency
)
) { ) {
throw new NotFoundError( throw new NotFoundError(
'No paths found. ' + `${"No paths found. " + "The destination_account does not accept "}${
'The destination_account does not accept ' + pathfind.destination.amount.currency
pathfind.destination.amount.currency + }, they only accept: ${paths.destination_currencies.join(", ")}`
', they only accept: ' + );
paths.destination_currencies.join(', ')
)
} else if (paths.source_currencies && paths.source_currencies.length > 0) { } else if (paths.source_currencies && paths.source_currencies.length > 0) {
throw new NotFoundError( throw new NotFoundError(
'No paths found. Please ensure' + "No paths found. Please ensure" +
' that the source_account has sufficient funds to execute' + " that the source_account has sufficient funds to execute" +
' the payment in one of the specified source_currencies. If it does' + " the payment in one of the specified source_currencies. If it does" +
' there may be insufficient liquidity in the network to execute' + " there may be insufficient liquidity in the network to execute" +
' this payment right now' " this payment right now"
) );
} else { } else {
throw new NotFoundError( throw new NotFoundError(
'No paths found.' + "No paths found." +
' Please ensure that the source_account has sufficient funds to' + " Please ensure that the source_account has sufficient funds to" +
' execute the payment. If it does there may be insufficient liquidity' + " execute the payment. If it does there may be insufficient liquidity" +
' in the network to execute this payment right now' " in the network to execute this payment right now"
) );
} }
} }
function getPaths(this: Client, pathfind: PathFind): Promise<GetPaths> { function getPaths(this: Client, pathfind: PathFind): Promise<GetPaths> {
validate.getPaths({pathfind}) validate.getPaths({ pathfind });
const address = pathfind.source.address const address = pathfind.source.address;
return requestPathFind(this.connection, pathfind) return requestPathFind(this.connection, pathfind)
.then((paths) => .then((paths) => conditionallyAddDirectXRPPath(this, address, paths))
conditionallyAddDirectXRPPath(this, address, paths)
)
.then((paths) => filterSourceFundsLowPaths(pathfind, paths)) .then((paths) => filterSourceFundsLowPaths(pathfind, paths))
.then((paths) => formatResponse(pathfind, paths)) .then((paths) => formatResponse(pathfind, paths));
} }
export default getPaths export default getPaths;

View File

@@ -1,18 +1,23 @@
import _ from 'lodash' import _ from "lodash";
import {validate, ensureClassicAddress} from '../common'
import parseAccountTrustline from './parse/account-trustline'
import {Client} from '..'
import {FormattedTrustline} from '../common/types/objects/trustlines'
export type GetTrustlinesOptions = { import type { Client } from "..";
counterparty?: string import { validate, ensureClassicAddress } from "../common";
currency?: string import { FormattedTrustline } from "../common/types/objects";
limit?: number
ledgerVersion?: number import parseAccountTrustline from "./parse/account-trustline";
export interface GetTrustlinesOptions {
counterparty?: string;
currency?: string;
limit?: number;
ledgerVersion?: number;
} }
function currencyFilter(currency: string, trustline: FormattedTrustline) { function currencyFilter(
return currency === null || trustline.specification.currency === currency currency: string | null,
trustline: FormattedTrustline
) {
return currency === null || trustline.specification.currency === currency;
} }
async function getTrustlines( async function getTrustlines(
@@ -21,25 +26,26 @@ async function getTrustlines(
options: GetTrustlinesOptions = {} options: GetTrustlinesOptions = {}
): Promise<FormattedTrustline[]> { ): Promise<FormattedTrustline[]> {
// 1. Validate // 1. Validate
validate.getTrustlines({address, options}) validate.getTrustlines({ address, options });
// Only support retrieving trustlines without a tag, // Only support retrieving trustlines without a tag,
// since it does not make sense to filter trustlines // since it does not make sense to filter trustlines
// by tag. // by tag.
address = ensureClassicAddress(address) address = ensureClassicAddress(address);
// 2. Make Request // 2. Make Request
const responses = await this.requestAll({command: 'account_lines', const responses = await this.requestAll({
command: "account_lines",
account: address, account: address,
ledger_index: options.ledgerVersion ?? 'validated', ledger_index: options.ledgerVersion ?? "validated",
limit: options.limit, limit: options.limit,
peer: options.counterparty peer: options.counterparty,
}) });
// 3. Return Formatted Response // 3. Return Formatted Response
const trustlines = _.flatMap(responses, (response) => response.result.lines) const trustlines = _.flatMap(responses, (response) => response.result.lines);
return trustlines.map(parseAccountTrustline).filter((trustline) => { return trustlines.map(parseAccountTrustline).filter((trustline) => {
return currencyFilter(options.currency || null, trustline) return currencyFilter(options.currency ?? null, trustline);
}) });
} }
export default getTrustlines export default getTrustlines;

View File

@@ -1,23 +1,27 @@
import _ from 'lodash' import * as assert from "assert";
import * as assert from 'assert'
import * as common from '../common'
import {Connection} from '../client'
import {FormattedTransactionType} from '../transaction/types'
import {Issue} from '../common/types/objects'
import {Client} from '..'
import {AccountInfoRequest} from '../models/methods'
import {dropsToXrp} from '..'
export type RecursiveData = { import _ from "lodash";
marker: string
results: Array<any> import { Client, dropsToXrp } from "..";
import { Connection } from "../client";
import * as common from "../common";
import { Issue } from "../common/types/objects";
import { AccountInfoRequest } from "../models/methods";
import { FormattedTransactionType } from "../transaction/types";
export interface RecursiveData {
marker: string;
results: any[];
} }
export type Getter = (marker?: string, limit?: number) => Promise<RecursiveData> export type Getter = (
marker?: string,
limit?: number
) => Promise<RecursiveData>;
function clamp(value: number, min: number, max: number): number { function clamp(value: number, min: number, max: number): number {
assert.ok(min <= max, 'Illegal clamp bounds') assert.ok(min <= max, "Illegal clamp bounds");
return Math.min(Math.max(value, min), max) return Math.min(Math.max(value, min), max);
} }
async function getXRPBalance( async function getXRPBalance(
@@ -26,13 +30,12 @@ async function getXRPBalance(
ledgerVersion?: number ledgerVersion?: number
): Promise<string> { ): Promise<string> {
const request: AccountInfoRequest = { const request: AccountInfoRequest = {
command: 'account_info', command: "account_info",
account: address, account: address,
ledger_index: ledgerVersion ledger_index: ledgerVersion,
} };
const data = await client const data = await client.request(request);
.request(request) return dropsToXrp(data.result.account_data.Balance);
return dropsToXrp(data.result.account_data.Balance)
} }
// If the marker is omitted from a response, you have reached the end // If the marker is omitted from a response, you have reached the end
@@ -40,64 +43,71 @@ async function getRecursiveRecur(
getter: Getter, getter: Getter,
marker: string | undefined, marker: string | undefined,
limit: number limit: number
): Promise<Array<any>> { ): Promise<any[]> {
const data = await getter(marker, limit) const data = await getter(marker, limit);
const remaining = limit - data.results.length const remaining = limit - data.results.length;
if (remaining > 0 && data.marker != null) { if (remaining > 0 && data.marker != null) {
return getRecursiveRecur(getter, data.marker, remaining).then((results) => data.results.concat(results) return getRecursiveRecur(getter, data.marker, remaining).then((results) =>
) data.results.concat(results)
);
} }
return data.results.slice(0, limit) return data.results.slice(0, limit);
} }
function getRecursive(getter: Getter, limit?: number): Promise<Array<any>> { function getRecursive(getter: Getter, limit?: number): Promise<any[]> {
return getRecursiveRecur(getter, undefined, limit || Infinity) return getRecursiveRecur(getter, undefined, limit || Infinity);
} }
function renameCounterpartyToIssuer<T>( function renameCounterpartyToIssuer<T>(
obj: T & {counterparty?: string; issuer?: string} obj: T & { counterparty?: string; issuer?: string }
): T & {issuer?: string} { ): T & { issuer?: string } {
const issuer = const issuer =
obj.counterparty != null obj.counterparty != null
? obj.counterparty ? obj.counterparty
: obj.issuer != null : obj.issuer != null
? obj.issuer ? obj.issuer
: undefined : undefined;
const withIssuer = Object.assign({}, obj, {issuer}) const withIssuer = { ...obj, issuer };
delete withIssuer.counterparty delete withIssuer.counterparty;
return withIssuer return withIssuer;
} }
export type RequestBookOffersArgs = {taker_gets: Issue; taker_pays: Issue} export interface RequestBookOffersArgs {
taker_gets: Issue;
taker_pays: Issue;
}
function renameCounterpartyToIssuerInOrder(order: RequestBookOffersArgs) { function renameCounterpartyToIssuerInOrder(order: RequestBookOffersArgs) {
const taker_gets = renameCounterpartyToIssuer(order.taker_gets) const taker_gets = renameCounterpartyToIssuer(order.taker_gets);
const taker_pays = renameCounterpartyToIssuer(order.taker_pays) const taker_pays = renameCounterpartyToIssuer(order.taker_pays);
const changes = {taker_gets, taker_pays} const changes = { taker_gets, taker_pays };
return Object.assign({}, order, _.omitBy(changes, value => value == null)) return { ...order, ..._.omitBy(changes, (value) => value == null) };
} }
function signum(num) { function signum(num) {
return num === 0 ? 0 : num > 0 ? 1 : -1 return num === 0 ? 0 : num > 0 ? 1 : -1;
} }
/** /**
* Order two rippled transactions based on their ledger_index. * Order two rippled transactions based on their ledger_index.
* If two transactions took place in the same ledger, sort * If two transactions took place in the same ledger, sort
* them based on TransactionIndex * them based on TransactionIndex
* See: https://developers.ripple.com/transaction-metadata.html * See: https://developers.ripple.com/transaction-metadata.html.
*
* @param first
* @param second
*/ */
function compareTransactions( function compareTransactions(
first: FormattedTransactionType, first: FormattedTransactionType,
second: FormattedTransactionType second: FormattedTransactionType
): number { ): number {
if (!first.outcome || !second.outcome) { if (!first.outcome || !second.outcome) {
return 0 return 0;
} }
if (first.outcome.ledgerVersion === second.outcome.ledgerVersion) { if (first.outcome.ledgerVersion === second.outcome.ledgerVersion) {
return signum(first.outcome.indexInLedger - second.outcome.indexInLedger) return signum(first.outcome.indexInLedger - second.outcome.indexInLedger);
} }
return first.outcome.ledgerVersion < second.outcome.ledgerVersion ? -1 : 1 return first.outcome.ledgerVersion < second.outcome.ledgerVersion ? -1 : 1;
} }
async function isPendingLedgerVersion( async function isPendingLedgerVersion(
@@ -105,27 +115,30 @@ async function isPendingLedgerVersion(
maxLedgerVersion?: number maxLedgerVersion?: number
): Promise<boolean> { ): Promise<boolean> {
const response = await client.request({ const response = await client.request({
command: 'ledger', command: "ledger",
ledger_index: 'validated' ledger_index: "validated",
}) });
const ledgerVersion = response.result.ledger_index const ledgerVersion = response.result.ledger_index;
return ledgerVersion < (maxLedgerVersion || 0) return ledgerVersion < (maxLedgerVersion || 0);
} }
async function ensureLedgerVersion(this: Client, options: any): Promise<object> { async function ensureLedgerVersion(
this: Client,
options: any
): Promise<object> {
if ( if (
Boolean(options) && Boolean(options) &&
options.ledgerVersion != null && options.ledgerVersion != null &&
options.ledgerVersion !== null options.ledgerVersion !== null
) { ) {
return Promise.resolve(options) return Promise.resolve(options);
} }
const response = await this.request({ const response = await this.request({
command: 'ledger', command: "ledger",
ledger_index: 'validated' ledger_index: "validated",
}) });
const ledgerVersion = response.result.ledger_index const ledgerVersion = response.result.ledger_index;
return Object.assign({}, options, { ledgerVersion }) return { ...options, ledgerVersion };
} }
export { export {
@@ -138,5 +151,5 @@ export {
isPendingLedgerVersion, isPendingLedgerVersion,
clamp, clamp,
common, common,
Connection Connection,
} };

View File

@@ -1,23 +1,29 @@
export type LedgerIndex = number | ('validated' | 'closed' | 'current') export type LedgerIndex = number | ("validated" | "closed" | "current");
export type AccountObjectType = 'check' | 'escrow' | 'offer' | 'payment_channel' | 'signer_list' | 'state' export type AccountObjectType =
| "check"
| "escrow"
| "offer"
| "payment_channel"
| "signer_list"
| "state";
export interface XRP { export interface XRP {
currency: "XRP" currency: "XRP";
} }
export interface IssuedCurrency { export interface IssuedCurrency {
currency: string currency: string;
issuer: string issuer: string;
} }
export type Currency = IssuedCurrency | XRP export type Currency = IssuedCurrency | XRP;
export interface IssuedCurrencyAmount extends IssuedCurrency { export interface IssuedCurrencyAmount extends IssuedCurrency {
value: string value: string;
} }
export type Amount = IssuedCurrencyAmount | string export type Amount = IssuedCurrencyAmount | string;
export interface Signer { export interface Signer {
Account: string; Account: string;
@@ -31,17 +37,25 @@ export interface Memo {
MemoFormat?: string; MemoFormat?: string;
} }
export type StreamType = "consensus" | "ledger" | "manifests" | "peer_status" | "transactions" | "transactions_proposed" | "server" | "validations" export type StreamType =
| "consensus"
| "ledger"
| "manifests"
| "peer_status"
| "transactions"
| "transactions_proposed"
| "server"
| "validations";
interface PathStep { interface PathStep {
account?: string account?: string;
currency?: string currency?: string;
issuer?: string issuer?: string;
} }
export type Path = PathStep[] export type Path = PathStep[];
export interface SignerEntry { export interface SignerEntry {
Account: string; Account: string;
SignerWeight: number; SignerWeight: number;
} }

View File

@@ -1,38 +1,38 @@
import { Amount } from "."; import { Amount } from ".";
interface CreatedNode { interface CreatedNode {
CreatedNode: { CreatedNode: {
LedgerEntryType: string LedgerEntryType: string;
LedgerIndex: string LedgerIndex: string;
NewFields: {[field: string]: any} NewFields: { [field: string]: any };
} };
} }
interface ModifiedNode { interface ModifiedNode {
ModifiedNode: { ModifiedNode: {
LedgerEntryType: string LedgerEntryType: string;
LedgerIndex: string LedgerIndex: string;
FinalFields: {[field: string]: any} FinalFields: { [field: string]: any };
PreviousFields: {[field: string]: any} PreviousFields: { [field: string]: any };
PreviousTxnID?: string PreviousTxnID?: string;
PreviouTxnLgrSeq?: number PreviouTxnLgrSeq?: number;
} };
} }
interface DeletedNode { interface DeletedNode {
DeletedNode: { DeletedNode: {
LedgerEntryType: string LedgerEntryType: string;
LedgerIndex: string LedgerIndex: string;
FinalFields: {[field: string]: any} FinalFields: { [field: string]: any };
} };
} }
type Node = CreatedNode | ModifiedNode | DeletedNode type Node = CreatedNode | ModifiedNode | DeletedNode;
export default interface Metadata { export default interface Metadata {
AffectedNodes: Node[] AffectedNodes: Node[];
DeliveredAmount?: Amount DeliveredAmount?: Amount;
delivered_amount?: Amount delivered_amount?: Amount;
TransactionIndex: number TransactionIndex: number;
TransactionResult: string TransactionResult: string;
} }

View File

@@ -1,38 +1,38 @@
import { Amount } from "."; import { Amount } from ".";
interface CreatedNode { interface CreatedNode {
CreatedNode: { CreatedNode: {
LedgerEntryType: string LedgerEntryType: string;
LedgerIndex: string LedgerIndex: string;
NewFields: {[field: string]: any} NewFields: { [field: string]: any };
} };
} }
interface ModifiedNode { interface ModifiedNode {
ModifiedNode: { ModifiedNode: {
LedgerEntryType: string LedgerEntryType: string;
LedgerIndex: string LedgerIndex: string;
FinalFields: {[field: string]: any} FinalFields: { [field: string]: any };
PreviousFields: {[field: string]: any} PreviousFields: { [field: string]: any };
PreviousTxnID?: string PreviousTxnID?: string;
PreviouTxnLgrSeq?: number PreviouTxnLgrSeq?: number;
} };
} }
interface DeletedNode { interface DeletedNode {
DeletedNode: { DeletedNode: {
LedgerEntryType: string LedgerEntryType: string;
LedgerIndex: string LedgerIndex: string;
FinalFields: {[field: string]: any} FinalFields: { [field: string]: any };
} };
} }
type Node = CreatedNode | ModifiedNode | DeletedNode type Node = CreatedNode | ModifiedNode | DeletedNode;
export interface TransactionMetadata { export interface TransactionMetadata {
AffectedNodes: Node[] AffectedNodes: Node[];
DeliveredAmount?: Amount DeliveredAmount?: Amount;
delivered_amount?: Amount delivered_amount?: Amount;
TransactionIndex: number TransactionIndex: number;
TransactionResult: string TransactionResult: string;
} }

View File

@@ -1,20 +1,20 @@
import { BaseLedgerEntry } from "./baseLedgerEntry"; import { BaseLedgerEntry } from "./baseLedgerEntry";
export interface AccountRoot extends BaseLedgerEntry{ export interface AccountRoot extends BaseLedgerEntry {
LedgerEntryType: 'AccountRoot' LedgerEntryType: "AccountRoot";
Account: string Account: string;
Balance: string Balance: string;
Flags: number Flags: number;
OwnerCount: number OwnerCount: number;
PreviousTxnID: string PreviousTxnID: string;
PreviousTxnLgrSeq: number PreviousTxnLgrSeq: number;
Sequence: number Sequence: number;
AccountTxnID?: string AccountTxnID?: string;
Domain?: string Domain?: string;
EmailHash?: string EmailHash?: string;
MessageKey?: string MessageKey?: string;
RegularKey?: string RegularKey?: string;
TicketCount?: number TicketCount?: number;
TickSize?: number TickSize?: number;
TransferRate?: number TransferRate?: number;
} }

View File

@@ -2,14 +2,14 @@ import { BaseLedgerEntry } from "./baseLedgerEntry";
interface Majority { interface Majority {
Majority: { Majority: {
Amendment: string Amendment: string;
CloseTime: number CloseTime: number;
} };
} }
export interface Amendments extends BaseLedgerEntry { export interface Amendments extends BaseLedgerEntry {
LedgerEntryType: 'Amendments' LedgerEntryType: "Amendments";
Amendments?: string[] Amendments?: string[];
Majorities?: Majority[] Majorities?: Majority[];
Flags: 0 Flags: 0;
} }

View File

@@ -1,3 +1,3 @@
export interface BaseLedgerEntry { export interface BaseLedgerEntry {
index: string index: string;
} }

View File

@@ -1,19 +1,20 @@
import { Amount } from "../common"; import { Amount } from "../common";
import { BaseLedgerEntry } from "./baseLedgerEntry"; import { BaseLedgerEntry } from "./baseLedgerEntry";
export interface Check extends BaseLedgerEntry { export interface Check extends BaseLedgerEntry {
LedgerEntryType: 'Check' LedgerEntryType: "Check";
Account: string Account: string;
Destination: string Destination: string;
Flags: 0 Flags: 0;
OwnerNode: string OwnerNode: string;
PreviousTxnID: string PreviousTxnID: string;
PreviousTxnLgrSeq: number PreviousTxnLgrSeq: number;
SendMax: Amount SendMax: Amount;
Sequence: number Sequence: number;
DestinationNode?: string DestinationNode?: string;
DestinationTag?: number DestinationTag?: number;
Expiration?: number Expiration?: number;
InvoiceID?: string InvoiceID?: string;
SourceTag?: number SourceTag?: number;
} }

View File

@@ -1,11 +1,11 @@
import { BaseLedgerEntry } from "./baseLedgerEntry"; import { BaseLedgerEntry } from "./baseLedgerEntry";
export interface DepositPreauth extends BaseLedgerEntry { export interface DepositPreauth extends BaseLedgerEntry {
LedgerEntryType: 'DepositPreauth' LedgerEntryType: "DepositPreauth";
Account: string Account: string;
Authorize: string Authorize: string;
Flags: 0 Flags: 0;
OwnerNode: string OwnerNode: string;
PreviousTxnID: string PreviousTxnID: string;
PreviousTxnLgrSeq: number PreviousTxnLgrSeq: number;
} }

View File

@@ -1,15 +1,15 @@
import { BaseLedgerEntry } from "./baseLedgerEntry"; import { BaseLedgerEntry } from "./baseLedgerEntry";
export interface DirectoryNode extends BaseLedgerEntry { export interface DirectoryNode extends BaseLedgerEntry {
LedgerEntryType: 'DirectoryNode' LedgerEntryType: "DirectoryNode";
Flags: number Flags: number;
RootIndex: string RootIndex: string;
Indexes: string[] Indexes: string[];
IndexNext?: number IndexNext?: number;
IndexPrevious?: number IndexPrevious?: number;
Owner?: string Owner?: string;
TakerPaysCurrency?: string TakerPaysCurrency?: string;
TakerPaysIssuer?: string TakerPaysIssuer?: string;
TakerGetsCurrency?: string TakerGetsCurrency?: string;
TakerGetsIssuer?: string TakerGetsIssuer?: string;
} }

View File

@@ -1,18 +1,18 @@
import { BaseLedgerEntry } from "./baseLedgerEntry"; import { BaseLedgerEntry } from "./baseLedgerEntry";
export interface Escrow extends BaseLedgerEntry { export interface Escrow extends BaseLedgerEntry {
LedgerEntryType: 'Escrow' LedgerEntryType: "Escrow";
Account: string Account: string;
Destination: string Destination: string;
Amount: string Amount: string;
Condition?: string Condition?: string;
CancelAfter?: number CancelAfter?: number;
FinishAfter?: number FinishAfter?: number;
Flags: number Flags: number;
SourceTag?: number SourceTag?: number;
DestinationTag?: number DestinationTag?: number;
OwnerNode: string OwnerNode: string;
DestinationNode?: string DestinationNode?: string;
PreviousTxnID: string PreviousTxnID: string;
PreviousTxnLgrSeq: number PreviousTxnLgrSeq: number;
} }

Some files were not shown because too many files have changed in this diff Show More