feat: Jest Test Runner (#2170)

This commit is contained in:
justinr1234
2023-02-03 17:03:07 -06:00
committed by GitHub
parent 5a63f18faf
commit 5fe480ece4
229 changed files with 13497 additions and 17033 deletions

View File

@@ -57,7 +57,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [12.x, 14.x, 16.x, 18.x] node-version: [14.x, 16.x, 18.x]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -98,7 +98,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [12.x, 14.x, 16.x, 18.x] node-version: [14.x, 16.x, 18.x]
services: services:
rippled: rippled:

4
.ncurc.json Normal file
View File

@@ -0,0 +1,4 @@
{
"reject": [
]
}

2
.nvmrc
View File

@@ -1 +1 @@
v12 v14

View File

@@ -39,4 +39,12 @@
"**/.git/subtree-cache/**": true, "**/.git/subtree-cache/**": true,
"**/.hg/store/**": true "**/.hg/store/**": true
}, },
"search.exclude": {
"**/.git": true,
"**/node_modules": true,
"**/tmp": true,
"**/docs/**/*.html": true,
"**/fixtures/**/*.json": true,
"**/docs/assets": true
},
} }

View File

@@ -122,8 +122,6 @@ If your code connects to the ledger (ex. Adding a new transaction type) it's han
All integration tests should be written in the `test/integration` folder, with new `Requests` and `Transactions` tests being in their respective folders. All integration tests should be written in the `test/integration` folder, with new `Requests` and `Transactions` tests being in their respective folders.
One last note for integration tests is that all imports from `xrpl.js` should be from `xrpl-local` instead of `../../src`. This is required for the integraiton tests to run in the browser.
For an example of how to write an integration test for `xrpl.js`, you can look at the [Payment integration test](./packages/xrpl/test/integration/transactions/payment.ts). For an example of how to write an integration test for `xrpl.js`, you can look at the [Payment integration test](./packages/xrpl/test/integration/transactions/payment.ts).
## Generate reference docs ## Generate reference docs

View File

@@ -25,7 +25,7 @@ All of which works in Node.js (tested for v14+) & web browsers (tested for Chrom
### Requirements ### Requirements
- **[Node.js v14](https://nodejs.org/)** is recommended. We also support v12 and v16. Other versions may work but are not frequently tested. + **[Node.js v14](https://nodejs.org/)** is recommended. We also support v16 and v18. Other versions may work but are not frequently tested.
### Installing xrpl.js ### Installing xrpl.js

20
jest.config.base.js Normal file
View File

@@ -0,0 +1,20 @@
const { TextDecoder, TextEncoder } = require("util");
module.exports = {
roots: ["<rootDir>/src"],
transform: {
"^.+\\.ts$": "ts-jest",
},
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
collectCoverage: true,
verbose: true,
testEnvironment: "node",
globals: {
TextDecoder: TextDecoder,
TextEncoder: TextEncoder,
error: console.error,
warn: console.warn,
info: console.info,
debug: console.debug,
},
};

8
jest.config.js Normal file
View File

@@ -0,0 +1,8 @@
const path = require("path");
const base = require("./jest.config.base.js");
module.exports = {
...base,
projects: ["<rootDir>/packages/**/jest.config.js"],
coverageDirectory: "<rootDir>/coverage/",
};

17071
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,9 @@
"lint": "lerna run lint --stream", "lint": "lerna run lint --stream",
"clean": "lerna run clean --stream", "clean": "lerna run clean --stream",
"build": "lerna run build --stream", "build": "lerna run build --stream",
"docgen": "lerna run docgen --stream" "docgen": "lerna run docgen --stream",
"update:check": "npx npm-check-updates --configFileName .ncurc.json",
"update:confirm": "npx npm-check-updates --configFileName .ncurc.json -u"
}, },
"dependencies": { "dependencies": {
"ripple-address-codec": "file:packages/ripple-address-codec", "ripple-address-codec": "file:packages/ripple-address-codec",
@@ -17,11 +19,12 @@
"xrpl": "file:packages/xrpl" "xrpl": "file:packages/xrpl"
}, },
"devDependencies": { "devDependencies": {
"@types/brorand": "^1.0.30",
"@types/chai": "^4.2.21", "@types/chai": "^4.2.21",
"@types/create-hash": "^1.2.2", "@types/create-hash": "^1.2.2",
"@types/jest": "^29.2.2",
"@types/lodash": "^4.14.136", "@types/lodash": "^4.14.136",
"@types/mocha": "^9.0.0", "@types/node": "^14.18.35",
"@types/node": "^17.0.14",
"@types/puppeteer": "5.4.6", "@types/puppeteer": "5.4.6",
"@types/ws": "^8.2.0", "@types/ws": "^8.2.0",
"@typescript-eslint/eslint-plugin": "^4.30.0", "@typescript-eslint/eslint-plugin": "^4.30.0",
@@ -40,15 +43,15 @@
"eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-import": "^2.24.1", "eslint-plugin-import": "^2.24.1",
"eslint-plugin-jsdoc": "^37.1.0", "eslint-plugin-jsdoc": "^37.1.0",
"eslint-plugin-mocha": "^10.0.3",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-tsdoc": "^0.2.14", "eslint-plugin-tsdoc": "^0.2.14",
"eventemitter2": "^6.0.0", "eventemitter2": "^6.0.0",
"expect": "^29.3.1",
"https-browserify": "^1.0.0", "https-browserify": "^1.0.0",
"jest": "^26.0.1", "jest": "^29.3.1",
"jest-mock": "^29.3.1",
"lerna": "^4.0.0", "lerna": "^4.0.0",
"mocha": "^10",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"nyc": "^15", "nyc": "^15",
"path-browserify": "1.0.1", "path-browserify": "1.0.1",
@@ -59,7 +62,7 @@
"source-map-support": "^0.5.16", "source-map-support": "^0.5.16",
"stream-browserify": "^3.0.0", "stream-browserify": "^3.0.0",
"stream-http": "3.2.0", "stream-http": "3.2.0",
"ts-jest": "^26.4.4", "ts-jest": "^29.0.3",
"ts-loader": "^9.2.5", "ts-loader": "^9.2.5",
"ts-node": "^10.2.1", "ts-node": "^10.2.1",
"typescript": "^4.4.2", "typescript": "^4.4.2",
@@ -69,7 +72,7 @@
"webpack-cli": "^4.2.0" "webpack-cli": "^4.2.0"
}, },
"workspaces": [ "workspaces": [
"packages/*" "./packages/*"
], ],
"engines": { "engines": {
"node": ">=12.0.0", "node": ">=12.0.0",

View File

@@ -40,5 +40,6 @@ module.exports = {
'jsdoc/require-jsdoc': 'off', 'jsdoc/require-jsdoc': 'off',
'jsdoc/require-param': 'off', 'jsdoc/require-param': 'off',
'tsdoc/syntax': 'off', 'tsdoc/syntax': 'off',
'@typescript-eslint/no-require-imports': 'off',
}, },
} }

View File

@@ -1,6 +1,8 @@
# ripple-address-codec # ripple-address-codec
## Unreleased ## Unreleased
### Changed
- All tests now use the Jest test runner and have been refactored for consistency across all packages
## 4.2.4 (2022-04-21) ## 4.2.4 (2022-04-21)
### Fixed ### Fixed

View File

@@ -1,8 +1,7 @@
// Jest configuration for api
const base = require('../../jest.config.base.js')
module.exports = { module.exports = {
"roots": [ ...base,
"<rootDir>/src" displayName: 'ripple-address-codec',
], }
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
}

View File

@@ -0,0 +1,245 @@
{
"name": "ripple-address-codec",
"version": "4.2.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ripple-address-codec",
"version": "4.2.4",
"license": "ISC",
"dependencies": {
"base-x": "^3.0.9",
"create-hash": "^1.1.2"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/base-x": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz",
"integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/cipher-base": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
"integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
"dependencies": {
"inherits": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/create-hash": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
"dependencies": {
"cipher-base": "^1.0.1",
"inherits": "^2.0.1",
"md5.js": "^1.3.4",
"ripemd160": "^2.0.1",
"sha.js": "^2.4.0"
}
},
"node_modules/hash-base": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
"integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==",
"dependencies": {
"inherits": "^2.0.4",
"readable-stream": "^3.6.0",
"safe-buffer": "^5.2.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
"integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
"dependencies": {
"hash-base": "^3.0.0",
"inherits": "^2.0.1",
"safe-buffer": "^5.1.2"
}
},
"node_modules/readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/ripemd160": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
"integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
"dependencies": {
"hash-base": "^3.0.0",
"inherits": "^2.0.1"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/sha.js": {
"version": "2.4.11",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
"dependencies": {
"inherits": "^2.0.1",
"safe-buffer": "^5.0.1"
},
"bin": {
"sha.js": "bin.js"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
}
},
"dependencies": {
"base-x": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz",
"integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==",
"requires": {
"safe-buffer": "^5.0.1"
}
},
"cipher-base": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
"integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
"requires": {
"inherits": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"create-hash": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
"requires": {
"cipher-base": "^1.0.1",
"inherits": "^2.0.1",
"md5.js": "^1.3.4",
"ripemd160": "^2.0.1",
"sha.js": "^2.4.0"
}
},
"hash-base": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
"integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==",
"requires": {
"inherits": "^2.0.4",
"readable-stream": "^3.6.0",
"safe-buffer": "^5.2.0"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
"integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
"requires": {
"hash-base": "^3.0.0",
"inherits": "^2.0.1",
"safe-buffer": "^5.1.2"
}
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
},
"ripemd160": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
"integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
"requires": {
"hash-base": "^3.0.0",
"inherits": "^2.0.1"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"sha.js": {
"version": "2.4.11",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
"requires": {
"inherits": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"requires": {
"safe-buffer": "~5.2.0"
}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
}
}
}

View File

@@ -10,7 +10,7 @@
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"base-x": "3.0.9", "base-x": "^3.0.9",
"create-hash": "^1.1.2" "create-hash": "^1.1.2"
}, },
"repository": { "repository": {
@@ -21,7 +21,7 @@
"prepublishOnly": "tslint -b ./ && jest", "prepublishOnly": "tslint -b ./ && jest",
"scripts": { "scripts": {
"build": "tsc -b", "build": "tsc -b",
"test": "jest", "test": "jest --verbose false --silent=false ./src/*.test.js",
"lint": "eslint . --ext .ts", "lint": "eslint . --ext .ts",
"clean": "rm -rf ./dist && rm -rf tsconfig.tsbuildinfo" "clean": "rm -rf ./dist && rm -rf tsconfig.tsbuildinfo"
}, },

View File

@@ -2,7 +2,7 @@ const {
classicAddressToXAddress, classicAddressToXAddress,
xAddressToClassicAddress, xAddressToClassicAddress,
isValidXAddress, isValidXAddress,
encodeXAddress encodeXAddress,
} = require('./index') } = require('./index')
const testCases = [ const testCases = [
@@ -10,137 +10,137 @@ const testCases = [
'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59',
false, false,
'X7AcgcsBL6XDcUb289X4mJ8djcdyKaB5hJDWMArnXr61cqZ', 'X7AcgcsBL6XDcUb289X4mJ8djcdyKaB5hJDWMArnXr61cqZ',
'T719a5UwUCnEs54UsxG9CJYYDhwmFCqkr7wxCcNcfZ6p5GZ' 'T719a5UwUCnEs54UsxG9CJYYDhwmFCqkr7wxCcNcfZ6p5GZ',
], ],
[ [
'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59',
1, 1,
'X7AcgcsBL6XDcUb289X4mJ8djcdyKaGZMhc9YTE92ehJ2Fu', 'X7AcgcsBL6XDcUb289X4mJ8djcdyKaGZMhc9YTE92ehJ2Fu',
'T719a5UwUCnEs54UsxG9CJYYDhwmFCvbJNZbi37gBGkRkbE' 'T719a5UwUCnEs54UsxG9CJYYDhwmFCvbJNZbi37gBGkRkbE',
], ],
[ [
'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59',
14, 14,
'X7AcgcsBL6XDcUb289X4mJ8djcdyKaGo2K5VpXpmCqbV2gS', 'X7AcgcsBL6XDcUb289X4mJ8djcdyKaGo2K5VpXpmCqbV2gS',
'T719a5UwUCnEs54UsxG9CJYYDhwmFCvqXVCALUGJGSbNV3x' 'T719a5UwUCnEs54UsxG9CJYYDhwmFCvqXVCALUGJGSbNV3x',
], ],
[ [
'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59',
11747, 11747,
'X7AcgcsBL6XDcUb289X4mJ8djcdyKaLFuhLRuNXPrDeJd9A', 'X7AcgcsBL6XDcUb289X4mJ8djcdyKaLFuhLRuNXPrDeJd9A',
'T719a5UwUCnEs54UsxG9CJYYDhwmFCziiNHtUukubF2Mg6t' 'T719a5UwUCnEs54UsxG9CJYYDhwmFCziiNHtUukubF2Mg6t',
], ],
[ [
'rLczgQHxPhWtjkaQqn3Q6UM8AbRbbRvs5K', 'rLczgQHxPhWtjkaQqn3Q6UM8AbRbbRvs5K',
false, false,
'XVZVpQj8YSVpNyiwXYSqvQoQqgBttTxAZwMcuJd4xteQHyt', 'XVZVpQj8YSVpNyiwXYSqvQoQqgBttTxAZwMcuJd4xteQHyt',
'TVVrSWtmQQssgVcmoMBcFQZKKf56QscyWLKnUyiuZW8ALU4' 'TVVrSWtmQQssgVcmoMBcFQZKKf56QscyWLKnUyiuZW8ALU4',
], ],
[ [
'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo',
false, false,
'X7YenJqxv3L66CwhBSfd3N8RzGXxYqPopMGMsCcpho79rex', 'X7YenJqxv3L66CwhBSfd3N8RzGXxYqPopMGMsCcpho79rex',
'T77wVQzA8ntj9wvCTNiQpNYLT5hmhRsFyXDoMLqYC4BzQtV' 'T77wVQzA8ntj9wvCTNiQpNYLT5hmhRsFyXDoMLqYC4BzQtV',
], ],
[ [
'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo',
58, 58,
'X7YenJqxv3L66CwhBSfd3N8RzGXxYqV56ZkTCa9UCzgaao1', 'X7YenJqxv3L66CwhBSfd3N8RzGXxYqV56ZkTCa9UCzgaao1',
'T77wVQzA8ntj9wvCTNiQpNYLT5hmhR9kej6uxm4jGcQD7rZ' 'T77wVQzA8ntj9wvCTNiQpNYLT5hmhR9kej6uxm4jGcQD7rZ',
], ],
[ [
'rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW', 'rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW',
23480, 23480,
'X7d3eHCXzwBeWrZec1yT24iZerQjYL8m8zCJ16ACxu1BrBY', 'X7d3eHCXzwBeWrZec1yT24iZerQjYL8m8zCJ16ACxu1BrBY',
'T7YChPFWifjCAXLEtg5N74c7fSAYsvSokwcmBPBUZWhxH5P' 'T7YChPFWifjCAXLEtg5N74c7fSAYsvSokwcmBPBUZWhxH5P',
], ],
[ [
'rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW', 'rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW',
11747, 11747,
'X7d3eHCXzwBeWrZec1yT24iZerQjYLo2CJf8oVC5CMWey5m', 'X7d3eHCXzwBeWrZec1yT24iZerQjYLo2CJf8oVC5CMWey5m',
'T7YChPFWifjCAXLEtg5N74c7fSAYsvTcc7nEfwuEEvn5Q4w' 'T7YChPFWifjCAXLEtg5N74c7fSAYsvTcc7nEfwuEEvn5Q4w',
], ],
[ [
'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf', 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf',
false, false,
'XVLhHMPHU98es4dbozjVtdWzVrDjtV5fdx1mHp98tDMoQXb', 'XVLhHMPHU98es4dbozjVtdWzVrDjtV5fdx1mHp98tDMoQXb',
'TVE26TYGhfLC7tQDno7G8dGtxSkYQn49b3qD26PK7FcGSKE' 'TVE26TYGhfLC7tQDno7G8dGtxSkYQn49b3qD26PK7FcGSKE',
], ],
[ [
'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf', 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf',
0, 0,
'XVLhHMPHU98es4dbozjVtdWzVrDjtV8AqEL4xcZj5whKbmc', 'XVLhHMPHU98es4dbozjVtdWzVrDjtV8AqEL4xcZj5whKbmc',
'TVE26TYGhfLC7tQDno7G8dGtxSkYQnSy8RHqGHoGJ59spi2' 'TVE26TYGhfLC7tQDno7G8dGtxSkYQnSy8RHqGHoGJ59spi2',
], ],
[ [
'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf', 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf',
1, 1,
'XVLhHMPHU98es4dbozjVtdWzVrDjtV8xvjGQTYPiAx6gwDC', 'XVLhHMPHU98es4dbozjVtdWzVrDjtV8xvjGQTYPiAx6gwDC',
'TVE26TYGhfLC7tQDno7G8dGtxSkYQnSz1uDimDdPYXzSpyw' 'TVE26TYGhfLC7tQDno7G8dGtxSkYQnSz1uDimDdPYXzSpyw',
], ],
[ [
'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf', 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf',
2, 2,
'XVLhHMPHU98es4dbozjVtdWzVrDjtV8zpDURx7DzBCkrQE7', 'XVLhHMPHU98es4dbozjVtdWzVrDjtV8zpDURx7DzBCkrQE7',
'TVE26TYGhfLC7tQDno7G8dGtxSkYQnTryP9tG9TW8GeMBmd' 'TVE26TYGhfLC7tQDno7G8dGtxSkYQnTryP9tG9TW8GeMBmd',
], ],
[ [
'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf', 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf',
32, 32,
'XVLhHMPHU98es4dbozjVtdWzVrDjtVoYiC9UvKfjKar4LJe', 'XVLhHMPHU98es4dbozjVtdWzVrDjtVoYiC9UvKfjKar4LJe',
'TVE26TYGhfLC7tQDno7G8dGtxSkYQnT2oqaCDzMEuCDAj1j' 'TVE26TYGhfLC7tQDno7G8dGtxSkYQnT2oqaCDzMEuCDAj1j',
], ],
[ [
'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf', 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf',
276, 276,
'XVLhHMPHU98es4dbozjVtdWzVrDjtVoKj3MnFGMXEFMnvJV', 'XVLhHMPHU98es4dbozjVtdWzVrDjtVoKj3MnFGMXEFMnvJV',
'TVE26TYGhfLC7tQDno7G8dGtxSkYQnTMgJJYfAbsiPsc6Zg' 'TVE26TYGhfLC7tQDno7G8dGtxSkYQnTMgJJYfAbsiPsc6Zg',
], ],
[ [
'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf', 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf',
65591, 65591,
'XVLhHMPHU98es4dbozjVtdWzVrDjtVozpjdhPQVdt3ghaWw', 'XVLhHMPHU98es4dbozjVtdWzVrDjtVozpjdhPQVdt3ghaWw',
'TVE26TYGhfLC7tQDno7G8dGtxSkYQn7ryu2W6njw7mT1jmS' 'TVE26TYGhfLC7tQDno7G8dGtxSkYQn7ryu2W6njw7mT1jmS',
], ],
[ [
'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf', 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf',
16781933, 16781933,
'XVLhHMPHU98es4dbozjVtdWzVrDjtVqrDUk2vDpkTjPsY73', 'XVLhHMPHU98es4dbozjVtdWzVrDjtVqrDUk2vDpkTjPsY73',
'TVE26TYGhfLC7tQDno7G8dGtxSkYQnVsw45sDtGHhLi27Qa' 'TVE26TYGhfLC7tQDno7G8dGtxSkYQnVsw45sDtGHhLi27Qa',
], ],
[ [
'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf', 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf',
4294967294, 4294967294,
'XVLhHMPHU98es4dbozjVtdWzVrDjtV1kAsixQTdMjbWi39u', 'XVLhHMPHU98es4dbozjVtdWzVrDjtV1kAsixQTdMjbWi39u',
'TVE26TYGhfLC7tQDno7G8dGtxSkYQnX8tDFQ53itLNqs6vU' 'TVE26TYGhfLC7tQDno7G8dGtxSkYQnX8tDFQ53itLNqs6vU',
], ],
[ [
'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf', 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf',
4294967295, 4294967295,
'XVLhHMPHU98es4dbozjVtdWzVrDjtV18pX8yuPT7y4xaEHi', 'XVLhHMPHU98es4dbozjVtdWzVrDjtV18pX8yuPT7y4xaEHi',
'TVE26TYGhfLC7tQDno7G8dGtxSkYQnXoy6kSDh6rZzApc69' 'TVE26TYGhfLC7tQDno7G8dGtxSkYQnXoy6kSDh6rZzApc69',
], ],
[ [
'rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY', 'rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY',
false, false,
'XV5sbjUmgPpvXv4ixFWZ5ptAYZ6PD2gYsjNFQLKYW33DzBm', 'XV5sbjUmgPpvXv4ixFWZ5ptAYZ6PD2gYsjNFQLKYW33DzBm',
'TVd2rqMkYL2AyS97NdELcpeiprNBjwLZzuUG5rZnaewsahi' 'TVd2rqMkYL2AyS97NdELcpeiprNBjwLZzuUG5rZnaewsahi',
], ],
[ [
'rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY', 'rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY',
0, 0,
'XV5sbjUmgPpvXv4ixFWZ5ptAYZ6PD2m4Er6SnvjVLpMWPjR', 'XV5sbjUmgPpvXv4ixFWZ5ptAYZ6PD2m4Er6SnvjVLpMWPjR',
'TVd2rqMkYL2AyS97NdELcpeiprNBjwRQUBetPbyrvXSTuxU' 'TVd2rqMkYL2AyS97NdELcpeiprNBjwRQUBetPbyrvXSTuxU',
], ],
[ [
'rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY', 'rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY',
13371337, 13371337,
'XV5sbjUmgPpvXv4ixFWZ5ptAYZ6PD2qwGkhgc48zzcx6Gkr', 'XV5sbjUmgPpvXv4ixFWZ5ptAYZ6PD2qwGkhgc48zzcx6Gkr',
'TVd2rqMkYL2AyS97NdELcpeiprNBjwVUDvp3vhpXbNhLwJi' 'TVd2rqMkYL2AyS97NdELcpeiprNBjwVUDvp3vhpXbNhLwJi',
] ],
] ]
;[false, true].forEach(isTestAddress => { ;[false, true].forEach((isTestAddress) => {
const MAX_32_BIT_UNSIGNED_INT = 4294967295 const MAX_32_BIT_UNSIGNED_INT = 4294967295
const network = isTestAddress ? ' (test)' : ' (main)' const network = isTestAddress ? ' (test)' : ' (main)'
@@ -149,13 +149,17 @@ const testCases = [
const classicAddress = testCase[0] const classicAddress = testCase[0]
const tag = testCase[1] !== false ? testCase[1] : false const tag = testCase[1] !== false ? testCase[1] : false
const xAddress = isTestAddress ? testCase[3] : testCase[2] const xAddress = isTestAddress ? testCase[3] : testCase[2]
test(`Converts ${classicAddress}${tag ? ':' + tag : ''} to ${xAddress}${network}`, () => { test(`Converts ${classicAddress}${
expect(classicAddressToXAddress(classicAddress, tag, isTestAddress)).toBe(xAddress) tag ? `:${tag}` : ''
} to ${xAddress}${network}`, () => {
expect(classicAddressToXAddress(classicAddress, tag, isTestAddress)).toBe(
xAddress,
)
const myClassicAddress = xAddressToClassicAddress(xAddress) const myClassicAddress = xAddressToClassicAddress(xAddress)
expect(myClassicAddress).toEqual({ expect(myClassicAddress).toEqual({
classicAddress, classicAddress,
tag, tag,
test: isTestAddress test: isTestAddress,
}) })
expect(isValidXAddress(xAddress)).toBe(true) expect(isValidXAddress(xAddress)).toBe(true)
}) })
@@ -177,23 +181,28 @@ const testCases = [
test(`Invalid classic address: Converting ${classicAddress}${network} throws`, () => { test(`Invalid classic address: Converting ${classicAddress}${network} throws`, () => {
expect(() => { expect(() => {
classicAddressToXAddress(classicAddress, false, isTestAddress) classicAddressToXAddress(classicAddress, false, isTestAddress)
}).toThrowError(new Error('invalid_input_size: decoded data must have length >= 5')) }).toThrowError(
new Error('invalid_input_size: decoded data must have length >= 5'),
)
}) })
} }
{ {
const highAndLowAccounts = [ const highAndLowAccounts = [
Buffer.from('00'.repeat(20), 'hex'), Buffer.from('00'.repeat(20), 'hex'),
Buffer.from('00'.repeat(19) + '01', 'hex'), Buffer.from(`${'00'.repeat(19)}01`, 'hex'),
Buffer.from('01'.repeat(20), 'hex'), Buffer.from('01'.repeat(20), 'hex'),
Buffer.from('FF'.repeat(20), 'hex') Buffer.from('FF'.repeat(20), 'hex'),
] ]
highAndLowAccounts.forEach(accountId => { highAndLowAccounts.forEach((accountId) => {
[false, 0, 1, MAX_32_BIT_UNSIGNED_INT].forEach(t => { const testCases = [false, 0, 1, MAX_32_BIT_UNSIGNED_INT]
const tag = (t | false) testCases.forEach((t) => {
const tag = t | false
const xAddress = encodeXAddress(accountId, tag, isTestAddress) const xAddress = encodeXAddress(accountId, tag, isTestAddress)
test(`Encoding ${accountId.toString('hex')}${tag ? ':' + tag : ''} to ${xAddress} has expected length`, () => { test(`Encoding ${accountId.toString('hex')}${
tag ? `:${tag}` : ''
} to ${xAddress} has expected length`, () => {
expect(xAddress.length).toBe(47) expect(xAddress.length).toBe(47)
}) })
}) })
@@ -237,7 +246,9 @@ test(`Invalid Account ID throws`, () => {
}) })
test(`isValidXAddress returns false for invalid X-address`, () => { test(`isValidXAddress returns false for invalid X-address`, () => {
expect(isValidXAddress('XVLhHMPHU98es4dbozjVtdWzVrDjtV18pX8zeUygYrCgrPh')).toBe(false) expect(
isValidXAddress('XVLhHMPHU98es4dbozjVtdWzVrDjtV18pX8zeUygYrCgrPh'),
).toBe(false)
}) })
test(`Converts X7AcgcsBL6XDcUb... to r9cZA1mLK5R5A... and tag: false`, () => { test(`Converts X7AcgcsBL6XDcUb... to r9cZA1mLK5R5A... and tag: false`, () => {
@@ -245,12 +256,14 @@ test(`Converts X7AcgcsBL6XDcUb... to r9cZA1mLK5R5A... and tag: false`, () => {
const tag = false const tag = false
const xAddress = 'X7AcgcsBL6XDcUb289X4mJ8djcdyKaB5hJDWMArnXr61cqZ' const xAddress = 'X7AcgcsBL6XDcUb289X4mJ8djcdyKaB5hJDWMArnXr61cqZ'
const isTestAddress = false const isTestAddress = false
expect(classicAddressToXAddress(classicAddress, tag, isTestAddress)).toBe(xAddress) expect(classicAddressToXAddress(classicAddress, tag, isTestAddress)).toBe(
xAddress,
)
const myClassicAddress = xAddressToClassicAddress(xAddress) const myClassicAddress = xAddressToClassicAddress(xAddress)
expect(myClassicAddress).toEqual({ expect(myClassicAddress).toEqual({
classicAddress, classicAddress,
tag, tag,
test: isTestAddress test: isTestAddress,
}) })
expect(isValidXAddress(xAddress)).toBe(true) expect(isValidXAddress(xAddress)).toBe(true)

View File

@@ -119,6 +119,7 @@ function isBufferForTestAddress(buf: Buffer): boolean {
if (PREFIX_BYTES.test.equals(decodedPrefix)) { if (PREFIX_BYTES.test.equals(decodedPrefix)) {
return true return true
} }
throw new Error('Invalid X-address: bad prefix') throw new Error('Invalid X-address: bad prefix')
} }

View File

@@ -17,28 +17,38 @@ function toBytes(hex) {
* @param hex Hexadecimal representation of expected decoded data * @param hex Hexadecimal representation of expected decoded data
*/ */
function makeEncodeDecodeTest(encoder, decoder, base58, hex) { function makeEncodeDecodeTest(encoder, decoder, base58, hex) {
test(`can translate between ${hex} and ${base58}`, function() { test(`can translate between ${hex} and ${base58}`, function () {
const actual = encoder(toBytes(hex)) const actual = encoder(toBytes(hex))
expect(actual).toBe(base58) expect(actual).toBe(base58)
}) })
test(`can translate between ${base58} and ${hex})`, function() { test(`can translate between ${base58} and ${hex})`, function () {
const buf = decoder(base58) const buf = decoder(base58)
expect(toHex(buf)).toBe(hex) expect(toHex(buf)).toBe(hex)
}) })
} }
makeEncodeDecodeTest(api.encodeAccountID, api.decodeAccountID, 'rJrRMgiRgrU6hDF4pgu5DXQdWyPbY35ErN', makeEncodeDecodeTest(
'BA8E78626EE42C41B46D46C3048DF3A1C3C87072') api.encodeAccountID,
api.decodeAccountID,
'rJrRMgiRgrU6hDF4pgu5DXQdWyPbY35ErN',
'BA8E78626EE42C41B46D46C3048DF3A1C3C87072',
)
makeEncodeDecodeTest(api.encodeNodePublic, api.decodeNodePublic, makeEncodeDecodeTest(
api.encodeNodePublic,
api.decodeNodePublic,
'n9MXXueo837zYH36DvMc13BwHcqtfAWNJY5czWVbp7uYTj7x17TH', 'n9MXXueo837zYH36DvMc13BwHcqtfAWNJY5czWVbp7uYTj7x17TH',
'0388E5BA87A000CB807240DF8C848EB0B5FFA5C8E5A521BC8E105C0F0A44217828') '0388E5BA87A000CB807240DF8C848EB0B5FFA5C8E5A521BC8E105C0F0A44217828',
)
makeEncodeDecodeTest(api.encodeAccountPublic, api.decodeAccountPublic, makeEncodeDecodeTest(
'aB44YfzW24VDEJQ2UuLPV2PvqcPCSoLnL7y5M1EzhdW4LnK5xMS3', api.encodeAccountPublic,
'023693F15967AE357D0327974AD46FE3C127113B1110D6044FD41E723689F81CC6') api.decodeAccountPublic,
'aB44YfzW24VDEJQ2UuLPV2PvqcPCSoLnL7y5M1EzhdW4LnK5xMS3',
'023693F15967AE357D0327974AD46FE3C127113B1110D6044FD41E723689F81CC6',
)
test('can decode arbitrary seeds', function() { test('can decode arbitrary seeds', function () {
const decoded = api.decodeSeed('sEdTM1uX8pu2do5XvTnutH6HsouMaM2') const decoded = api.decodeSeed('sEdTM1uX8pu2do5XvTnutH6HsouMaM2')
expect(toHex(decoded.bytes)).toBe('4C3A1D213FBDFB14C7C28D609469B341') expect(toHex(decoded.bytes)).toBe('4C3A1D213FBDFB14C7C28D609469B341')
expect(decoded.type).toBe('ed25519') expect(decoded.type).toBe('ed25519')
@@ -48,7 +58,7 @@ test('can decode arbitrary seeds', function() {
expect(decoded2.type).toBe('secp256k1') expect(decoded2.type).toBe('secp256k1')
}) })
test('can pass a type as second arg to encodeSeed', function() { test('can pass a type as second arg to encodeSeed', function () {
const edSeed = 'sEdTM1uX8pu2do5XvTnutH6HsouMaM2' const edSeed = 'sEdTM1uX8pu2do5XvTnutH6HsouMaM2'
const decoded = api.decodeSeed(edSeed) const decoded = api.decodeSeed(edSeed)
const type = 'ed25519' const type = 'ed25519'
@@ -57,125 +67,161 @@ test('can pass a type as second arg to encodeSeed', function() {
expect(api.encodeSeed(decoded.bytes, type)).toBe(edSeed) expect(api.encodeSeed(decoded.bytes, type)).toBe(edSeed)
}) })
test('isValidClassicAddress - secp256k1 address valid', function() { test('isValidClassicAddress - secp256k1 address valid', function () {
expect(api.isValidClassicAddress('rU6K7V3Po4snVhBBaU29sesqs2qTQJWDw1')).toBe(true) expect(api.isValidClassicAddress('rU6K7V3Po4snVhBBaU29sesqs2qTQJWDw1')).toBe(
true,
)
}) })
test('isValidClassicAddress - ed25519 address valid', function() { test('isValidClassicAddress - ed25519 address valid', function () {
expect(api.isValidClassicAddress('rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD')).toBe(true) expect(api.isValidClassicAddress('rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD')).toBe(
true,
)
}) })
test('isValidClassicAddress - invalid', function() { test('isValidClassicAddress - invalid', function () {
expect(api.isValidClassicAddress('rU6K7V3Po4snVhBBaU29sesqs2qTQJWDw2')).toBe(false) expect(api.isValidClassicAddress('rU6K7V3Po4snVhBBaU29sesqs2qTQJWDw2')).toBe(
false,
)
}) })
test('isValidClassicAddress - empty', function() { test('isValidClassicAddress - empty', function () {
expect(api.isValidClassicAddress('')).toBe(false) expect(api.isValidClassicAddress('')).toBe(false)
}) })
describe('encodeSeed', function() { describe('encodeSeed', function () {
it('encodes a secp256k1 seed', function () {
it('encodes a secp256k1 seed', function() { const result = api.encodeSeed(
const result = api.encodeSeed(Buffer.from('CF2DE378FBDD7E2EE87D486DFB5A7BFF', 'hex'), 'secp256k1') Buffer.from('CF2DE378FBDD7E2EE87D486DFB5A7BFF', 'hex'),
'secp256k1',
)
expect(result).toBe('sn259rEFXrQrWyx3Q7XneWcwV6dfL') expect(result).toBe('sn259rEFXrQrWyx3Q7XneWcwV6dfL')
}) })
it('encodes low secp256k1 seed', function() { it('encodes low secp256k1 seed', function () {
const result = api.encodeSeed(Buffer.from('00000000000000000000000000000000', 'hex'), 'secp256k1') const result = api.encodeSeed(
Buffer.from('00000000000000000000000000000000', 'hex'),
'secp256k1',
)
expect(result).toBe('sp6JS7f14BuwFY8Mw6bTtLKWauoUs') expect(result).toBe('sp6JS7f14BuwFY8Mw6bTtLKWauoUs')
}) })
it('encodes high secp256k1 seed', function() { it('encodes high secp256k1 seed', function () {
const result = api.encodeSeed(Buffer.from('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 'hex'), 'secp256k1') const result = api.encodeSeed(
Buffer.from('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 'hex'),
'secp256k1',
)
expect(result).toBe('saGwBRReqUNKuWNLpUAq8i8NkXEPN') expect(result).toBe('saGwBRReqUNKuWNLpUAq8i8NkXEPN')
}) })
it('encodes an ed25519 seed', function() { it('encodes an ed25519 seed', function () {
const result = api.encodeSeed(Buffer.from('4C3A1D213FBDFB14C7C28D609469B341', 'hex'), 'ed25519') const result = api.encodeSeed(
Buffer.from('4C3A1D213FBDFB14C7C28D609469B341', 'hex'),
'ed25519',
)
expect(result).toBe('sEdTM1uX8pu2do5XvTnutH6HsouMaM2') expect(result).toBe('sEdTM1uX8pu2do5XvTnutH6HsouMaM2')
}) })
it('encodes low ed25519 seed', function() { it('encodes low ed25519 seed', function () {
const result = api.encodeSeed(Buffer.from('00000000000000000000000000000000', 'hex'), 'ed25519') const result = api.encodeSeed(
Buffer.from('00000000000000000000000000000000', 'hex'),
'ed25519',
)
expect(result).toBe('sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE') expect(result).toBe('sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE')
}) })
it('encodes high ed25519 seed', function() { it('encodes high ed25519 seed', function () {
const result = api.encodeSeed(Buffer.from('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 'hex'), 'ed25519') const result = api.encodeSeed(
Buffer.from('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 'hex'),
'ed25519',
)
expect(result).toBe('sEdV19BLfeQeKdEXyYA4NhjPJe6XBfG') expect(result).toBe('sEdV19BLfeQeKdEXyYA4NhjPJe6XBfG')
}) })
test('attempting to encode a seed with less than 16 bytes of entropy throws', function() { test('attempting to encode a seed with less than 16 bytes of entropy throws', function () {
expect(() => { expect(() => {
api.encodeSeed(Buffer.from('CF2DE378FBDD7E2EE87D486DFB5A7B', 'hex'), 'secp256k1') api.encodeSeed(
Buffer.from('CF2DE378FBDD7E2EE87D486DFB5A7B', 'hex'),
'secp256k1',
)
}).toThrow('entropy must have length 16') }).toThrow('entropy must have length 16')
}) })
test('attempting to encode a seed with more than 16 bytes of entropy throws', function() { test('attempting to encode a seed with more than 16 bytes of entropy throws', function () {
expect(() => { expect(() => {
api.encodeSeed(Buffer.from('CF2DE378FBDD7E2EE87D486DFB5A7BFFFF', 'hex'), 'secp256k1') api.encodeSeed(
Buffer.from('CF2DE378FBDD7E2EE87D486DFB5A7BFFFF', 'hex'),
'secp256k1',
)
}).toThrow('entropy must have length 16') }).toThrow('entropy must have length 16')
}) })
}) })
describe('decodeSeed', function() { describe('decodeSeed', function () {
it('can decode an Ed25519 seed', function () {
it('can decode an Ed25519 seed', function() {
const decoded = api.decodeSeed('sEdTM1uX8pu2do5XvTnutH6HsouMaM2') const decoded = api.decodeSeed('sEdTM1uX8pu2do5XvTnutH6HsouMaM2')
expect(toHex(decoded.bytes)).toBe('4C3A1D213FBDFB14C7C28D609469B341') expect(toHex(decoded.bytes)).toBe('4C3A1D213FBDFB14C7C28D609469B341')
expect(decoded.type).toBe('ed25519') expect(decoded.type).toBe('ed25519')
}) })
it('can decode a secp256k1 seed', function() { it('can decode a secp256k1 seed', function () {
const decoded = api.decodeSeed('sn259rEFXrQrWyx3Q7XneWcwV6dfL') const decoded = api.decodeSeed('sn259rEFXrQrWyx3Q7XneWcwV6dfL')
expect(toHex(decoded.bytes)).toBe('CF2DE378FBDD7E2EE87D486DFB5A7BFF') expect(toHex(decoded.bytes)).toBe('CF2DE378FBDD7E2EE87D486DFB5A7BFF')
expect(decoded.type).toBe('secp256k1') expect(decoded.type).toBe('secp256k1')
}) })
}) })
describe('encodeAccountID', function() { describe('encodeAccountID', function () {
it('can encode an AccountID', function () {
it('can encode an AccountID', function() { const encoded = api.encodeAccountID(
const encoded = api.encodeAccountID(Buffer.from('BA8E78626EE42C41B46D46C3048DF3A1C3C87072', 'hex')) Buffer.from('BA8E78626EE42C41B46D46C3048DF3A1C3C87072', 'hex'),
)
expect(encoded).toBe('rJrRMgiRgrU6hDF4pgu5DXQdWyPbY35ErN') expect(encoded).toBe('rJrRMgiRgrU6hDF4pgu5DXQdWyPbY35ErN')
}) })
test('unexpected length should throw', function() { test('unexpected length should throw', function () {
expect(() => { expect(() => {
api.encodeAccountID(Buffer.from('ABCDEF', 'hex')) api.encodeAccountID(Buffer.from('ABCDEF', 'hex'))
}).toThrow( }).toThrow(
'unexpected_payload_length: bytes.length does not match expectedLength' 'unexpected_payload_length: bytes.length does not match expectedLength',
) )
}) })
}) })
describe('decodeNodePublic', function() { describe('decodeNodePublic', function () {
it('can decode a NodePublic', function () {
it('can decode a NodePublic', function() { const decoded = api.decodeNodePublic(
const decoded = api.decodeNodePublic('n9MXXueo837zYH36DvMc13BwHcqtfAWNJY5czWVbp7uYTj7x17TH') 'n9MXXueo837zYH36DvMc13BwHcqtfAWNJY5czWVbp7uYTj7x17TH',
expect(toHex(decoded)).toBe('0388E5BA87A000CB807240DF8C848EB0B5FFA5C8E5A521BC8E105C0F0A44217828') )
expect(toHex(decoded)).toBe(
'0388E5BA87A000CB807240DF8C848EB0B5FFA5C8E5A521BC8E105C0F0A44217828',
)
}) })
}) })
test('encodes 123456789 with version byte of 0', () => { test('encodes 123456789 with version byte of 0', () => {
expect(api.codec.encode(Buffer.from('123456789'), { expect(
versions: [0], api.codec.encode(Buffer.from('123456789'), {
expectedLength: 9 versions: [0],
})).toBe('rnaC7gW34M77Kneb78s') expectedLength: 9,
}),
).toBe('rnaC7gW34M77Kneb78s')
}) })
test('multiple versions with no expected length should throw', () => { test('multiple versions with no expected length should throw', () => {
expect(() => { expect(() => {
api.codec.decode('rnaC7gW34M77Kneb78s', { api.codec.decode('rnaC7gW34M77Kneb78s', {
versions: [0, 1] versions: [0, 1],
}) })
}).toThrow('expectedLength is required because there are >= 2 possible versions') }).toThrow(
'expectedLength is required because there are >= 2 possible versions',
)
}) })
test('attempting to decode data with length < 5 should throw', () => { test('attempting to decode data with length < 5 should throw', () => {
expect(() => { expect(() => {
api.codec.decode('1234', { api.codec.decode('1234', {
versions: [0] versions: [0],
}) })
}).toThrow('invalid_input_size: decoded data must have length >= 5') }).toThrow('invalid_input_size: decoded data must have length >= 5')
}) })
@@ -183,15 +229,17 @@ test('attempting to decode data with length < 5 should throw', () => {
test('attempting to decode data with unexpected version should throw', () => { test('attempting to decode data with unexpected version should throw', () => {
expect(() => { expect(() => {
api.codec.decode('rnaC7gW34M77Kneb78s', { api.codec.decode('rnaC7gW34M77Kneb78s', {
versions: [2] versions: [2],
}) })
}).toThrow('version_invalid: version bytes do not match any of the provided version(s)') }).toThrow(
'version_invalid: version bytes do not match any of the provided version(s)',
)
}) })
test('invalid checksum should throw', () => { test('invalid checksum should throw', () => {
expect(() => { expect(() => {
api.codec.decode('123456789', { api.codec.decode('123456789', {
versions: [0, 1] versions: [0, 1],
}) })
}).toThrow('checksum_invalid') }).toThrow('checksum_invalid')
}) })
@@ -199,48 +247,51 @@ test('invalid checksum should throw', () => {
test('empty payload should throw', () => { test('empty payload should throw', () => {
expect(() => { expect(() => {
api.codec.decode('', { api.codec.decode('', {
versions: [0, 1] versions: [0, 1],
}) })
}).toThrow('invalid_input_size: decoded data must have length >= 5') }).toThrow('invalid_input_size: decoded data must have length >= 5')
}) })
test('decode data', () => { test('decode data', () => {
expect(api.codec.decode('rnaC7gW34M77Kneb78s', { expect(
versions: [0] api.codec.decode('rnaC7gW34M77Kneb78s', {
})).toStrictEqual({ versions: [0],
}),
).toStrictEqual({
version: [0], version: [0],
bytes: Buffer.from('123456789'), bytes: Buffer.from('123456789'),
type: null type: null,
}) })
}) })
test('decode data with expected length', function() { test('decode data with expected length', function () {
expect(api.codec.decode('rnaC7gW34M77Kneb78s', { expect(
api.codec.decode('rnaC7gW34M77Kneb78s', {
versions: [0], versions: [0],
expectedLength: 9 expectedLength: 9,
}) }),
).toStrictEqual({ ).toStrictEqual({
version: [0], version: [0],
bytes: Buffer.from('123456789'), bytes: Buffer.from('123456789'),
type: null type: null,
}) })
}) })
test('decode data with wrong expected length should throw', function() { test('decode data with wrong expected length should throw', function () {
expect(() => { expect(() => {
api.codec.decode('rnaC7gW34M77Kneb78s', { api.codec.decode('rnaC7gW34M77Kneb78s', {
versions: [0], versions: [0],
expectedLength: 8 expectedLength: 8,
}) })
}).toThrow( }).toThrow(
'version_invalid: version bytes do not match any of the provided version(s)' 'version_invalid: version bytes do not match any of the provided version(s)',
) )
expect(() => { expect(() => {
api.codec.decode('rnaC7gW34M77Kneb78s', { api.codec.decode('rnaC7gW34M77Kneb78s', {
versions: [0], versions: [0],
expectedLength: 10 expectedLength: 10,
}) })
}).toThrow( }).toThrow(
'version_invalid: version bytes do not match any of the provided version(s)' 'version_invalid: version bytes do not match any of the provided version(s)',
) )
}) })

View File

@@ -2,15 +2,16 @@
* Codec class * Codec class
*/ */
import * as baseCodec from 'base-x' import baseCodec = require('base-x')
import * as createHash from 'create-hash' import type { BaseConverter } from 'base-x'
import createHash = require('create-hash')
import { seqEqual, concatArgs } from './utils' import { seqEqual, concatArgs } from './utils'
class Codec { class Codec {
private readonly _sha256: (bytes: Uint8Array) => Buffer private readonly _sha256: (bytes: Uint8Array) => Buffer
private readonly _alphabet: string private readonly _alphabet: string
private readonly _codec: baseCodec.BaseConverter private readonly _codec: BaseConverter
public constructor(options: { public constructor(options: {
sha256: (bytes: Uint8Array) => Buffer sha256: (bytes: Uint8Array) => Buffer
@@ -56,7 +57,7 @@ class Codec {
): { ): {
version: number[] version: number[]
bytes: Buffer bytes: Buffer
type: string | null type: 'ed25519' | 'secp256k1' | null
} { } {
const versions = opts.versions const versions = opts.versions
const types = opts.versionTypes const types = opts.versionTypes
@@ -204,7 +205,7 @@ export function decodeSeed(
): { ): {
version: number[] version: number[]
bytes: Buffer bytes: Buffer
type: string | null type: 'ed25519' | 'secp256k1' | null
} { } {
return codecWithXrpAlphabet.decode(seed, opts) return codecWithXrpAlphabet.decode(seed, opts)
} }

View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"include": ["src/**/*.ts", "src/*.test.ts"]
}

View File

@@ -13,7 +13,8 @@
"preserveConstEnums": false, "preserveConstEnums": false,
"suppressImplicitAnyIndexErrors": false, "suppressImplicitAnyIndexErrors": false,
"skipLibCheck": true, "skipLibCheck": true,
"declaration": true "declaration": true,
"strictNullChecks": true
}, },
"include": ["src/**/*.ts"] "include": ["src/**/*.ts"]
} }

View File

@@ -14,6 +14,8 @@ module.exports = {
}, },
}, },
ignorePatterns: ['jest.config.js', '.eslintrc.js'],
// Specify global variables that are predefined // Specify global variables that are predefined
env: { env: {
browser: true, // Enable browser global variables browser: true, // Enable browser global variables
@@ -103,19 +105,19 @@ module.exports = {
'max-lines-per-function': 'off', 'max-lines-per-function': 'off',
'require-unicode-regexp': 'off', 'require-unicode-regexp': 'off',
'no-undef-init': 'off', 'no-undef-init': 'off',
'curly': 'off', curly: 'off',
'eqeqeq': 'off', eqeqeq: 'off',
'no-console': 'off', 'no-console': 'off',
'max-classes-per-file': 'off', 'max-classes-per-file': 'off',
'operator-assignment': 'off', 'operator-assignment': 'off',
'class-methods-use-this': 'off', 'class-methods-use-this': 'off',
'no-else-return': 'off', 'no-else-return': 'off',
'yoda': 'off', yoda: 'off',
'max-depth': 'off', 'max-depth': 'off',
'multiline-comment-style': 'off', 'multiline-comment-style': 'off',
'one-var': 'off', 'one-var': 'off',
'no-negated-condition': 'off', 'no-negated-condition': 'off',
'radix': 'off', radix: 'off',
'no-nested-ternary': 'off', 'no-nested-ternary': 'off',
'no-useless-concat': 'off', 'no-useless-concat': 'off',
'object-shorthand': 'off', 'object-shorthand': 'off',

View File

@@ -1 +0,0 @@
10.22.0

View File

@@ -1,6 +1,8 @@
# ripple-binary-codec Release History # ripple-binary-codec Release History
## Unreleased ## Unreleased
### Changed
- All tests now use the Jest test runner and have been refactored for consistency across all packages
## 1.4.2 (2022-06-27) ## 1.4.2 (2022-06-27)
- Fixed standard currency codes with lowercase and allowed symbols not decoding into standard codes. - Fixed standard currency codes with lowercase and allowed symbols not decoding into standard codes.

View File

@@ -0,0 +1,8 @@
// Jest configuration for api
const base = require('../../jest.config.base.js')
module.exports = {
...base,
roots: [...base.roots, '<rootDir>/test'],
displayName: 'ripple-binary-codec',
}

View File

@@ -23,7 +23,7 @@
"build": "tsc -b && copyfiles ./src/enums/definitions.json ./dist/enums/", "build": "tsc -b && copyfiles ./src/enums/definitions.json ./dist/enums/",
"clean": "rm -rf ./dist && rm -rf tsconfig.tsbuildinfo", "clean": "rm -rf ./dist && rm -rf tsconfig.tsbuildinfo",
"prepare": "npm run build && npm test", "prepare": "npm run build && npm test",
"test": "jest", "test": "jest --verbose false --silent=false ./test/*.test.js",
"lint": "eslint . --ext .ts --ext .test.js" "lint": "eslint . --ext .ts --ext .test.js"
}, },
"repository": { "repository": {
@@ -38,6 +38,6 @@
"readmeFilename": "README.md", "readmeFilename": "README.md",
"prettier": "@xrplf/prettier-config", "prettier": "@xrplf/prettier-config",
"engines": { "engines": {
"node": ">=10.22.0" "node": ">= 10"
} }
} }

View File

@@ -10,7 +10,7 @@ import { FieldInstance } from './enums'
import { STObject } from './types/st-object' import { STObject } from './types/st-object'
import { JsonObject } from './types/serialized-type' import { JsonObject } from './types/serialized-type'
import { Buffer } from 'buffer/' import { Buffer } from 'buffer/'
import * as bigInt from 'big-integer' import bigInt = require('big-integer')
/** /**
* Construct a BinaryParser * Construct a BinaryParser

View File

@@ -1,5 +1,5 @@
import { HashPrefix } from './hash-prefixes' import { HashPrefix } from './hash-prefixes'
import * as createHash from 'create-hash' import createHash = require('create-hash')
import { Hash256 } from './types/hash-256' import { Hash256 } from './types/hash-256'
import { BytesList } from './serdes/binary-serializer' import { BytesList } from './serdes/binary-serializer'
import { Buffer } from 'buffer/' import { Buffer } from 'buffer/'

View File

@@ -102,7 +102,7 @@ function decodeQuality(value: string): string {
return quality.decode(value).toString() return quality.decode(value).toString()
} }
export = { export {
decode, decode,
encode, encode,
encodeForSigning, encodeForSigning,

View File

@@ -10,7 +10,7 @@ import { UInt32 } from './types/uint-32'
import { UInt8 } from './types/uint-8' import { UInt8 } from './types/uint-8'
import { BinaryParser } from './serdes/binary-parser' import { BinaryParser } from './serdes/binary-parser'
import { JsonObject } from './types/serialized-type' import { JsonObject } from './types/serialized-type'
import * as bigInt from 'big-integer' import bigInt = require('big-integer')
/** /**
* Computes the hash of a list of objects * Computes the hash of a list of objects

View File

@@ -1,6 +1,6 @@
import { coreTypes } from './types' import { coreTypes } from './types'
import { Decimal } from 'decimal.js' import { Decimal } from 'decimal.js'
import * as bigInt from 'big-integer' import bigInt = require('big-integer')
import { Buffer } from 'buffer/' import { Buffer } from 'buffer/'
/** /**

View File

@@ -5,7 +5,7 @@ import { BinaryParser } from '../serdes/binary-parser'
import { AccountID } from './account-id' import { AccountID } from './account-id'
import { Currency } from './currency' import { Currency } from './currency'
import { JsonObject, SerializedType } from './serialized-type' import { JsonObject, SerializedType } from './serialized-type'
import * as bigInt from 'big-integer' import bigInt = require('big-integer')
import { Buffer } from 'buffer/' import { Buffer } from 'buffer/'
/** /**

View File

@@ -1,6 +1,6 @@
import { BytesList } from '../serdes/binary-serializer' import { BytesList } from '../serdes/binary-serializer'
import { BinaryParser } from '../serdes/binary-parser' import { BinaryParser } from '../serdes/binary-parser'
import * as bigInt from 'big-integer' import bigInt = require('big-integer')
import { Buffer } from 'buffer/' import { Buffer } from 'buffer/'
type JSON = string | number | boolean | null | undefined | JSON[] | JsonObject type JSON = string | number | boolean | null | undefined | JSON[] | JsonObject

View File

@@ -1,6 +1,6 @@
import { UInt } from './uint' import { UInt } from './uint'
import { BinaryParser } from '../serdes/binary-parser' import { BinaryParser } from '../serdes/binary-parser'
import * as bigInt from 'big-integer' import bigInt = require('big-integer')
import { isInstance } from 'big-integer' import { isInstance } from 'big-integer'
import { Buffer } from 'buffer/' import { Buffer } from 'buffer/'

View File

@@ -1,4 +1,4 @@
import * as bigInt from 'big-integer' import bigInt = require('big-integer')
import { Comparable } from './serialized-type' import { Comparable } from './serialized-type'
import { Buffer } from 'buffer/' import { Buffer } from 'buffer/'

View File

@@ -1,5 +1,5 @@
const { loadFixture } = require('./utils') const { loadFixture } = require('./utils')
const { coreTypes } = require('../dist/types') const { coreTypes } = require('../src/types')
const { Amount } = coreTypes const { Amount } = coreTypes
const fixtures = loadFixture('data-driven-tests.json') const fixtures = loadFixture('data-driven-tests.json')

View File

@@ -1,5 +1,5 @@
const fixtures = require('./fixtures/codec-fixtures.json') const fixtures = require('./fixtures/codec-fixtures.json')
const { decode, encode, decodeLedgerData } = require('../dist') const { decode, encode, decodeLedgerData } = require('../src')
function json(object) { function json(object) {
return JSON.stringify(object) return JSON.stringify(object)

View File

@@ -1,14 +1,14 @@
const { coreTypes } = require('../dist/types') const { coreTypes } = require('../src/types')
const Decimal = require('decimal.js') const Decimal = require('decimal.js')
const { encodeAccountID } = require('ripple-address-codec') const { encodeAccountID } = require('ripple-address-codec')
const { binary } = require('../dist/coretypes') const { binary } = require('../src/coretypes')
const { Amount, Hash160 } = coreTypes const { Amount, Hash160 } = coreTypes
const { makeParser, readJSON } = binary const { makeParser, readJSON } = binary
const { Field, TransactionType } = require('./../dist/enums') const { Field, TransactionType } = require('./../src/enums')
const { parseHexOnly, hexOnly, loadFixture } = require('./utils') const { parseHexOnly, hexOnly, loadFixture } = require('./utils')
const fixtures = loadFixture('data-driven-tests.json') const fixtures = loadFixture('data-driven-tests.json')
const { BytesList } = require('../dist/serdes/binary-serializer') const { BytesList } = require('../src/serdes/binary-serializer')
const { Buffer } = require('buffer/') const { Buffer } = require('buffer/')
const __ = hexOnly const __ = hexOnly

View File

@@ -1,7 +1,7 @@
const { binary } = require('../dist/coretypes') const { binary } = require('../src/coretypes')
const { encode, decode } = require('../dist') const { encode, decode } = require('../src')
const { makeParser, BytesList, BinarySerializer } = binary const { makeParser, BytesList, BinarySerializer } = binary
const { coreTypes } = require('../dist/types') const { coreTypes } = require('../src/types')
const { UInt8, UInt16, UInt32, UInt64, STObject } = coreTypes const { UInt8, UInt16, UInt32, UInt64, STObject } = coreTypes
const bigInt = require('big-integer') const bigInt = require('big-integer')
const { Buffer } = require('buffer/') const { Buffer } = require('buffer/')

View File

@@ -1,4 +1,4 @@
const { coreTypes } = require('../dist/types') const { coreTypes } = require('../src/types')
const { Hash128, Hash160, Hash256, AccountID, Currency } = coreTypes const { Hash128, Hash160, Hash256, AccountID, Currency } = coreTypes
const { Buffer } = require('buffer/') const { Buffer } = require('buffer/')

View File

@@ -3,7 +3,7 @@ const {
transactionTreeHash, transactionTreeHash,
ledgerHash, ledgerHash,
accountStateHash, accountStateHash,
} = require('../dist/ledger-hashes') } = require('../src/ledger-hashes')
describe('Ledger Hashes', function () { describe('Ledger Hashes', function () {
function testFactory(ledgerFixture) { function testFactory(ledgerFixture) {

View File

@@ -1,4 +1,4 @@
const { encode, decode } = require('../dist') const { encode, decode } = require('../src')
let str = let str =
'1100612200000000240000000125000068652D0000000055B6632D6376A2D9319F20A1C6DCCB486432D1E4A79951229D4C3DE2946F51D56662400009184E72A00081140DD319918CD5AE792BF7EC80D63B0F01B4573BBC' '1100612200000000240000000125000068652D0000000055B6632D6376A2D9319F20A1C6DCCB486432D1E4A79951229D4C3DE2946F51D56662400009184E72A00081140DD319918CD5AE792BF7EC80D63B0F01B4573BBC'

View File

@@ -1,4 +1,4 @@
const { encode, decode } = require('../dist') const { encode, decode } = require('../src')
let json = { let json = {
Account: 'rrrrrrrrrrrrrrrrrrrrrhoLvTp', Account: 'rrrrrrrrrrrrrrrrrrrrrhoLvTp',

View File

@@ -1,4 +1,4 @@
const { quality } = require('../dist/coretypes') const { quality } = require('../src/coretypes')
describe('Quality encode/decode', function () { describe('Quality encode/decode', function () {
const bookDirectory = const bookDirectory =

View File

@@ -1,6 +1,6 @@
const { ShaMap } = require('../dist/shamap.js') const { ShaMap } = require('../src/shamap')
const { binary, HashPrefix } = require('../dist/coretypes') const { binary, HashPrefix } = require('../src/coretypes')
const { coreTypes } = require('../dist/types') const { coreTypes } = require('../src/types')
const { loadFixture } = require('./utils') const { loadFixture } = require('./utils')
const { Buffer } = require('buffer/') const { Buffer } = require('buffer/')

View File

@@ -3,7 +3,7 @@ const {
encodeForSigning, encodeForSigning,
encodeForSigningClaim, encodeForSigningClaim,
encodeForMultisigning, encodeForMultisigning,
} = require('../dist') } = require('../src')
const tx_json = { const tx_json = {
Account: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ', Account: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ',

View File

@@ -1,4 +1,4 @@
const { encode, decode } = require('../dist') const { encode, decode } = require('../src')
// Notice: no Amount or Fee // Notice: no Amount or Fee
const tx_json = { const tx_json = {

View File

@@ -1,5 +1,5 @@
const { coreTypes } = require('../dist/types') const { coreTypes } = require('../src/types')
const { SerializedType } = require('../dist/types/serialized-type') const { SerializedType } = require('../src/types/serialized-type')
describe('SerializedType interfaces', () => { describe('SerializedType interfaces', () => {
Object.entries(coreTypes).forEach(([name, Value]) => { Object.entries(coreTypes).forEach(([name, Value]) => {

View File

@@ -1,7 +1,7 @@
const { coreTypes } = require('../dist/types') const { coreTypes } = require('../src/types')
const { UInt8, UInt64 } = coreTypes const { UInt8, UInt64 } = coreTypes
const { encode } = require('../dist') const { encode } = require('../src')
const binary = const binary =
'11007222000300003700000000000000003800000000000000006280000000000000000000000000000000000000005553440000000000000000000000000000000000000000000000000166D5438D7EA4C680000000000000000000000000005553440000000000AE123A8556F3CF91154711376AFB0F894F832B3D67D5438D7EA4C680000000000000000000000000005553440000000000F51DFC2A09D62CBBA1DFBDD4691DAC96AD98B90F' '11007222000300003700000000000000003800000000000000006280000000000000000000000000000000000000005553440000000000000000000000000000000000000000000000000166D5438D7EA4C680000000000000000000000000005553440000000000AE123A8556F3CF91154711376AFB0F894F832B3D67D5438D7EA4C680000000000000000000000000005553440000000000F51DFC2A09D62CBBA1DFBDD4691DAC96AD98B90F'

View File

@@ -1,4 +1,4 @@
const { encode, decode } = require('./../dist/index') const { encode, decode } = require('./../src/index')
const fixtures = require('./fixtures/x-codec-fixtures.json') const fixtures = require('./fixtures/x-codec-fixtures.json')
let json_x1 = { let json_x1 = {

View File

@@ -1,7 +1,7 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es6",
"lib": [ "lib": [
"es2017" "es2017"
], ],

View File

@@ -5,7 +5,7 @@ module.exports = {
parserOptions: { parserOptions: {
// Enable linting rules with type information from our tsconfig // Enable linting rules with type information from our tsconfig
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
project: ['./tsconfig.json'], project: ['./tsconfig.json', './tsconfig.eslint.json'],
sourceType: 'module', // Allow the use of imports / ES modules sourceType: 'module', // Allow the use of imports / ES modules
@@ -19,11 +19,11 @@ module.exports = {
browser: true, // Enable browser global variables browser: true, // Enable browser global variables
node: true, // Enable node global variables & Node.js scoping 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 es2020: true, // Add all ECMAScript 2020 globals and automatically set the ecmaVersion parser option to ES2020
mocha: true, // Add Mocha testing global variables jest: true, // Add Jest testing global variables
}, },
plugins: [], plugins: [],
extends: ['@xrplf/eslint-config/base', 'plugin:mocha/recommended'], extends: ['@xrplf/eslint-config/base'],
rules: { rules: {
// ** TODO ** // ** TODO **
@@ -56,5 +56,6 @@ module.exports = {
'eslint-comments/require-description': 'off', 'eslint-comments/require-description': 'off',
'no-shadow': 'off', 'no-shadow': 'off',
'multiline-comment-style': 'off', 'multiline-comment-style': 'off',
'@typescript-eslint/no-require-imports': 'off',
}, },
} }

View File

@@ -1,6 +1,8 @@
# ripple-keypairs Release History # ripple-keypairs Release History
## Unreleased ## Unreleased
### Changed
- All tests now use the Jest test runner and have been refactored for consistency across all packages
## 1.1.4 (2022-05-02) ## 1.1.4 (2022-05-02)
- `hexToBytes` now produces empty output for empty input, rather than `[0]`. - `hexToBytes` now produces empty output for empty input, rather than `[0]`.

View File

@@ -0,0 +1,8 @@
// Jest configuration for api
const base = require('../../jest.config.base.js')
module.exports = {
...base,
roots: [...base.roots, '<rootDir>/test'],
displayName: 'ripple-keypairs',
}

View File

@@ -4,7 +4,7 @@
"description": "Cryptographic key pairs for the XRP Ledger", "description": "Cryptographic key pairs for the XRP Ledger",
"scripts": { "scripts": {
"build": "tsc -b", "build": "tsc -b",
"test": "tsc -b && nyc mocha", "test": "jest --verbose false --silent=false ./test/*.test.ts",
"clean": "rm -rf ./dist && rm -rf tsconfig.tsbuildinfo", "clean": "rm -rf ./dist && rm -rf tsconfig.tsbuildinfo",
"lint": "eslint . --ext .ts", "lint": "eslint . --ext .ts",
"prepublish": "npm run lint && npm test" "prepublish": "npm run lint && npm test"

View File

@@ -1,7 +1,7 @@
/* eslint-disable no-bitwise -- /* eslint-disable no-bitwise --
* lots of bitwise operators necessary for this */ * lots of bitwise operators necessary for this */
import * as hashjs from 'hash.js' import * as hashjs from 'hash.js'
import * as BigNum from 'bn.js' import BigNum = require('bn.js')
export default class Sha512 { export default class Sha512 {
// TODO: type of `hash`? // TODO: type of `hash`?

View File

@@ -1,5 +1,5 @@
import * as assert from 'assert' import * as assert from 'assert'
import * as brorand from 'brorand' import brorand = require('brorand')
import * as hashjs from 'hash.js' import * as hashjs from 'hash.js'
import * as elliptic from 'elliptic' import * as elliptic from 'elliptic'
@@ -157,7 +157,7 @@ function deriveNodeAddress(publicKey): string {
const { decodeSeed } = addressCodec const { decodeSeed } = addressCodec
export = { export {
generateSeed, generateSeed,
deriveKeypair, deriveKeypair,
sign, sign,

View File

@@ -1,6 +1,6 @@
import * as assert from 'assert' import * as assert from 'assert'
import * as hashjs from 'hash.js' import * as hashjs from 'hash.js'
import * as BN from 'bn.js' import BN = require('bn.js')
function bytesToHex(a: Iterable<number> | ArrayLike<number>): string { function bytesToHex(a: Iterable<number> | ArrayLike<number>): string {
return Array.from(a, (byteValue) => { return Array.from(a, (byteValue) => {

View File

@@ -1,33 +1,36 @@
'use strict' // eslint-disable-line strict import assert from 'assert'
import fixtures from './fixtures/api.json'
import * as api from '../src'
const assert = require('assert')
const fixtures = require('./fixtures/api.json')
const api = require('../dist')
const decodeSeed = api.decodeSeed const decodeSeed = api.decodeSeed
const entropy = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] const entropy = new Uint8Array([
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
])
describe('api', () => { describe('api', () => {
it('generateSeed - secp256k1', () => { it('generateSeed - secp256k1', () => {
assert.strictEqual(api.generateSeed({entropy}), fixtures.secp256k1.seed) assert.strictEqual(api.generateSeed({ entropy }), fixtures.secp256k1.seed)
}) })
it('generateSeed - secp256k1, random', () => { it('generateSeed - secp256k1, random', () => {
const seed = api.generateSeed() const seed = api.generateSeed()
assert(seed.charAt(0) === 's') assert(seed.startsWith('s'))
const {type, bytes} = decodeSeed(seed) const { type, bytes } = decodeSeed(seed)
assert(type === 'secp256k1') assert(type === 'secp256k1')
assert(bytes.length === 16) assert(bytes.length === 16)
}) })
it('generateSeed - ed25519', () => { it('generateSeed - ed25519', () => {
assert.strictEqual(api.generateSeed({entropy, algorithm: 'ed25519'}), assert.strictEqual(
fixtures.ed25519.seed) api.generateSeed({ entropy, algorithm: 'ed25519' }),
fixtures.ed25519.seed,
)
}) })
it('generateSeed - ed25519, random', () => { it('generateSeed - ed25519, random', () => {
const seed = api.generateSeed({algorithm: 'ed25519'}) const seed = api.generateSeed({ algorithm: 'ed25519' })
assert(seed.slice(0, 3) === 'sEd') assert(seed.startsWith('sEd'))
const {type, bytes} = decodeSeed(seed) const { type, bytes } = decodeSeed(seed)
assert(type === 'ed25519') assert(type === 'ed25519')
assert(bytes.length === 16) assert(bytes.length === 16)
}) })
@@ -43,12 +46,16 @@ describe('api', () => {
}) })
it('deriveKeypair - secp256k1 - validator', () => { it('deriveKeypair - secp256k1 - validator', () => {
const keypair = api.deriveKeypair(fixtures.secp256k1.seed, {validator: true}) const keypair = api.deriveKeypair(fixtures.secp256k1.seed, {
validator: true,
})
assert.deepEqual(keypair, fixtures.secp256k1.validatorKeypair) assert.deepEqual(keypair, fixtures.secp256k1.validatorKeypair)
}) })
it('deriveKeypair - ed25519 - validator', () => { it('deriveKeypair - ed25519 - validator', () => {
const keypair = api.deriveKeypair(fixtures.ed25519.seed, {validator: true}) const keypair = api.deriveKeypair(fixtures.ed25519.seed, {
validator: true,
})
assert.deepEqual(keypair, fixtures.ed25519.validatorKeypair) assert.deepEqual(keypair, fixtures.ed25519.validatorKeypair)
}) })
@@ -65,7 +72,7 @@ describe('api', () => {
it('sign - secp256k1', () => { it('sign - secp256k1', () => {
const privateKey = fixtures.secp256k1.keypair.privateKey const privateKey = fixtures.secp256k1.keypair.privateKey
const message = fixtures.secp256k1.message const message = fixtures.secp256k1.message
const messageHex = (Buffer.from(message, 'utf8')).toString('hex') const messageHex = Buffer.from(message, 'utf8').toString('hex')
const signature = api.sign(messageHex, privateKey) const signature = api.sign(messageHex, privateKey)
assert.strictEqual(signature, fixtures.secp256k1.signature) assert.strictEqual(signature, fixtures.secp256k1.signature)
}) })
@@ -74,14 +81,14 @@ describe('api', () => {
const signature = fixtures.secp256k1.signature const signature = fixtures.secp256k1.signature
const publicKey = fixtures.secp256k1.keypair.publicKey const publicKey = fixtures.secp256k1.keypair.publicKey
const message = fixtures.secp256k1.message const message = fixtures.secp256k1.message
const messageHex = (Buffer.from(message, 'utf8')).toString('hex') const messageHex = Buffer.from(message, 'utf8').toString('hex')
assert(api.verify(messageHex, signature, publicKey)) assert(api.verify(messageHex, signature, publicKey))
}) })
it('sign - ed25519', () => { it('sign - ed25519', () => {
const privateKey = fixtures.ed25519.keypair.privateKey const privateKey = fixtures.ed25519.keypair.privateKey
const message = fixtures.ed25519.message const message = fixtures.ed25519.message
const messageHex = (Buffer.from(message, 'utf8')).toString('hex') const messageHex = Buffer.from(message, 'utf8').toString('hex')
const signature = api.sign(messageHex, privateKey) const signature = api.sign(messageHex, privateKey)
assert.strictEqual(signature, fixtures.ed25519.signature) assert.strictEqual(signature, fixtures.ed25519.signature)
}) })
@@ -90,20 +97,20 @@ describe('api', () => {
const signature = fixtures.ed25519.signature const signature = fixtures.ed25519.signature
const publicKey = fixtures.ed25519.keypair.publicKey const publicKey = fixtures.ed25519.keypair.publicKey
const message = fixtures.ed25519.message const message = fixtures.ed25519.message
const messageHex = (Buffer.from(message, 'utf8')).toString('hex') const messageHex = Buffer.from(message, 'utf8').toString('hex')
assert(api.verify(messageHex, signature, publicKey)) assert(api.verify(messageHex, signature, publicKey))
}) })
it('deriveNodeAddress', () => { it('deriveNodeAddress', () => {
const x = 'n9KHn8NfbBsZV5q8bLfS72XyGqwFt5mgoPbcTV4c6qKiuPTAtXYk' const addrX = 'n9KHn8NfbBsZV5q8bLfS72XyGqwFt5mgoPbcTV4c6qKiuPTAtXYk'
const y = 'rU7bM9ENDkybaxNrefAVjdLTyNLuue1KaJ' const addrY = 'rU7bM9ENDkybaxNrefAVjdLTyNLuue1KaJ'
assert.strictEqual(api.deriveNodeAddress(x), y) assert.strictEqual(api.deriveNodeAddress(addrX), addrY)
}) })
it('Random Address', () => { it('Random Address', () => {
const seed = api.generateSeed() const seed = api.generateSeed()
const keypair = api.deriveKeypair(seed) const keypair = api.deriveKeypair(seed)
const address = api.deriveAddress(keypair.publicKey) const address = api.deriveAddress(keypair.publicKey)
assert(address[0] === 'r') assert(address.startsWith('r'))
}) })
}) })

View File

@@ -1,9 +1,5 @@
/* eslint-disable no-unused-expressions/no-unused-expressions */ import assert from 'assert'
import * as api from 'ripple-address-codec'
'use strict'
const assert = require('assert')
const api = require('ripple-address-codec')
function toHex(bytes) { function toHex(bytes) {
return Buffer.from(bytes).toString('hex').toUpperCase() return Buffer.from(bytes).toString('hex').toUpperCase()
@@ -13,27 +9,31 @@ function toBytes(hex) {
return Buffer.from(hex, 'hex').toJSON().data return Buffer.from(hex, 'hex').toJSON().data
} }
describe('ripple-address-codec', function() { describe('ripple-address-codec', function () {
function makeTest(type, base58, hex) { function makeTest(type, base58, hex) {
it('can translate between ' + hex + ' and ' + base58 + ' (encode ' + type + ')', function() { it(`can translate between ${hex} and ${base58} (encode ${type})`, () => {
const actual = api['encode' + type](toBytes(hex)) const actual = api[`encode${type}`](toBytes(hex))
assert.equal(actual, base58) assert.equal(actual, base58)
}) })
it('can translate between ' + base58 + ' and ' + hex + ' (decode ' + type + ')', function() { it(`can translate between ${base58} and ${hex} (decode ${type})`, () => {
const buf = api['decode' + type](base58) const buf = api[`decode${type}`](base58)
assert.equal(toHex(buf), hex) assert.equal(toHex(buf), hex)
}) })
} }
makeTest('AccountID', 'rJrRMgiRgrU6hDF4pgu5DXQdWyPbY35ErN', makeTest(
'BA8E78626EE42C41B46D46C3048DF3A1C3C87072') 'AccountID',
'rJrRMgiRgrU6hDF4pgu5DXQdWyPbY35ErN',
'BA8E78626EE42C41B46D46C3048DF3A1C3C87072',
)
makeTest( makeTest(
'NodePublic', 'NodePublic',
'n9MXXueo837zYH36DvMc13BwHcqtfAWNJY5czWVbp7uYTj7x17TH', 'n9MXXueo837zYH36DvMc13BwHcqtfAWNJY5czWVbp7uYTj7x17TH',
'0388E5BA87A000CB807240DF8C848EB0B5FFA5C8E5A521BC8E105C0F0A44217828') '0388E5BA87A000CB807240DF8C848EB0B5FFA5C8E5A521BC8E105C0F0A44217828',
)
it('can decode arbitrary seeds', function() { it('can decode arbitrary seeds', () => {
const decoded = api.decodeSeed('sEdTM1uX8pu2do5XvTnutH6HsouMaM2') const decoded = api.decodeSeed('sEdTM1uX8pu2do5XvTnutH6HsouMaM2')
assert.equal(toHex(decoded.bytes), '4C3A1D213FBDFB14C7C28D609469B341') assert.equal(toHex(decoded.bytes), '4C3A1D213FBDFB14C7C28D609469B341')
assert.equal(decoded.type, 'ed25519') assert.equal(decoded.type, 'ed25519')
@@ -43,11 +43,16 @@ describe('ripple-address-codec', function() {
assert.equal(decoded2.type, 'secp256k1') assert.equal(decoded2.type, 'secp256k1')
}) })
it('can pass a type as second arg to encodeSeed', function() { it('can pass a type as second arg to encodeSeed', () => {
const edSeed = 'sEdTM1uX8pu2do5XvTnutH6HsouMaM2' const edSeed = 'sEdTM1uX8pu2do5XvTnutH6HsouMaM2'
const decoded = api.decodeSeed(edSeed) const decoded = api.decodeSeed(edSeed)
assert.equal(toHex(decoded.bytes), '4C3A1D213FBDFB14C7C28D609469B341') assert.equal(toHex(decoded.bytes), '4C3A1D213FBDFB14C7C28D609469B341')
assert.equal(decoded.type, 'ed25519') assert.equal(decoded.type, 'ed25519')
if (decoded.type === null) {
assert.fail('decoded.type should not be null')
}
assert.equal(api.encodeSeed(decoded.bytes, decoded.type), edSeed) assert.equal(api.encodeSeed(decoded.bytes, decoded.type), edSeed)
}) })
}) })
export {}

View File

@@ -1,9 +1,7 @@
'use strict' // eslint-disable-line strict import assert from 'assert'
import * as utils from '../src/utils'
const assert = require('assert') describe('utils', function () {
const utils = require('../dist/utils')
describe('utils', () => {
it('hexToBytes - empty', () => { it('hexToBytes - empty', () => {
assert.deepEqual(utils.hexToBytes(''), []) assert.deepEqual(utils.hexToBytes(''), [])
}) })
@@ -18,9 +16,14 @@ describe('utils', () => {
it('bytesToHex - DEADBEEF', () => { it('bytesToHex - DEADBEEF', () => {
assert.deepEqual(utils.bytesToHex([222, 173, 190, 239]), 'DEADBEEF') assert.deepEqual(utils.bytesToHex([222, 173, 190, 239]), 'DEADBEEF')
}); })
it('bytesToHex - DEADBEEF (Uint8Array)', () => { it('bytesToHex - DEADBEEF (Uint8Array)', () => {
assert.deepEqual(utils.bytesToHex(new Uint8Array([222, 173, 190, 239])), 'DEADBEEF') assert.deepEqual(
}); utils.bytesToHex(new Uint8Array([222, 173, 190, 239])),
'DEADBEEF',
)
})
}) })
export {}

View File

@@ -1,81 +0,0 @@
/* eslint-disable no-unused-expressions/no-unused-expressions */
'use strict'
const assert = require('assert')
const api = require('ripple-address-codec')
function toHex(bytes) {
return Buffer.from(bytes).toString('hex').toUpperCase()
}
function toBytes(hex) {
return Buffer.from(hex, 'hex').toJSON().data
}
describe('ripple-address-codec', function() {
describe('encodeSeed', function() {
it('encodes a secp256k1 seed', function() {
const result = api.encodeSeed(toBytes('CF2DE378FBDD7E2EE87D486DFB5A7BFF'), 'secp256k1')
assert.equal(result, 'sn259rEFXrQrWyx3Q7XneWcwV6dfL')
})
it('encodes low secp256k1 seed', function() {
const result = api.encodeSeed(toBytes('00000000000000000000000000000000'), 'secp256k1')
assert.equal(result, 'sp6JS7f14BuwFY8Mw6bTtLKWauoUs')
})
it('encodes high secp256k1 seed', function() {
const result = api.encodeSeed(toBytes('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'), 'secp256k1')
assert.equal(result, 'saGwBRReqUNKuWNLpUAq8i8NkXEPN')
})
it('encodes an ed25519 seed', function() {
const result = api.encodeSeed(toBytes('4C3A1D213FBDFB14C7C28D609469B341'), 'ed25519')
assert.equal(result, 'sEdTM1uX8pu2do5XvTnutH6HsouMaM2')
})
it('encodes low ed25519 seed', function() {
const result = api.encodeSeed(toBytes('00000000000000000000000000000000'), 'ed25519')
assert.equal(result, 'sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE')
})
it('encodes high ed25519 seed', function() {
const result = api.encodeSeed(toBytes('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'), 'ed25519')
assert.equal(result, 'sEdV19BLfeQeKdEXyYA4NhjPJe6XBfG')
})
})
describe('decodeSeed', function() {
it('can decode an Ed25519 seed', function() {
const decoded = api.decodeSeed('sEdTM1uX8pu2do5XvTnutH6HsouMaM2')
assert.equal(toHex(decoded.bytes), '4C3A1D213FBDFB14C7C28D609469B341')
assert.equal(decoded.type, 'ed25519')
})
it('can decode a secp256k1 seed', function() {
const decoded = api.decodeSeed('sn259rEFXrQrWyx3Q7XneWcwV6dfL')
assert.equal(toHex(decoded.bytes), 'CF2DE378FBDD7E2EE87D486DFB5A7BFF')
assert.equal(decoded.type, 'secp256k1')
})
})
describe('encodeAccountID', function() {
it('can encode an AccountID', function() {
const encoded = api.encodeAccountID(toBytes('BA8E78626EE42C41B46D46C3048DF3A1C3C87072'))
assert.equal(encoded, 'rJrRMgiRgrU6hDF4pgu5DXQdWyPbY35ErN')
})
})
describe('decodeNodePublic', function() {
it('can decode a NodePublic', function() {
const decoded = api.decodeNodePublic('n9MXXueo837zYH36DvMc13BwHcqtfAWNJY5czWVbp7uYTj7x17TH')
assert.equal(toHex(decoded), '0388E5BA87A000CB807240DF8C848EB0B5FFA5C8E5A521BC8E105C0F0A44217828')
})
})
})

View File

@@ -0,0 +1,99 @@
import assert from 'assert'
import * as api from 'ripple-address-codec'
function toHex(bytes: Buffer) {
return Buffer.from(bytes).toString('hex').toUpperCase()
}
function toBytes(hex: string) {
return Buffer.from(hex, 'hex').toJSON().data
}
describe('ripple-address-codec', function () {
describe('encodeSeed', function () {
it('encodes a secp256k1 seed', () => {
const result = api.encodeSeed(
Buffer.from(toBytes('CF2DE378FBDD7E2EE87D486DFB5A7BFF')),
'secp256k1',
)
assert.equal(result, 'sn259rEFXrQrWyx3Q7XneWcwV6dfL')
})
it('encodes low secp256k1 seed', () => {
const result = api.encodeSeed(
Buffer.from(toBytes('00000000000000000000000000000000')),
'secp256k1',
)
assert.equal(result, 'sp6JS7f14BuwFY8Mw6bTtLKWauoUs')
})
it('encodes high secp256k1 seed', () => {
const result = api.encodeSeed(
Buffer.from(toBytes('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF')),
'secp256k1',
)
assert.equal(result, 'saGwBRReqUNKuWNLpUAq8i8NkXEPN')
})
it('encodes an ed25519 seed', () => {
const result = api.encodeSeed(
Buffer.from(toBytes('4C3A1D213FBDFB14C7C28D609469B341')),
'ed25519',
)
assert.equal(result, 'sEdTM1uX8pu2do5XvTnutH6HsouMaM2')
})
it('encodes low ed25519 seed', () => {
const result = api.encodeSeed(
Buffer.from(toBytes('00000000000000000000000000000000')),
'ed25519',
)
assert.equal(result, 'sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE')
})
it('encodes high ed25519 seed', () => {
const result = api.encodeSeed(
Buffer.from(toBytes('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF')),
'ed25519',
)
assert.equal(result, 'sEdV19BLfeQeKdEXyYA4NhjPJe6XBfG')
})
})
describe('decodeSeed', function () {
it('can decode an Ed25519 seed', () => {
const decoded = api.decodeSeed('sEdTM1uX8pu2do5XvTnutH6HsouMaM2')
assert.equal(toHex(decoded.bytes), '4C3A1D213FBDFB14C7C28D609469B341')
assert.equal(decoded.type, 'ed25519')
})
it('can decode a secp256k1 seed', () => {
const decoded = api.decodeSeed('sn259rEFXrQrWyx3Q7XneWcwV6dfL')
assert.equal(toHex(decoded.bytes), 'CF2DE378FBDD7E2EE87D486DFB5A7BFF')
assert.equal(decoded.type, 'secp256k1')
})
})
describe('encodeAccountID', function () {
it('can encode an AccountID', () => {
const encoded = api.encodeAccountID(
Buffer.from(toBytes('BA8E78626EE42C41B46D46C3048DF3A1C3C87072')),
)
assert.equal(encoded, 'rJrRMgiRgrU6hDF4pgu5DXQdWyPbY35ErN')
})
})
describe('decodeNodePublic', function () {
it('can decode a NodePublic', () => {
const decoded = api.decodeNodePublic(
'n9MXXueo837zYH36DvMc13BwHcqtfAWNJY5czWVbp7uYTj7x17TH',
)
assert.equal(
toHex(decoded),
'0388E5BA87A000CB807240DF8C848EB0B5FFA5C8E5A521BC8E105C0F0A44217828',
)
})
})
})
export {}

View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"include": ["src/**/*.ts", "test/**/*.ts"]
}

View File

@@ -1,7 +1,7 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"target": "ES2017", "target": "es6",
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"outDir": "./dist", "outDir": "./dist",
@@ -11,7 +11,9 @@
"noUnusedParameters": true, "noUnusedParameters": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true "forceConsistentCasingInFileNames": true,
"strictNullChecks": true,
"resolveJsonModule": true
}, },
"references": [{ "references": [{
"path": "../ripple-address-codec/tsconfig.json" "path": "../ripple-address-codec/tsconfig.json"

View File

@@ -6,7 +6,12 @@ module.exports = {
parserOptions: { parserOptions: {
// Enable linting rules with type information from our tsconfig // Enable linting rules with type information from our tsconfig
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
project: ['./tsconfig.eslint.json'], project: [
'./tsconfig.eslint.json',
'../ripple-binary-codec/tsconfig.eslint.json',
'../ripple-address-codec/tsconfig.eslint.json',
'../ripple-keypairs/tsconfig.eslint.json',
],
// Allow the use of imports / ES modules // Allow the use of imports / ES modules
sourceType: 'module', sourceType: 'module',
@@ -23,11 +28,13 @@ module.exports = {
node: true, node: true,
// Add all ECMAScript 2020 globals and automatically set the ecmaVersion parser option to ES2020 // Add all ECMAScript 2020 globals and automatically set the ecmaVersion parser option to ES2020
es2020: true, es2020: true,
jest: true,
}, },
plugins: [], plugins: [],
extends: ['@xrplf/eslint-config/base', 'plugin:mocha/recommended'], extends: ['@xrplf/eslint-config/base'],
rules: { rules: {
'multiline-comment-style': 'off',
// Disabled until https://github.com/import-js/eslint-plugin-import/pull/2305 is resolved to // Disabled until https://github.com/import-js/eslint-plugin-import/pull/2305 is resolved to
// accomodate this change https://github.com/XRPLF/xrpl.js/pull/2133 // accomodate this change https://github.com/XRPLF/xrpl.js/pull/2133
'import/no-unused-modules': 'off', 'import/no-unused-modules': 'off',
@@ -51,7 +58,6 @@ module.exports = {
// no-shadow has false-positives for enum, @typescript-eslint version fixes that // no-shadow has false-positives for enum, @typescript-eslint version fixes that
'no-shadow': 'off', 'no-shadow': 'off',
'@typescript-eslint/no-shadow': ['error'], '@typescript-eslint/no-shadow': ['error'],
'multiline-comment-style': ['error', 'starred-block'],
'jsdoc/check-examples': 'off', 'jsdoc/check-examples': 'off',
'tsdoc/syntax': 'off', 'tsdoc/syntax': 'off',
@@ -74,13 +80,16 @@ module.exports = {
'max-statements': 'off', 'max-statements': 'off',
// Snippets have logs on console to better understand the working. // Snippets have logs on console to better understand the working.
'no-console': 'off', 'no-console': 'off',
'import/no-extraneous-dependencies': 'off',
}, },
}, },
{ {
files: ['test/**/*.ts'], files: ['test/**/*.ts'],
rules: { rules: {
// Because this project is managed by lerna, dev dependencies are /*
// hoisted and do not appear in the package.json. * Because this project is managed by lerna, dev dependencies are
* hoisted and do not appear in the package.json.
*/
'import/no-extraneous-dependencies': 'off', 'import/no-extraneous-dependencies': 'off',
'node/no-extraneous-import': 'off', 'node/no-extraneous-import': 'off',
@@ -102,19 +111,12 @@ module.exports = {
// Tests are already in 2 callbacks, so max 3 is pretty restrictive // Tests are already in 2 callbacks, so max 3 is pretty restrictive
'max-nested-callbacks': 'off', 'max-nested-callbacks': 'off',
// setup/teardown client is easier to do in before/after, even if there is only one testcase
'mocha/no-hooks-for-single-case': 'off',
// messes with fixtures // messes with fixtures
'consistent-default-export-name/default-import-match-filename': 'off', 'consistent-default-export-name/default-import-match-filename': 'off',
}, },
}, },
{ {
files: ['test/client/*.ts'], files: ['test/client/*.ts'],
rules: {
// Rule does not work with dynamically generated tests.
'mocha/no-setup-in-describe': 'off',
},
}, },
{ {
files: ['test/models/*.ts'], files: ['test/models/*.ts'],

View File

@@ -4,11 +4,16 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
## Unreleased ## Unreleased
### Fixed ### Fixed
* Code splitting improvements for lodash
* Fixed missing reason code in websocket implemntation on websocket disconnect
* Fix timeout error in request manager
* Improved typescript typing
### Added ### Added
### Changed ### Changed
* All tests now use the Jest test runner and have been refactored for consistency across all packages
### Deprecated ### Deprecated
Wallet.fromMmnemonic() Wallet.fromMmnemonic()

View File

@@ -0,0 +1,8 @@
// Jest configuration for api
const base = require('../../jest.config.base.js')
module.exports = {
...base,
roots: [...base.roots, '<rootDir>/test'],
displayName: 'xrpl.js',
}

View File

@@ -0,0 +1,14 @@
// the jest.fn() API
import * as jest from 'jest-mock'
// The matchers API
import expect from 'expect'
// Add missing Jest functions
window.test = window.it
window.test.each = (inputs) => (testName, test) =>
inputs.forEach((args) => window.it(testName, () => test(...args)))
window.test.todo = function () {
return undefined
}
window.jest = jest
window.expect = expect

View File

@@ -0,0 +1,34 @@
const webpackConfig = require('./test/webpack.config')[0]()
delete webpackConfig.entry
module.exports = function (config) {
config.set({
plugins: ['karma-webpack', 'karma-jasmine', 'karma-chrome-launcher'],
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
webpack: webpackConfig,
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['jasmine'],
// list of files / patterns to load in the browser
files: [
'build/xrpl-latest.js',
'test/integration/index.ts',
'karma-setup.js',
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'karma-setup.js': ['webpack'],
// Use webpack to bundle our test files
'test/integration/index.ts': ['webpack'],
},
browsers: ['ChromeHeadless'],
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -33,9 +33,19 @@
"ws": "^8.2.2" "ws": "^8.2.2"
}, },
"devDependencies": { "devDependencies": {
"@geut/browser-node-core": "^2.0.13",
"@types/node": "^14.18.36",
"assert-browserify": "^2.0.0",
"browserify-fs": "^1.0.0",
"constants-browserify": "^1.0.0",
"https-browserify": "^1.0.0",
"karma": "^6.4.1",
"karma-chrome-launcher": "^3.1.1",
"karma-jasmine": "^5.1.0",
"karma-webpack": "^5.0.0",
"node-polyfill-webpack-plugin": "^2.0.1",
"react": "^18.2.0", "react": "^18.2.0",
"typedoc": "^0.23.24", "typedoc": "^0.23.24"
"xrpl-local": "file:./src"
}, },
"resolutions": { "resolutions": {
"elliptic": "^6.5.4" "elliptic": "^6.5.4"
@@ -48,13 +58,13 @@
"build:browserTests": "webpack --config ./test/webpack.config.js", "build:browserTests": "webpack --config ./test/webpack.config.js",
"analyze": "run-s build:web --analyze", "analyze": "run-s build:web --analyze",
"watch": "run-s build:lib --watch", "watch": "run-s build:lib --watch",
"clean": "rm -rf dist", "clean": "rm -rf dist build coverage",
"docgen": "tsc --build tsconfig.docs.json && typedoc && echo js.xrpl.org >> ../../docs/CNAME", "docgen": "tsc --build tsconfig.docs.json && typedoc && echo js.xrpl.org >> ../../docs/CNAME",
"prepublish": "run-s clean build", "prepublish": "run-s clean build",
"test": "nyc mocha --config=test/.mocharc.json --exit", "test": "jest --verbose false --silent=false ./test/**/*.test.ts --testPathIgnorePatterns=./test/integration --testPathIgnorePatterns=./test/fixtures",
"test:integration": "TS_NODE_PROJECT=tsconfig.build.json nyc mocha ./test/integration/**/*.test.ts ./test/integration/*.test.ts", "test:integration": "TS_NODE_PROJECT=tsconfig.build.json jest --verbose false --silent=false --runInBand ./test/integration/**/*.test.ts",
"test:browser": "npm run build:browserTests && TS_NODE_PROJECT=tsconfig.build.json nyc mocha ./test/browser/*.ts", "test:browser": "npm run build && npm run build:browserTests && karma start ./karma.config.js --single-run",
"test:watch": "TS_NODE_PROJECT=src/tsconfig.json mocha --config=test/.mocharc.json --watch --reporter dot", "test:watch": "jest --watch --verbose false --silent=false --runInBand ./test/**/*.test.ts --testPathIgnorePatterns=./test/integration --testPathIgnorePatterns=./test/fixtures",
"format": "prettier --write '{src,test}/**/*.ts'", "format": "prettier --write '{src,test}/**/*.ts'",
"lint": "eslint . --ext .ts --max-warnings 0", "lint": "eslint . --ext .ts --max-warnings 0",
"perf": "./scripts/perf_test.sh", "perf": "./scripts/perf_test.sh",

View File

@@ -4,7 +4,7 @@ import {
PaymentChannelCreate, PaymentChannelCreate,
PaymentChannelClaim, PaymentChannelClaim,
hashes, hashes,
} from '../../dist/npm' } from '../../src'
const client = new Client('wss://s.altnet.rippletest.net:51233') const client = new Client('wss://s.altnet.rippletest.net:51233')

View File

@@ -1,4 +1,4 @@
import { Client, LedgerResponse, TxResponse } from '../../dist/npm' import { Client, LedgerResponse, TxResponse } from '../../src'
const client = new Client('wss://s.altnet.rippletest.net:51233') const client = new Client('wss://s.altnet.rippletest.net:51233')

View File

@@ -4,7 +4,7 @@ import {
AccountSet, AccountSet,
convertStringToHex, convertStringToHex,
SignerListSet, SignerListSet,
} from '../../dist/npm' } from '../../src'
const client = new Client('wss://s.altnet.rippletest.net:51233') const client = new Client('wss://s.altnet.rippletest.net:51233')

View File

@@ -1,4 +1,4 @@
import { Client, Payment, PaymentFlags, TrustSet } from '../../dist/npm' import { Client, Payment, PaymentFlags, TrustSet } from '../../src'
const client = new Client('wss://s.altnet.rippletest.net:51233') const client = new Client('wss://s.altnet.rippletest.net:51233')

View File

@@ -1,4 +1,4 @@
import { Client, Payment, RipplePathFindResponse } from '../../dist/npm' import { Client, Payment, RipplePathFindResponse } from '../../src'
const client = new Client('wss://s.altnet.rippletest.net:51233') const client = new Client('wss://s.altnet.rippletest.net:51233')

View File

@@ -1,4 +1,4 @@
import { Client, Payment } from '../../dist/npm' import { Client, Payment } from '../../src'
/** /**
* When implementing Reliable Transaction Submission, there are many potential solutions, each with different trade-offs. * When implementing Reliable Transaction Submission, there are many potential solutions, each with different trade-offs.

View File

@@ -4,7 +4,7 @@ import {
EscrowCreate, EscrowCreate,
EscrowFinish, EscrowFinish,
isoTimeToRippleTime, isoTimeToRippleTime,
} from '../../dist/npm' } from '../../src'
const client = new Client('wss://s.altnet.rippletest.net:51233') const client = new Client('wss://s.altnet.rippletest.net:51233')

View File

@@ -1,4 +1,4 @@
import { Client, Payment, SetRegularKey } from '../../dist/npm' import { Client, Payment, SetRegularKey } from '../../src'
const client = new Client('wss://s.altnet.rippletest.net:51233') const client = new Client('wss://s.altnet.rippletest.net:51233')

View File

@@ -1,7 +1,7 @@
{ {
"extends": "../tsconfig.json", "extends": "../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"rootDir": "./src" "rootDir": "../../xrpl"
}, },
"include": ["./src/**/*.ts"] "include": ["./src/**/*.ts", "../src/**/*.ts", "../src/**/*.json"]
} }

View File

@@ -1,4 +1,4 @@
import type { Client } from '..' import type { Client } from '../client'
import { XRPLFaucetError } from '../errors' import { XRPLFaucetError } from '../errors'
export interface FaucetWallet { export interface FaucetWallet {

View File

@@ -3,7 +3,7 @@ import { request as httpsRequest, RequestOptions } from 'https'
import { isValidClassicAddress } from 'ripple-address-codec' import { isValidClassicAddress } from 'ripple-address-codec'
import type { Client } from '..' import type { Client } from '../client'
import { RippledError, XRPLFaucetError } from '../errors' import { RippledError, XRPLFaucetError } from '../errors'
import { import {

View File

@@ -2,7 +2,7 @@
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import { fromSeed } from 'bip32' import { fromSeed } from 'bip32'
import { mnemonicToSeedSync, validateMnemonic } from 'bip39' import { mnemonicToSeedSync, validateMnemonic } from 'bip39'
import _ from 'lodash' import isEqual from 'lodash/isEqual'
import { import {
classicAddressToXAddress, classicAddressToXAddress,
isValidXAddress, isValidXAddress,
@@ -498,7 +498,7 @@ class Wallet {
}) })
/* eslint-enable @typescript-eslint/consistent-type-assertions -- Done with dynamic checking */ /* eslint-enable @typescript-eslint/consistent-type-assertions -- Done with dynamic checking */
if (!_.isEqual(decoded, txCopy)) { if (!isEqual(decoded, txCopy)) {
const data = { const data = {
decoded, decoded,
tx, tx,

View File

@@ -20,7 +20,7 @@ export default class RequestManager {
{ {
resolve: (value: Response | PromiseLike<Response>) => void resolve: (value: Response | PromiseLike<Response>) => void
reject: (value: Error) => void reject: (value: Error) => void
timer: NodeJS.Timeout timer: ReturnType<typeof setTimeout>
} }
>() >()
@@ -34,7 +34,10 @@ export default class RequestManager {
public resolve(id: string | number, response: Response): void { public resolve(id: string | number, response: Response): void {
const promise = this.promisesAwaitingResponse.get(id) const promise = this.promisesAwaitingResponse.get(id)
if (promise == null) { if (promise == null) {
throw new XrplError(`No existing promise with id ${id}`) throw new XrplError(`No existing promise with id ${id}`, {
type: 'resolve',
response,
})
} }
clearTimeout(promise.timer) clearTimeout(promise.timer)
promise.resolve(response) promise.resolve(response)
@@ -51,7 +54,10 @@ export default class RequestManager {
public reject(id: string | number, error: Error): void { public reject(id: string | number, error: Error): void {
const promise = this.promisesAwaitingResponse.get(id) const promise = this.promisesAwaitingResponse.get(id)
if (promise == null) { if (promise == null) {
throw new XrplError(`No existing promise with id ${id}`) throw new XrplError(`No existing promise with id ${id}`, {
type: 'reject',
error,
})
} }
clearTimeout(promise.timer) clearTimeout(promise.timer)
// TODO: figure out how to have a better stack trace for an error // TODO: figure out how to have a better stack trace for an error
@@ -93,20 +99,35 @@ export default class RequestManager {
newId = request.id newId = request.id
} }
const newRequest = JSON.stringify({ ...request, id: newId }) const newRequest = JSON.stringify({ ...request, id: newId })
const timer = setTimeout( // Typing required for Jest running in browser
() => this.reject(newId, new TimeoutError()), const timer: ReturnType<typeof setTimeout> = setTimeout(() => {
timeout, this.reject(
) newId,
new TimeoutError(
`Timeout for request: ${JSON.stringify(request)} with id ${newId}`,
request,
),
)
}, 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).
*/ */
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Reason above. // The following type assertions are required to get this code to pass in browser environments
if (timer.unref) { // where setTimeout has a different type
timer.unref() // eslint-disable-next-line max-len -- Necessary to disable both rules.
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access -- Reason above.
if ((timer as unknown as any).unref) {
// eslint-disable-next-line max-len -- Necessary to disable both rules.
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call -- Reason above.
;(timer as unknown as any).unref()
} }
if (this.promisesAwaitingResponse.has(newId)) { if (this.promisesAwaitingResponse.has(newId)) {
throw new XrplError(`Response with id '${newId}' is already pending`) clearTimeout(timer)
throw new XrplError(
`Response with id '${newId}' is already pending`,
request,
)
} }
const newPromise = new Promise<Response>( const newPromise = new Promise<Response>(
(resolve: (value: Response | PromiseLike<Response>) => void, reject) => { (resolve: (value: Response | PromiseLike<Response>) => void, reject) => {

View File

@@ -3,13 +3,13 @@ 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 {
public onclose?: () => void public onclose?: (closeEvent: CloseEvent) => void
public onopen?: () => void public onopen?: (openEvent: Event) => void
public onerror?: (error: Error) => void public onerror?: (error: Error) => void
public onmessage?: (message: MessageEvent) => void public onmessage?: (message: MessageEvent) => void
public readyState: number public readyState: number
public constructor(url: string) public constructor(url: string)
public close(code?: number): void public close(code?: number, reason?: Buffer): void
public send(message: string): void public send(message: string): void
} }
@@ -52,8 +52,13 @@ export default class WSWrapper extends EventEmitter {
this.ws = new WebSocket(url) this.ws = new WebSocket(url)
this.ws.onclose = (): void => { this.ws.onclose = (closeEvent: CloseEvent): void => {
this.emit('close') let reason: Uint8Array | undefined
if (closeEvent.reason) {
const enc = new TextEncoder()
reason = enc.encode(closeEvent.reason)
}
this.emit('close', closeEvent.code, reason)
} }
this.ws.onopen = (): void => { this.ws.onopen = (): void => {
@@ -71,10 +76,13 @@ export default class WSWrapper extends EventEmitter {
/** /**
* Closes the websocket. * Closes the websocket.
*
* @param code - Close code.
* @param reason - Close reason.
*/ */
public close(): void { public close(code?: number, reason?: Buffer): void {
if (this.readyState === 1) { if (this.readyState === 1) {
this.ws.close() this.ws.close(code, reason)
} }
} }

View File

@@ -1,8 +1,9 @@
/* eslint-disable max-lines -- Connection is a large file w/ lots of imports/exports */ /* eslint-disable max-lines -- Connection is a large file w/ lots of imports/exports */
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import { Agent } from 'http' import { Agent } from 'http'
import _ from 'lodash' import omitBy from 'lodash/omitBy'
import WebSocket from 'ws' import WebSocket from 'ws'
import { import {
@@ -63,7 +64,7 @@ function getAgent(url: string, config: ConnectionOptions): Agent | undefined {
const parsedURL = new URL(url) const parsedURL = new URL(url)
const parsedProxyURL = new URL(config.proxy) const parsedProxyURL = new URL(config.proxy)
const proxyOptions = _.omitBy( const proxyOptions = omitBy(
{ {
secureEndpoint: parsedURL.protocol === 'wss:', secureEndpoint: parsedURL.protocol === 'wss:',
secureProxy: parsedProxyURL.protocol === 'https:', secureProxy: parsedProxyURL.protocol === 'https:',
@@ -125,7 +126,7 @@ function createWebSocket(
Authorization: `Basic ${base64}`, Authorization: `Basic ${base64}`,
} }
} }
const optionsOverrides = _.omitBy( const optionsOverrides = omitBy(
{ {
ca: config.trustedCertificates, ca: config.trustedCertificates,
key: config.key, key: config.key,
@@ -175,8 +176,10 @@ async function websocketSendAsync(
export class Connection extends EventEmitter { export class Connection extends EventEmitter {
private readonly url: string | undefined private readonly url: string | undefined
private ws: WebSocket | null = null private ws: WebSocket | null = null
private reconnectTimeoutID: null | NodeJS.Timeout = null // Typing necessary for Jest tests running in browser
private heartbeatIntervalID: null | NodeJS.Timeout = null private reconnectTimeoutID: null | ReturnType<typeof setTimeout> = null
// Typing necessary for Jest tetsts running in browser
private heartbeatIntervalID: null | ReturnType<typeof setTimeout> = null
private readonly retryConnectionBackoff = new ExponentialBackoff({ private readonly retryConnectionBackoff = new ExponentialBackoff({
min: 100, min: 100,
max: SECONDS_PER_MINUTE * 1000, max: SECONDS_PER_MINUTE * 1000,
@@ -224,6 +227,7 @@ export class Connection extends EventEmitter {
* @returns When the websocket is connected. * @returns When the websocket is connected.
* @throws ConnectionError if there is a connection error, RippleError if there is already a WebSocket in existence. * @throws ConnectionError if there is a connection error, RippleError if there is already a WebSocket in existence.
*/ */
// eslint-disable-next-line max-lines-per-function -- Necessary
public async connect(): Promise<void> { public async connect(): Promise<void> {
if (this.isConnected()) { if (this.isConnected()) {
return Promise.resolve() return Promise.resolve()
@@ -245,14 +249,17 @@ export class Connection extends EventEmitter {
} }
// Create the connection timeout, in case the connection hangs longer than expected. // Create the connection timeout, in case the connection hangs longer than expected.
const connectionTimeoutID = setTimeout(() => { const connectionTimeoutID: ReturnType<typeof setTimeout> = setTimeout(
this.onConnectionFailed( () => {
new ConnectionError( this.onConnectionFailed(
`Error: connect() timed out after ${this.config.connectionTimeout} ms. If your internet connection is working, the ` + new ConnectionError(
`rippled server may be blocked or inaccessible. You can also try setting the 'connectionTimeout' option in the Client constructor.`, `Error: connect() timed out after ${this.config.connectionTimeout} ms. 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.`,
) ),
}, 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)
@@ -337,7 +344,7 @@ export class Connection extends EventEmitter {
timeout?: number, timeout?: number,
): Promise<unknown> { ): Promise<unknown> {
if (!this.shouldBeConnected || this.ws == null) { if (!this.shouldBeConnected || this.ws == null) {
throw new NotConnectedError() throw new NotConnectedError(JSON.stringify(request), request)
} }
const [id, message, responsePromise] = this.requestManager.createRequest( const [id, message, responsePromise] = this.requestManager.createRequest(
request, request,
@@ -429,7 +436,9 @@ export class Connection extends EventEmitter {
* @throws Error if the websocket initialized is somehow null. * @throws Error if the websocket initialized is somehow null.
*/ */
// eslint-disable-next-line max-lines-per-function -- Many error code conditionals to check. // eslint-disable-next-line max-lines-per-function -- Many error code conditionals to check.
private async onceOpen(connectionTimeoutID: NodeJS.Timeout): Promise<void> { private async onceOpen(
connectionTimeoutID: ReturnType<typeof setTimeout>,
): Promise<void> {
if (this.ws == null) { if (this.ws == null) {
throw new XrplError('onceOpen: ws is null') throw new XrplError('onceOpen: ws is null')
} }
@@ -458,13 +467,14 @@ export class Connection extends EventEmitter {
this.ws = null this.ws = null
if (code === undefined) { if (code === undefined) {
const reasonText = reason ? reason.toString() : 'undefined' // Useful to keep this code for debugging purposes.
// eslint-disable-next-line no-console -- The error is helpful for debugging. // const reasonText = reason ? reason.toString() : 'undefined'
console.error( // // eslint-disable-next-line no-console -- The error is helpful for debugging.
`Disconnected but the disconnect code was undefined (The given reason was ${reasonText}).` + // console.error(
`This could be caused by an exception being thrown during a 'connect' callback. ` + // `Disconnected but the disconnect code was undefined (The given reason was ${reasonText}).` +
`Disconnecting with code 1011 to indicate an internal error has occurred.`, // `This could be caused by an exception being thrown during a 'connect' callback. ` +
) // `Disconnecting with code 1011 to indicate an internal error has occurred.`,
// )
/* /*
* Error code 1011 represents an Internal Error according to * Error code 1011 represents an Internal Error according to

View File

@@ -454,6 +454,10 @@ class Client extends EventEmitter {
event: 'consensusPhase', event: 'consensusPhase',
listener: (phase: ConsensusStream) => void, listener: (phase: ConsensusStream) => void,
): this ): this
public on(
event: 'manifestReceived',
listener: (manifest: ManifestResponse) => void,
): this
public on(event: 'path_find', listener: (path: PathFindStream) => void): this public on(event: 'path_find', listener: (path: PathFindStream) => void): this
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- needs to be any for overload // eslint-disable-next-line @typescript-eslint/no-explicit-any -- needs to be any for overload
public on(event: 'error', listener: (...err: any[]) => void): this public on(event: 'error', listener: (...err: any[]) => void): this

View File

@@ -108,6 +108,10 @@ export interface ResponseOnlyTxInfo {
* The sequence number of the ledger that included this transaction. * The sequence number of the ledger that included this transaction.
*/ */
ledger_index?: number ledger_index?: number
/**
* @deprecated Alias for ledger_index.
*/
inLedger?: number
} }
/** /**

View File

@@ -49,7 +49,7 @@ export interface LedgerDataRequest extends BaseRequest {
type LabeledLedgerEntry = { ledgerEntryType: string } & LedgerEntry type LabeledLedgerEntry = { ledgerEntryType: string } & LedgerEntry
interface BinaryLedgerEntry { export interface BinaryLedgerEntry {
data: string data: string
} }

View File

@@ -33,6 +33,14 @@ export interface JobType {
in_progress?: number in_progress?: number
} }
// The states for validating and proposing do not exist in the field state_accounting
// See https://github.com/XRPLF/rippled/blob/develop/src/ripple/app/misc/NetworkOPs.cpp#L4545
// https://github.com/XRPLF/rippled/blob/develop/src/ripple/app/misc/NetworkOPs.h#L66
export type StateAccountingFinal = Record<
Exclude<ServerState, 'validating' | 'proposing'>,
StateAccounting
>
/** /**
* Response expected from a {@link ServerInfoRequest}. * Response expected from a {@link ServerInfoRequest}.
* *
@@ -158,6 +166,14 @@ export interface ServerInfoResponse extends BaseResponse {
* cost. * cost.
*/ */
load_factor_server?: number load_factor_server?: number
/**
* The number of peer connections which were severed.
*/
peer_disconnects?: string
/**
* The number of peer connections which were severed due to excess resource consumption.
*/
peer_disconnects_resources?: string
network_ledger?: 'waiting' network_ledger?: 'waiting'
/** How many other rippled servers this one is currently connected to. */ /** How many other rippled servers this one is currently connected to. */
peers: number peers: number
@@ -179,13 +195,13 @@ export interface ServerInfoResponse extends BaseResponse {
* The number of consecutive microseconds the server has been in the * The number of consecutive microseconds the server has been in the
* current state. * current state.
*/ */
server_state_duration_us: number server_state_duration_us: string
/** /**
* A map of various server states with information about the time the * A map of various server states with information about the time the
* server spends in each. This can be useful for tracking the long-term * server spends in each. This can be useful for tracking the long-term
* health of your server's connectivity to the network. * health of your server's connectivity to the network.
*/ */
state_accounting: Record<ServerState, StateAccounting> state_accounting: StateAccountingFinal
/** The current time in UTC, according to the server's clock. */ /** The current time in UTC, according to the server's clock. */
time: string time: string
/** Number of consecutive seconds that the server has been operational. */ /** Number of consecutive seconds that the server has been operational. */
@@ -227,6 +243,11 @@ export interface ServerInfoResponse extends BaseResponse {
* static validator list. * static validator list.
*/ */
validator_list_expires?: string validator_list_expires?: string
validator_list?: {
count: number
expiration: 'never' | 'unknown' | string
status: 'active' | 'expired' | 'unknown'
}
} }
} }
} }

View File

@@ -1,5 +1,5 @@
import { BaseRequest, BaseResponse } from './baseMethod' import { BaseRequest, BaseResponse } from './baseMethod'
import { JobType, ServerState, StateAccounting } from './serverInfo' import { JobType, ServerState, StateAccountingFinal } from './serverInfo'
/** /**
* The `server_state` command asks the server for various machine-readable * The `server_state` command asks the server for various machine-readable
@@ -35,7 +35,10 @@ export interface ServerStateResponse extends BaseResponse {
io_latency_ms: number io_latency_ms: number
jq_trans_overflow: string jq_trans_overflow: string
last_close: { last_close: {
converge_time_s: number // coverage_time_s only exists for `server_info` requests. `server_state` is a "non human" api request,
// therefore the type is coverage_time
// See https://github.com/XRPLF/rippled/blob/83faf43140e27e5d6d6779eaa0ffb75c33d98029/src/ripple/app/misc/NetworkOPs.cpp#L2458
converge_time: number
proposers: number proposers: number
} }
load?: { load?: {
@@ -48,24 +51,27 @@ export interface ServerStateResponse extends BaseResponse {
load_factor_fee_queue?: number load_factor_fee_queue?: number
load_factor_fee_reference?: number load_factor_fee_reference?: number
load_factor_server?: number load_factor_server?: number
peer_disconnects?: string
peer_disconnects_resources?: string
peers: number peers: number
pubkey_node: string pubkey_node: string
pubkey_validator?: string pubkey_validator?: string
server_state: ServerState server_state: ServerState
server_state_duration_us: number server_state_duration_us: string
state_accounting: Record<ServerState, StateAccounting> state_accounting: StateAccountingFinal
time: string time: string
uptime: number uptime: number
validated_ledger?: { validated_ledger?: {
age: number age?: number
base_fee: number base_fee: number
close_time: number
hash: string hash: string
reserve_base: number reserve_base: number
reserve_inc: number reserve_inc: number
seq: number seq: number
} }
validation_quorum: number validation_quorum: number
validator_list_expires?: string validator_list_expires?: number
} }
} }
} }

View File

@@ -53,12 +53,14 @@ export function validateCheckCash(tx: Record<string, unknown>): void {
) )
} }
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Necessary check
if (tx.Amount != null && tx.Amount !== undefined && !isAmount(tx.Amount)) { if (tx.Amount != null && tx.Amount !== undefined && !isAmount(tx.Amount)) {
throw new ValidationError('CheckCash: invalid Amount') throw new ValidationError('CheckCash: invalid Amount')
} }
if ( if (
tx.DeliverMin != null && tx.DeliverMin != null &&
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Necessary check
tx.DeliverMin !== undefined && tx.DeliverMin !== undefined &&
!isAmount(tx.DeliverMin) !isAmount(tx.DeliverMin)
) { ) {

View File

@@ -1,7 +1,8 @@
/* eslint-disable complexity -- verifies 19 tx types hence a lot of checks needed */ /* eslint-disable complexity -- verifies 19 tx types hence a lot of checks needed */
/* eslint-disable max-lines-per-function -- need to work with a lot of Tx verifications */ /* eslint-disable max-lines-per-function -- need to work with a lot of Tx verifications */
import _ from 'lodash' import isEqual from 'lodash/isEqual'
import omitBy from 'lodash/omitBy'
import { encode, decode } from 'ripple-binary-codec' import { encode, decode } from 'ripple-binary-codec'
import { ValidationError } from '../../errors' import { ValidationError } from '../../errors'
@@ -210,9 +211,9 @@ export function validate(transaction: Record<string, unknown>): void {
} }
if ( if (
!_.isEqual( !isEqual(
decode(encode(tx)), decode(encode(tx)),
_.omitBy(tx, (value) => value == null), omitBy(tx, (value) => value == null),
) )
) { ) {
throw new ValidationError(`Invalid Transaction: ${tx.TransactionType}`) throw new ValidationError(`Invalid Transaction: ${tx.TransactionType}`)

View File

@@ -1,4 +1,4 @@
import _ from 'lodash' import flatMap from 'lodash/flatMap'
import type { Client } from '..' import type { Client } from '..'
import { LedgerIndex } from '../models/common' import { LedgerIndex } from '../models/common'
@@ -111,7 +111,7 @@ async function getBalances(
// combine results // combine results
await Promise.all([xrpPromise, linesPromise]).then( await Promise.all([xrpPromise, linesPromise]).then(
([xrpBalance, linesResponses]) => { ([xrpBalance, linesResponses]) => {
const accountLinesBalance = _.flatMap(linesResponses, (response) => const accountLinesBalance = flatMap(linesResponses, (response) =>
formatBalances(response.result.lines), formatBalances(response.result.lines),
) )
if (xrpBalance !== '') { if (xrpBalance !== '') {

View File

@@ -1,8 +1,9 @@
/* eslint-disable max-lines-per-function -- Needs to process orderbooks. */ /* eslint-disable max-lines-per-function -- Needs to process orderbooks. */
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import _ from 'lodash' import flatMap from 'lodash/flatMap'
import type { Client } from '../client' import type { Client } from '../client'
import { ValidationError } from '../errors'
import { LedgerIndex } from '../models/common' import { LedgerIndex } from '../models/common'
import { OfferFlags } from '../models/ledger/Offer' import { OfferFlags } from '../models/ledger/Offer'
import { import {
@@ -22,6 +23,13 @@ function sortOffers(offers: BookOffer[]): BookOffer[] {
}) })
} }
const getOrderbookOptionsSet = new Set([
'limit',
'ledger_index',
'ledger_hash',
'taker',
])
/** /**
* Fetch orderbook (buy/sell orders) between two accounts. * Fetch orderbook (buy/sell orders) between two accounts.
* *
@@ -40,7 +48,7 @@ function sortOffers(offers: BookOffer[]): BookOffer[] {
* the order book. Defaults to 20. * the order book. Defaults to 20.
* @returns An object containing buy and sell objects. * @returns An object containing buy and sell objects.
*/ */
// eslint-disable-next-line max-params -- Once bound to Client, getOrderbook only has 3 parameters. // eslint-disable-next-line max-params, complexity -- Once bound to Client, getOrderbook only has 3 parameters.
async function getOrderbook( async function getOrderbook(
this: Client, this: Client,
takerPays: TakerAmount, takerPays: TakerAmount,
@@ -48,21 +56,60 @@ async function getOrderbook(
options: { options: {
limit?: number limit?: number
ledger_index?: LedgerIndex ledger_index?: LedgerIndex
ledger_hash?: string ledger_hash?: string | null
taker?: string taker?: string | null
} = {}, } = {},
): Promise<{ ): Promise<{
buy: BookOffer[] buy: BookOffer[]
sell: BookOffer[] sell: BookOffer[]
}> { }> {
Object.keys(options).forEach((key) => {
if (!getOrderbookOptionsSet.has(key)) {
throw new ValidationError(`Unexpected option: ${key}`, options)
}
})
if (options.limit && typeof options.limit !== 'number') {
throw new ValidationError('limit must be a number', options.limit)
}
if (
options.ledger_index &&
!(
typeof options.ledger_index === 'number' ||
(typeof options.ledger_index === 'string' &&
['validated', 'closed', 'current'].includes(options.ledger_index))
)
) {
throw new ValidationError(
'ledger_index must be a number or a string of "validated", "closed", or "current"',
options.ledger_index,
)
}
if (
options.ledger_hash !== undefined &&
options.ledger_hash !== null &&
typeof options.ledger_hash !== 'string'
) {
throw new ValidationError(
'ledger_hash must be a string',
options.ledger_hash,
)
}
if (options.taker !== undefined && typeof options.taker !== 'string') {
throw new ValidationError('taker must be a string', options.taker)
}
const request: BookOffersRequest = { const request: BookOffersRequest = {
command: 'book_offers', command: 'book_offers',
taker_pays: takerPays, taker_pays: takerPays,
taker_gets: takerGets, taker_gets: takerGets,
ledger_index: options.ledger_index ?? 'validated', ledger_index: options.ledger_index ?? 'validated',
ledger_hash: options.ledger_hash, ledger_hash: options.ledger_hash === null ? undefined : options.ledger_hash,
limit: options.limit ?? DEFAULT_LIMIT, limit: options.limit ?? DEFAULT_LIMIT,
taker: options.taker, taker: options.taker ? options.taker : undefined,
} }
// 2. Make Request // 2. Make Request
const directOfferResults = await this.requestAll(request) const directOfferResults = await this.requestAll(request)
@@ -70,11 +117,11 @@ async function getOrderbook(
request.taker_pays = takerGets request.taker_pays = takerGets
const reverseOfferResults = await this.requestAll(request) const reverseOfferResults = await this.requestAll(request)
// 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,
) )

View File

@@ -1,5 +1,6 @@
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import _ from 'lodash' import flatten from 'lodash/flatten'
import groupBy from 'lodash/groupBy'
import { Amount, IssuedCurrencyAmount } from '../models/common' import { Amount, IssuedCurrencyAmount } from '../models/common'
import { TransactionMetadata, Node } from '../models/transactions/metadata' import { TransactionMetadata, Node } from '../models/transactions/metadata'
@@ -63,7 +64,7 @@ function groupByAccount(balanceChanges: BalanceChange[]): Array<{
account: string account: string
balances: Balance[] balances: Balance[]
}> { }> {
const grouped = _.groupBy(balanceChanges, (node) => node.account) const grouped = groupBy(balanceChanges, (node) => node.account)
return Object.entries(grouped).map(([account, items]) => { return Object.entries(grouped).map(([account, items]) => {
return { account, balances: items.map((item) => item.balance) } return { account, balances: items.map((item) => item.balance) }
}) })
@@ -186,5 +187,5 @@ export default function getBalanceChanges(
} }
return [] return []
}) })
return groupByAccount(_.flatten(quantities)) return groupByAccount(flatten(quantities))
} }

View File

@@ -1,5 +1,5 @@
import binary from 'ripple-binary-codec' import { encodeForSigningClaim } from 'ripple-binary-codec'
import keypairs from 'ripple-keypairs' import { sign } from 'ripple-keypairs'
import { xrpToDrops } from './xrpConversion' import { xrpToDrops } from './xrpConversion'
@@ -17,11 +17,11 @@ function signPaymentChannelClaim(
amount: string, amount: string,
privateKey: string, privateKey: string,
): string { ): string {
const signingData = binary.encodeForSigningClaim({ const signingData = encodeForSigningClaim({
channel, channel,
amount: xrpToDrops(amount), amount: xrpToDrops(amount),
}) })
return keypairs.sign(signingData, privateKey) return sign(signingData, privateKey)
} }
export default signPaymentChannelClaim export default signPaymentChannelClaim

View File

@@ -1,5 +1,5 @@
import binary from 'ripple-binary-codec' import { encodeForSigningClaim } from 'ripple-binary-codec'
import keypairs from 'ripple-keypairs' import { verify } from 'ripple-keypairs'
import { xrpToDrops } from './xrpConversion' import { xrpToDrops } from './xrpConversion'
@@ -20,11 +20,11 @@ function verifyPaymentChannelClaim(
signature: string, signature: string,
publicKey: string, publicKey: string,
): boolean { ): boolean {
const signingData = binary.encodeForSigningClaim({ const signingData = encodeForSigningClaim({
channel, channel,
amount: xrpToDrops(amount), amount: xrpToDrops(amount),
}) })
return keypairs.verify(signingData, signature, publicKey) return verify(signingData, signature, publicKey)
} }
export default verifyPaymentChannelClaim export default verifyPaymentChannelClaim

View File

@@ -1,5 +1,6 @@
import { assert } from 'chai' import { assert } from 'chai'
import ExponentialBackoff from 'xrpl-local/client/ExponentialBackoff'
import ExponentialBackoff from '../src/client/ExponentialBackoff'
describe('ExponentialBackoff', function () { describe('ExponentialBackoff', function () {
it('duration() return value starts with the min value', function () { it('duration() return value starts with the min value', function () {

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