Compare commits

...

18 Commits
1.3.0 ... 1.3.4

Author SHA1 Message Date
Elliot Lee
eb2a497dee Release 1.3.4 2019-10-18 14:41:58 -07:00
Elliot Lee
988381d584 Merge pull request #1048 from FredKSchott/tsc-update
Update TypeScript
2019-10-16 23:04:20 -07:00
Elliot Lee
0b163eae23 XRP Ledger Hashes: create README.md 2019-10-15 16:34:48 -07:00
Fred K. Schott
fadfd4e06c update typescript 2019-10-13 16:19:45 -07:00
Sohail Salehi
eb521faa8d Doc update for sign() method (#1010)
* Add multi-signing example to the sign() method

Explains why the `signAs` option is compulsory for multi-signing scenarios.
2019-10-08 11:59:12 -07:00
Tyler Storm
14e6bf5ef9 Integrate ripple-hashes (#1039)
* Update TxParser and Mocha

* Convert ripple-hashes to TypeScript and add into ripple-lib along with unit tests

* Switch casing to camelCase

* Document intToVuc

* Convert Slots to numbers, add exception if number is outside 0-15

* Remove Shamap v2 support

* Improve naming
2019-10-01 23:35:11 -07:00
Elliot Lee
b6bddd3b0e [MINOR] Remove unnecessary semicolon 2019-09-26 03:18:54 -07:00
Elliot Lee
d5ed9b6cf5 Improve error message when signing fails
When there are multiple representations of the same value
(for example, trailing zeros) the verification will fail.

This points users to the error.data object for details about
the failure, and adds a diff so that the cause of the
discrepancy can be seen at a glance.
2019-09-24 09:58:24 -07:00
Tyler Storm
7a9912d4e0 Update TxParser and Mocha 2019-09-24 09:57:56 -07:00
Mo Morsi
4d2ddceb4e Change "wipple" to "xrp1ntel" (#1036)
The Wipple Analytics Engine is now accessed through the xrp1ntel website
2019-09-12 13:15:26 -07:00
Elliot Lee
ae2aed675a v1.3.3 2019-09-10 19:30:11 -07:00
Elliot Lee
1d7cb41218 Add rippleTimeToISO8601.md.ejs 2019-09-04 00:31:02 -07:00
Elliot Lee
76db002eb5 Expand node version compatibility
Previously the library could not be installed with node 12:

https://github.com/ripple/ripple-binary-codec/pull/33
2019-09-04 00:17:53 -07:00
Elliot Lee
ebb64ba177 v1.3.2 2019-09-03 16:43:31 -07:00
Elliot Lee
1a685e2b68 Fix #1032 (#1033)
* Update ripple-address-codec@latest

* Export and document rippleTimeToISO8601
2019-09-03 16:38:56 -07:00
Rome Reginelli
9c561885a1 Docs: update recommended Node ver. (#1031) 2019-08-29 20:34:25 -07:00
Elliot Lee
612e98b198 Release 1.3.1 2019-08-26 13:58:09 -07:00
Elliot Lee
0a41d5ccf1 Upgrade to gulp 4; remove http server (#1030)
* Update gulp version to ^4.0.2 and Gulpfile.js

* Remove http server
2019-08-26 13:50:36 -07:00
56 changed files with 1743 additions and 2808 deletions

View File

@@ -12,7 +12,7 @@ Warning: Use at your own risk.
## Data and visualizations
- **[Wipple - XRP Intelligence](https://wipple.devnull.network/)**
- **[xrp1ntel - XRP Intelligence](https://xrp1ntel.com/)**
Monitor the XRP Network in real time and explore historical statistics.

View File

@@ -6,15 +6,12 @@ const fs = require('fs');
const path = require('path');
const assert = require('assert');
const gulp = require('gulp');
const rename = require('gulp-rename');
const webpack = require('webpack');
const bump = require('gulp-bump');
const argv = require('yargs').argv;
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
var pkg = require('./package.json');
const pkg = require('./package.json');
var uglifyOptions = {
const uglifyOptions = {
mangle: {
reserved: ['_', 'RippleError', 'RippledError', 'UnexpectedError',
'LedgerVersionError', 'ConnectionError', 'NotConnectedError',
@@ -36,7 +33,7 @@ function getWebpackConfig(extension, overrides) {
output: {
library: 'ripple',
path: path.join(__dirname, 'build/'),
filename: ['ripple-', extension].join(pkg.version)
filename: `ripple-${pkg.version}${extension}`
},
plugins: [
new webpack.NormalModuleReplacementPlugin(/^ws$/, './wswrapper'),
@@ -69,7 +66,7 @@ function getWebpackConfig(extension, overrides) {
return _.assign({}, defaults, overrides);
}
function webpackConfigForWebTest(testFileName, path) {
function webpackConfigForWebTest(testFileName) {
var match = testFileName.match(/\/?([^\/]*)-test.js$/);
if (!match) {
assert(false, 'wrong filename:' + testFileName);
@@ -83,28 +80,13 @@ function webpackConfigForWebTest(testFileName, path) {
entry: testFileName,
output: {
library: match[1].replace(/-/g, '_'),
path: './test-compiled-for-web/' + (path ? path : ''),
path: path.join(__dirname, 'test-compiled-for-web/'),
filename: match[1] + '-test.js'
}
};
return getWebpackConfig('.js', configOverrides);
}
gulp.task('build-tests', function(callback) {
var times = 0;
function done() {
if (++times >= 5) {
callback();
}
}
webpack(webpackConfigForWebTest('./test/rangeset-test.js'), done);
webpack(webpackConfigForWebTest('./test/connection-test.js'), done);
webpack(webpackConfigForWebTest('./test/api-test.js'), done);
webpack(webpackConfigForWebTest('./test/broadcast-api-test.js'), done);
webpack(webpackConfigForWebTest('./test/integration/integration-test.js',
'integration/'), done);
});
function createLink(from, to) {
if (fs.existsSync(to)) {
fs.unlinkSync(to);
@@ -120,11 +102,22 @@ function createBuildLink(callback) {
};
}
gulp.task('build', function(callback) {
webpack(getWebpackConfig('.js'), createBuildLink(callback));
});
function watch(callback) {
gulp.watch('src/*', gulp.series(buildDebug));
callback();
}
gulp.task('build-min', function(callback) {
function build(callback) {
webpack(getWebpackConfig('.js'), createBuildLink(callback));
}
function buildDebug(callback) {
const webpackConfig = getWebpackConfig('-debug.js', {devtool: 'eval'});
webpackConfig.plugins.unshift(new webpack.LoaderOptionsPlugin({debug: true}));
webpack(webpackConfig, callback);
}
function buildMin(callback) {
const webpackConfig = getWebpackConfig('-min.js');
webpackConfig.plugins.push(new UglifyJsPlugin({uglifyOptions}));
webpack(webpackConfig, function() {
@@ -132,86 +125,27 @@ gulp.task('build-min', function(callback) {
'./build/ripple-latest-min.js');
callback();
});
});
gulp.task('build-debug', function(callback) {
const webpackConfig = getWebpackConfig('-debug.js', {devtool: 'eval'});
webpackConfig.plugins.unshift(new webpack.LoaderOptionsPlugin({debug: true}));
webpack(webpackConfig, callback);
});
/**
* Generate a WebPack external for a given unavailable module which replaces
* that module's constructor with an error-thrower
*/
function buildUseError(cons) {
return ('var {<CONS>:function(){throw new Error('
+ '"Class is unavailable in this build: <CONS>")}}')
.replace(new RegExp('<CONS>', 'g'), cons);
}
gulp.task('build-core', function(callback) {
var configOverrides = {
cache: false,
entry: './src/remote.ts',
externals: [{
'./transaction': buildUseError('Transaction'),
'./orderbook': buildUseError('OrderBook'),
'./account': buildUseError('Account'),
'./serializedobject': buildUseError('SerializedObject')
}],
plugins: [
new UglifyJsPlugin()
]
};
webpack(getWebpackConfig('-core.js', configOverrides), callback);
});
gulp.task('bower-build', ['build'], function() {
return gulp.src(['./build/ripple-', '.js'].join(pkg.version))
.pipe(rename('ripple.js'))
.pipe(gulp.dest('./dist/bower'));
});
gulp.task('bower-build-min', ['build-min'], function() {
return gulp.src(['./build/ripple-', '-min.js'].join(pkg.version))
.pipe(rename('ripple-min.js'))
.pipe(gulp.dest('./dist/bower'));
});
gulp.task('bower-build-debug', ['build-debug'], function() {
return gulp.src(['./build/ripple-', '-debug.js'].join(pkg.version))
.pipe(rename('ripple-debug.js'))
.pipe(gulp.dest('./dist/bower'));
});
gulp.task('bower-version', function() {
gulp.src('./dist/bower/bower.json')
.pipe(bump({version: pkg.version}))
.pipe(gulp.dest('./dist/bower'));
});
gulp.task('bower', ['bower-build', 'bower-build-min', 'bower-build-debug',
'bower-version']);
gulp.task('watch', function() {
gulp.watch('src/*', ['build-debug']);
});
gulp.task('version-bump', function() {
if (!argv.type) {
throw new Error('No type found, pass it in using the --type argument');
function buildTests(callback) {
var times = 0;
function done() {
if (++times >= 5) {
callback();
}
}
webpack(webpackConfigForWebTest('./test/rangeset-test.js'), done);
webpack(webpackConfigForWebTest('./test/connection-test.js'), done);
webpack(webpackConfigForWebTest('./test/api-test.js'), done);
webpack(webpackConfigForWebTest('./test/broadcast-api-test.js'), done);
webpack(webpackConfigForWebTest('./test/integration/integration-test.js',
'integration/'), done);
}
gulp.src('./package.json')
.pipe(bump({type: argv.type}))
.pipe(gulp.dest('./'));
});
exports.watch = watch;
exports.build = build;
exports.buildDebug = buildDebug;
exports.buildMin = buildMin;
exports.buildTests = buildTests;
gulp.task('version-beta', function() {
gulp.src('./package.json')
.pipe(bump({version: pkg.version + '-beta'}))
.pipe(gulp.dest('./'));
});
gulp.task('default', ['build', 'build-debug', 'build-min']);
exports.default = gulp.parallel(build, buildDebug, buildMin);

View File

@@ -1,5 +1,35 @@
# ripple-lib Release History
## 1.3.4 (2019-10-18)
* Update ripple-lib-transactionparser
* Improve error message when signing fails (e.g. due to trailing zeros)
* Integrate ripple-hashes (in TypeScript with improved naming and docs)
* Add multi-signing example to sign() method docs
* Update TypeScript
## 1.3.3 (2019-09-10)
* Expand node version compatibility to support Node.js 12 ([ripple-binary-codec#32](https://github.com/ripple/ripple-binary-codec/issues/32))
## 1.3.2 (2019-09-03)
* Export and document `rippleTimeToISO8601()` method
* Add type for isValidAddress (fixes error TS7016, #1032)
* Docs: update recommended Node.js version (#1031)
When using this release with [rippled](https://github.com/ripple/rippled), we recommend using [rippled version 1.3.1](https://github.com/ripple/rippled/releases/tag/1.3.1) or later.
## 1.3.1 (2019-08-26)
* Upgrade to gulp 4 (#1030)
* Remove http server (unused)
* Update dependencies
There are no changes in the browser version with this release. The npm package for Node.js should be slightly smaller.
When using this release with [rippled](https://github.com/ripple/rippled), we recommend using [rippled version 1.3.1](https://github.com/ripple/rippled/releases/tag/1.3.1).
## 1.3.0 (2019-08-16)
### Bug fixes:

View File

@@ -91,6 +91,7 @@
- [xrpToDrops](#xrptodrops)
- [dropsToXrp](#dropstoxrp)
- [iso8601ToRippleTime](#iso8601torippletime)
- [rippleTimeToISO8601](#rippletimetoiso8601)
- [txFlags](#txflags)
- [schemaValidator](#schemavalidator)
- [schemaValidate](#schemavalidate)
@@ -141,7 +142,7 @@ api.connect().then(() => {
}).catch(console.error);
```
RippleAPI is designed to work in [Node.js](https://nodejs.org) version **6.11.3**. RippleAPI may work on older Node.js versions if you use [Babel](https://babeljs.io/) for [ECMAScript 6](https://babeljs.io/docs/learn-es2015/) support.
RippleAPI is designed to work in [Node.js](https://nodejs.org) version 6 or higher. Ripple recommends Node.js v10 LTS.
The code samples in this documentation are written with ECMAScript 6 (ES6) features, but `RippleAPI` also works with ECMAScript 5 (ES5). Regardless of whether you use ES5 or ES6, the methods that return Promises return [ES6-style promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
@@ -5402,6 +5403,8 @@ options | object | *Optional* Options that control the type of signature that wi
*options.* signAs | [address](#address) | *Optional* The account that the signature should count for in multisigning.
secret | secret string | *Optional* The secret of the account that is initiating the transaction. (This field cannot be used with keypair).
Please note that when this method is used for multisigning, the `options` parameter is not *Optional* anymore. It will be compulsory. See the multisigning example in this section for more details.
### Return Value
This method returns an object with the following structure:
@@ -5428,6 +5431,93 @@ return api.sign(txJSON, secret); // or: api.sign(txJSON, keypair);
}
```
### Example (multisigning)
```javascript
const RippleAPI = require('ripple-lib').RippleAPI;
// jon's address will have a multi-signing setup with a quorum of 2
const jon = {
account: 'rJKpme4m2zBQceBuU89d7vLMzgoUw2Ptj',
secret: 'sh4Va7b1wQof8knHFV2sxwX12fSgK'
};
const aya = {
account: 'rnrPdBjs98fFFfmRpL6hM7exT788SWQPFN',
secret: 'snaMuMrXeVc2Vd4NYvHofeGNjgYoe'
};
const bran = {
account: 'rJ93RLnT1t5A8fCr7HTScw7WtfKJMRXodH',
secret: 'shQtQ8Um5MS218yvEU3Ehy1eZQKqH'
};
// Setup the signers list with a quorum of 2
const multiSignSetupTransaction = {
"Flags": 0,
"TransactionType": "SignerListSet",
"Account": "rJKpme4m2zBQceBuU89d7vLMzgoUw2Ptj",
"Fee": "120",
"SignerQuorum": 2,
"SignerEntries": [
{
"SignerEntry": {
"Account": "rnrPdBjs98fFFfmRpL6hM7exT788SWQPFN",
"SignerWeight": 2
}
},
{
"SignerEntry": {
"Account": "rJ93RLnT1t5A8fCr7HTScw7WtfKJMRXodH",
"SignerWeight": 1
}
},
]
};
// a transaction which requires multi signing
const multiSignPaymentTransaction = {
TransactionType: 'Payment',
Account: 'rJKpme4m2zBQceBuU89d7vLMzgoUw2Ptj',
Destination: 'rJ93RLnT1t5A8fCr7HTScw7WtfKJMRXodH',
Amount: '88000000'
};
const api = new RippleAPI({
server: 'wss://s.altnet.rippletest.net:51233'
});
api.connect().then(() => {
// adding the multi signing feature to jon's account
api.prepareTransaction(multiSignSetupTransaction).then((prepared) => {
console.log(prepared);
jonSign = api.sign(prepared.txJSON, jon.secret).signedTransaction;
api.submit(jonSign).then( response => {
console.log(response.resultCode, response.resultMessage);
// multi sign a transaction
api.prepareTransaction(multiSignPaymentTransaction).then(prepared => {
console.log(prepared);
// Aya and Bran sign it too but with 'signAs' set to their own account
let ayaSign = api.sign(prepared.txJSON, aya.secret, {'signAs': aya.account}).signedTransaction;
let branSign = api.sign(prepared.txJSON, bran.secret, {'signAs': bran.account}).signedTransaction;
// signatures are combined and submitted
let combinedTx = api.combine([ayaSign, branSign]);
api.submit(combinedTx.signedTransaction).then(response => {
console.log(response.tx_json.hash);
return api.disconnect();
}).catch(console.error);
}).catch(console.error);
}).catch(console.error)
}).catch(console.error);
}).catch(console.error);
```
Assuming the multisigning account was setup properly, the above example will respond with `resultCode: 'tesSUCCESS'` and the hash for the transaction.
If any of `{signAs: some_address}` options were missing the code will return a validation error as follow:
```
[ValidationError(txJSON is not the same for all signedTransactions)]
```
## combine
@@ -5860,6 +5950,34 @@ api.iso8601ToRippleTime('2017-02-17T15:04:57Z');
540659097
```
## rippleTimeToISO8601
`rippleTimeToISO8601(rippleTime: number): string`
This method takes the number of seconds since the "Ripple Epoch" of January 1, 2000 (00:00 UTC) and returns a string representation of a date.
The Ripple Epoch is 946684800 seconds after the Unix Epoch.
This method is useful for interpreting timestamps returned by the rippled APIs. The rippled APIs represent time as an unsigned integer of the number of seconds since the Ripple Epoch.
### Parameters
`rippleTime`: A number of seconds since the Ripple Epoch.
### Return Value
A string representing a date and time, created by calling a `Date` object's `toISOString()` method.
### Example
```javascript
api.rippleTimeToISO8601(540659097);
```
```json
'2017-02-17T15:04:57.000Z'
```
## txFlags
`txFlags.TRANSACTION_TYPE.FLAG`

View File

@@ -26,7 +26,7 @@ api.connect().then(() => {
}).catch(console.error);
```
RippleAPI is designed to work in [Node.js](https://nodejs.org) version **6.11.3**. RippleAPI may work on older Node.js versions if you use [Babel](https://babeljs.io/) for [ECMAScript 6](https://babeljs.io/docs/learn-es2015/) support.
RippleAPI is designed to work in [Node.js](https://nodejs.org) version 6 or higher. Ripple recommends Node.js v10 LTS.
The code samples in this documentation are written with ECMAScript 6 (ES6) features, but `RippleAPI` also works with ECMAScript 5 (ES5). Regardless of whether you use ES5 or ES6, the methods that return Promises return [ES6-style promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).

View File

@@ -62,6 +62,7 @@
<% include computeLedgerHash.md.ejs %>
<% include xrpToDropsAndDropsToXrp.md.ejs %>
<% include iso8601ToRippleTime.md.ejs %>
<% include rippleTimeToISO8601.md.ejs %>
<% include txFlags.md.ejs %>
<% include schemaValidator.md.ejs %>
<% include events.md.ejs %>

View File

@@ -0,0 +1,27 @@
## rippleTimeToISO8601
`rippleTimeToISO8601(rippleTime: number): string`
This method takes the number of seconds since the "Ripple Epoch" of January 1, 2000 (00:00 UTC) and returns a string representation of a date.
The Ripple Epoch is 946684800 seconds after the Unix Epoch.
This method is useful for interpreting timestamps returned by the rippled APIs. The rippled APIs represent time as an unsigned integer of the number of seconds since the Ripple Epoch.
### Parameters
`rippleTime`: A number of seconds since the Ripple Epoch.
### Return Value
A string representing a date and time, created by calling a `Date` object's `toISOString()` method.
### Example
```javascript
api.rippleTimeToISO8601(540659097);
```
```json
'2017-02-17T15:04:57.000Z'
```

View File

@@ -1,6 +1,6 @@
{
"name": "ripple-lib",
"version": "1.3.0",
"version": "1.3.4",
"license": "ISC",
"description": "A JavaScript API for interacting with Ripple in Node.js and the browser",
"files": [
@@ -23,11 +23,11 @@
"https-proxy-agent": "2.2.1",
"jsonschema": "1.2.2",
"lodash": "^4.17.4",
"ripple-address-codec": "^2.0.1",
"ripple-binary-codec": "0.2.2",
"ripple-hashes": "0.3.2",
"lodash.isequal": "^4.5.0",
"ripple-address-codec": "^3.0.4",
"ripple-binary-codec": "^0.2.4",
"ripple-keypairs": "^0.10.1",
"ripple-lib-transactionparser": "0.7.1",
"ripple-lib-transactionparser": "0.8.0",
"ws": "^3.3.1"
},
"devDependencies": {
@@ -36,13 +36,10 @@
"doctoc": "^0.15.0",
"ejs": "^2.3.4",
"eventemitter2": "^0.4.14",
"gulp": "^3.8.10",
"gulp-bump": "^0.1.13",
"gulp-rename": "^1.2.0",
"jayson": "^1.2.2",
"gulp": "^4.0.2",
"json-loader": "^0.5.2",
"json-schema-to-markdown-table": "^0.4.0",
"mocha": "6.1.3",
"mocha": "6.2.0",
"mocha-junit-reporter": "^1.9.1",
"null-loader": "^0.1.1",
"nyc": "^14.1.1",
@@ -51,24 +48,22 @@
"ts-node": "8.0.3",
"tslint": "^5.8.0",
"tslint-eslint-rules": "^4.1.1",
"typescript": "3.4.2",
"typescript": "^3.6.4",
"uglifyjs-webpack-plugin": "^1.1.4",
"webpack": "3.12.0",
"yargs": "13.2.2"
"webpack": "3.12.0"
},
"scripts": {
"build": "gulp",
"doctoc": "doctoc docs/index.md --title '# RippleAPI Reference' --github --maxlevel 2",
"docgen": "node --harmony scripts/build_docs.js",
"clean": "rm -rf dist/npm",
"compile": "mkdir -p dist/npm/common/js && cp -r src/common/js/ dist/npm/common/js/ && mkdir -p dist/npm/common && cp -r src/common/schemas dist/npm/common/ && tsc --build",
"compile": "mkdir -p dist/npm/common && cp -r src/common/schemas dist/npm/common/ && tsc --build",
"watch": "tsc -w",
"prepublish": "npm run clean && npm run compile && npm run build",
"test": "TS_NODE_PROJECT=src/tsconfig.json nyc mocha --exit",
"lint": "tslint -p ./",
"perf": "./scripts/perf_test.sh",
"start": "node scripts/http.js",
"sauce": "node scripts/sauce-runner.js"
"start": "node scripts/http.js"
},
"repository": {
"type": "git",

View File

@@ -45,7 +45,6 @@ unittest() {
integrationtest() {
mocha test/integration/integration-test.js
mocha test/integration/http-integration-test.js
# run integration tests in PhantomJS
#gulp build-tests build-min

View File

@@ -1,16 +0,0 @@
'use strict';
const createHTTPServer = require('../dist/npm/http').createHTTPServer;
const port = 5990;
const serverUrl = 'wss://s1.ripple.com';
function main() {
const server = createHTTPServer({server: serverUrl}, port);
server.start().then(() => {
console.log('Server started on port ' + String(port));
});
}
main();

View File

@@ -5,6 +5,7 @@ import {
validate,
xrpToDrops,
dropsToXrp,
rippleTimeToISO8601,
iso8601ToRippleTime,
txFlags
} from './common'
@@ -337,6 +338,7 @@ class RippleAPI extends EventEmitter {
xrpToDrops = xrpToDrops
dropsToXrp = dropsToXrp
rippleTimeToISO8601 = rippleTimeToISO8601
iso8601ToRippleTime = iso8601ToRippleTime
txFlags = txFlags

View File

@@ -1,7 +1,7 @@
import * as _ from 'lodash'
import {EventEmitter} from 'events'
import {parse as parseUrl} from 'url'
import * as WebSocket from 'ws'
import WebSocket from 'ws'
import RangeSet from './rangeset'
import {RippledError, DisconnectedError, NotConnectedError,
TimeoutError, ResponseFormatError, ConnectionError,

View File

@@ -0,0 +1,49 @@
# XRP Ledger Hashes
Methods to hash XRP Ledger objects
## Methods
### computeBinaryTransactionHash = (txBlobHex: string): string
Compute the hash of a binary transaction blob.
### computeTransactionHash = (txJSON: any): string
Compute the hash of a transaction in txJSON format.
### computeBinaryTransactionSigningHash = (txBlobHex: string): string
### computeTransactionSigningHash = (txJSON: any): string
### computeAccountHash = (address: string): string
Compute the hash of an account, given the account's classic address (starting with `r`).
### computeSignerListHash = (address: string): string
Compute the hash of an account's SignerList.
### computeOrderHash = (address: string, sequence: number): string
Compute the hash of an order, given the owner's classic address (starting with `r`) and the account sequence number of the `OfferCreate` order transaction.
### computeTrustlineHash = (address1: string, address2: string, currency: string): string
Compute the hash of a trustline, given the two parties' classic addresses (starting with `r`) and the currency code.
### computeTransactionTreeHash = (transactions: any[]): string
### computeStateTreeHash = (entries: any[]): string
### computeLedgerHash = (ledgerHeader): string
Compute the hash of a ledger.
### computeEscrowHash = (address, sequence): string
Compute the hash of an escrow, given the owner's classic address (starting with `r`) and the account sequence number of the `EscrowCreate` escrow transaction.
### computePaymentChannelHash = (address, dstAddress, sequence): string
Compute the hash of a payment channel, given the owner's classic address (starting with `r`), the classic address of the destination, and the account sequence number of the `PaymentChannelCreate` payment channel transaction.

View File

@@ -0,0 +1,40 @@
/**
* Prefix for hashing functions.
*
* These prefixes are inserted before the source material used to
* generate various hashes. This is done to put each hash in its own
* "space." This way, two different types of objects with the
* same binary data will produce different hashes.
*
* Each prefix is a 4-byte value with the last byte set to zero
* and the first three bytes formed from the ASCII equivalent of
* some arbitrary string. For example "TXN".
*/
enum HashPrefix {
// transaction plus signature to give transaction ID
TRANSACTION_ID = 0x54584E00, // 'TXN'
// transaction plus metadata
TRANSACTION_NODE = 0x534E4400, // 'TND'
// inner node in tree
INNER_NODE = 0x4D494E00, // 'MIN'
// leaf node in tree
LEAF_NODE = 0x4D4C4E00, // 'MLN'
// inner transaction to sign
TRANSACTION_SIGN = 0x53545800, // 'STX'
// inner transaction to sign (TESTNET)
TRANSACTION_SIGN_TESTNET = 0x73747800, // 'stx'
// inner transaction to multisign
TRANSACTION_MULTISIGN = 0x534D5400, // 'SMT'
// ledger
LEDGER = 0x4C575200 // 'LWR'
}
export default HashPrefix

155
src/common/hashes/index.ts Normal file
View File

@@ -0,0 +1,155 @@
import BigNumber from 'bignumber.js'
import {decodeAddress} from 'ripple-address-codec'
import sha512Half from './sha512Half'
import HashPrefix from './hash-prefix'
import {SHAMap, NodeType} from './shamap'
import {encode} from 'ripple-binary-codec'
import ledgerspaces from './ledgerspaces'
const padLeftZero = (string: string, length: number): string => {
return Array(length - string.length + 1).join('0') + string
}
const intToHex = (integer: number, byteLength: number): string => {
return padLeftZero(Number(integer).toString(16), byteLength * 2)
}
const bytesToHex = (bytes: number[]): string => {
return Buffer.from(bytes).toString('hex')
}
const bigintToHex = (integerString: string | number | BigNumber, byteLength: number): string => {
const hex = (new BigNumber(integerString)).toString(16)
return padLeftZero(hex, byteLength * 2)
}
const ledgerSpaceHex = (name: string): string => {
return intToHex(ledgerspaces[name].charCodeAt(0), 2)
}
const addressToHex = (address: string): string => {
return (Buffer.from(decodeAddress(address))).toString('hex')
}
const currencyToHex = (currency: string): string => {
if (currency.length === 3) {
let bytes = new Array(20 + 1).join('0').split('').map(parseFloat)
bytes[12] = currency.charCodeAt(0) & 0xff
bytes[13] = currency.charCodeAt(1) & 0xff
bytes[14] = currency.charCodeAt(2) & 0xff
return bytesToHex(bytes)
}
return currency
}
const addLengthPrefix = (hex: string): string => {
const length = hex.length / 2
if (length <= 192) {
return bytesToHex([length]) + hex
} else if (length <= 12480) {
const x = length - 193
return bytesToHex([193 + (x >>> 8), x & 0xff]) + hex
} else if (length <= 918744) {
const x = length - 12481
return bytesToHex([241 + (x >>> 16), x >>> 8 & 0xff, x & 0xff]) + hex
}
throw new Error('Variable integer overflow.')
}
export const computeBinaryTransactionHash = (txBlobHex: string): string => {
const prefix = HashPrefix.TRANSACTION_ID.toString(16).toUpperCase()
return sha512Half(prefix + txBlobHex)
}
export const computeTransactionHash = (txJSON: any): string => {
return computeBinaryTransactionHash(encode(txJSON))
}
export const computeBinaryTransactionSigningHash = (txBlobHex: string): string => {
const prefix = HashPrefix.TRANSACTION_SIGN.toString(16).toUpperCase()
return sha512Half(prefix + txBlobHex)
}
export const computeTransactionSigningHash = (txJSON: any): string => {
return computeBinaryTransactionSigningHash(encode(txJSON))
}
export const computeAccountHash = (address: string): string => {
return sha512Half(ledgerSpaceHex('account') + addressToHex(address))
}
export const computeSignerListHash = (address: string): string => {
return sha512Half(ledgerSpaceHex('signerList') +
addressToHex(address) +
'00000000' /* uint32(0) signer list index */)
}
export const computeOrderHash = (address: string, sequence: number): string => {
const prefix = '00' + intToHex(ledgerspaces.offer.charCodeAt(0), 1)
return sha512Half(prefix + addressToHex(address) + intToHex(sequence, 4))
}
export const computeTrustlineHash = (address1: string, address2: string, currency: string): string => {
const address1Hex = addressToHex(address1)
const address2Hex = addressToHex(address2)
const swap = (new BigNumber(address1Hex, 16)).greaterThan(
new BigNumber(address2Hex, 16))
const lowAddressHex = swap ? address2Hex : address1Hex
const highAddressHex = swap ? address1Hex : address2Hex
const prefix = ledgerSpaceHex('rippleState')
return sha512Half(prefix + lowAddressHex + highAddressHex +
currencyToHex(currency))
}
export const computeTransactionTreeHash = (transactions: any[]): string => {
const shamap = new SHAMap()
transactions.forEach((txJSON) => {
const txBlobHex = encode(txJSON)
const metaHex = encode(txJSON.metaData)
const txHash = computeBinaryTransactionHash(txBlobHex)
const data = addLengthPrefix(txBlobHex) + addLengthPrefix(metaHex)
shamap.addItem(txHash, data, NodeType.TRANSACTION_METADATA)
})
return shamap.hash
}
export const computeStateTreeHash = (entries: any[]): string => {
const shamap = new SHAMap()
entries.forEach((ledgerEntry) => {
const data = encode(ledgerEntry)
shamap.addItem(ledgerEntry.index, data, NodeType.ACCOUNT_STATE)
})
return shamap.hash
}
// see rippled Ledger::updateHash()
export const computeLedgerHash = (ledgerHeader): string => {
const prefix = HashPrefix.LEDGER.toString(16).toUpperCase()
return sha512Half(prefix +
intToHex(ledgerHeader.ledger_index, 4) +
bigintToHex(ledgerHeader.total_coins, 8) +
ledgerHeader.parent_hash +
ledgerHeader.transaction_hash +
ledgerHeader.account_hash +
intToHex(ledgerHeader.parent_close_time, 4) +
intToHex(ledgerHeader.close_time, 4) +
intToHex(ledgerHeader.close_time_resolution, 1) +
intToHex(ledgerHeader.close_flags, 1)
)
}
export const computeEscrowHash = (address, sequence): string => {
return sha512Half(ledgerSpaceHex('escrow') + addressToHex(address) +
intToHex(sequence, 4))
}
export const computePaymentChannelHash = (address, dstAddress, sequence): string => {
return sha512Half(ledgerSpaceHex('paychan') + addressToHex(address) +
addressToHex(dstAddress) + intToHex(sequence, 4))
}

View File

@@ -0,0 +1,25 @@
/**
* Ripple ledger namespace prefixes.
*
* The Ripple ledger is a key-value store. In order to avoid name collisions,
* names are partitioned into namespaces.
*
* Each namespace is just a single character prefix.
*/
export default {
account : 'a',
dirNode : 'd',
generatorMap : 'g',
rippleState : 'r',
offer : 'o', // Entry for an offer.
ownerDir : 'O', // Directory of things owned by an account.
bookDir : 'B', // Directory of order books.
contract : 'c',
skipList : 's',
amendment : 'f',
feeSettings : 'e',
signerList : 'S',
escrow : 'u',
paychan : 'x'
}

View File

@@ -0,0 +1,7 @@
import {createHash} from 'crypto'
const sha512Half = (hex: string): string => {
return createHash('sha512').update(Buffer.from(hex, 'hex')).digest('hex').toUpperCase().slice(0, 64)
}
export default sha512Half

173
src/common/hashes/shamap.ts Normal file
View File

@@ -0,0 +1,173 @@
import hashPrefix from './hash-prefix'
import sha512Half from './sha512Half'
const HEX_ZERO = '0000000000000000000000000000000000000000000000000000000000000000'
export enum NodeType {
INNER = 1,
TRANSACTION_NO_METADATA = 2,
TRANSACTION_METADATA = 3,
ACCOUNT_STATE = 4
}
export abstract class Node {
/**
* Abstract constructor representing a node in a SHAMap tree.
* Can be either InnerNode or Leaf.
* @constructor
*/
public constructor() {}
public addItem(_tag: string, _node: Node): void {
throw new Error('Called unimplemented virtual method SHAMapTreeNode#addItem.')
}
public get hash(): string|void {
throw new Error('Called unimplemented virtual method SHAMapTreeNode#hash.');
}
}
export class InnerNode extends Node {
public leaves: { [slot: number]: Node }
public type: NodeType
public depth: number
public empty: boolean
/**
* Define an Inner (non-leaf) node in a SHAMap tree.
* @param {number} depth i.e. how many parent inner nodes
* @constructor
*/
public constructor(depth: number = 0) {
super()
this.leaves = {}
this.type = NodeType.INNER
this.depth = depth
this.empty = true
}
/**
* @param {string} tag equates to a ledger entry `index`
* @param {Node} node to add
* @return {void}
*/
public addItem(tag: string, node: Node): void {
const existingNode = this.getNode(parseInt(tag[this.depth],16))
if (existingNode) {
// A node already exists in this slot
if (existingNode instanceof InnerNode) {
// There is an inner node, so we need to go deeper
existingNode.addItem(tag, node)
} else if (existingNode instanceof Leaf) {
if (existingNode.tag === tag) {
// Collision
throw new Error(
'Tried to add a node to a SHAMap that was already in there.')
} else {
// Turn it into an inner node
const newInnerNode = new InnerNode(this.depth + 1)
// Parent new and existing node
newInnerNode.addItem(existingNode.tag, existingNode)
newInnerNode.addItem(tag, node)
// And place the newly created inner node in the slot
this.setNode(parseInt(tag[this.depth], 16), newInnerNode)
}
}
} else {
// Neat, we have a nice open spot for the new node
this.setNode(parseInt(tag[this.depth], 16), node)
}
}
/**
* Overwrite the node that is currently in a given slot.
* @param {number} slot a number 0-15
* @param {Node} node to place
* @return {void}
*/
public setNode(slot: number, node: Node): void {
if (slot < 0 || slot > 15) {
throw new Error ('Invalid slot: slot must be between 0-15.')
}
this.leaves[slot] = node
this.empty = false
}
/**
* Get the node that is currently in a given slot.
* @param {number} slot a number 0-15
* @return {Node}
*/
public getNode(slot: number): Node {
if (slot < 0 || slot > 15) {
throw new Error ('Invalid slot: slot must be between 0-15.')
}
return this.leaves[slot]
}
public get hash(): string {
if (this.empty) return HEX_ZERO
let hex = ''
for (let i = 0; i < 16; i++) {
hex += this.leaves[i] ? this.leaves[i].hash : HEX_ZERO
}
const prefix = hashPrefix.INNER_NODE.toString(16)
return sha512Half(prefix + hex)
}
}
export class Leaf extends Node {
public tag: string
public type: NodeType
public data: string
/**
* Leaf node in a SHAMap tree.
* @param {string} tag equates to a ledger entry `index`
* @param {string} data hex of account state, transaction etc
* @param {number} type one of TYPE_ACCOUNT_STATE, TYPE_TRANSACTION_MD etc
* @constructor
*/
public constructor(tag: string, data: string, type: NodeType) {
super()
this.tag = tag
this.type = type
this.data = data
}
public get hash(): string|void {
switch (this.type) {
case NodeType.ACCOUNT_STATE:
const leafPrefix = hashPrefix.LEAF_NODE.toString(16)
return sha512Half(leafPrefix + this.data + this.tag)
case NodeType.TRANSACTION_NO_METADATA:
const txIDPrefix = hashPrefix.TRANSACTION_ID.toString(16)
return sha512Half(txIDPrefix + this.data)
case NodeType.TRANSACTION_METADATA:
const txNodePrefix = hashPrefix.TRANSACTION_NODE.toString(16)
return sha512Half(txNodePrefix + this.data + this.tag)
default:
throw new Error('Tried to hash a SHAMap node of unknown type.')
}
}
}
export class SHAMap {
public root: InnerNode
/**
* SHAMap tree.
* @constructor
*/
public constructor() {
this.root = new InnerNode(0)
}
public addItem(tag: string, data: string, type: NodeType): void {
this.root.addItem(tag, new Leaf(tag, data, type))
}
public get hash(): string {
return this.root.hash
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -35,7 +35,7 @@ class RangeSet {
}
addRange(start: number, end: number) {
assert(start <= end, `invalid range ${start} <= ${end}`)
assert.ok(start <= end, `invalid range ${start} <= ${end}`)
this.ranges = mergeIntervals(this.ranges.concat([[start, end]]))
}

View File

@@ -121,7 +121,7 @@ function loadSchemas() {
]
const titles = schemas.map(schema => schema.title)
const duplicates = _.keys(_.pickBy(_.countBy(titles), count => count > 1))
assert(duplicates.length === 0, 'Duplicate schemas for: ' + duplicates)
assert.ok(duplicates.length === 0, 'Duplicate schemas for: ' + duplicates)
const validator = new Validator()
// Register custom format validators that ignore undefined instances
// since jsonschema will still call the format validator on a missing

View File

@@ -1,84 +0,0 @@
/* eslint-disable new-cap */
import * as assert from 'assert'
import * as _ from 'lodash'
import * as jayson from 'jayson'
import {RippleAPI} from './api'
/* istanbul ignore next */
function createHTTPServer(options, httpPort) {
const rippleAPI = new RippleAPI(options)
const methodNames = _.filter(_.keys(RippleAPI.prototype), k => {
return typeof RippleAPI.prototype[k] === 'function'
&& k !== 'connect'
&& k !== 'disconnect'
&& k !== 'constructor'
&& k !== 'RippleAPI'
})
function applyPromiseWithCallback(fnName, callback, args_) {
try {
let args = args_
if (!_.isArray(args_)) {
const fnParameters = jayson.Utils.getParameterNames(rippleAPI[fnName])
args = fnParameters.map(name => args_[name])
const defaultArgs = _.omit(args_, fnParameters)
assert(_.size(defaultArgs) <= 1,
'Function must have no more than one default argument')
if (_.size(defaultArgs) > 0) {
args.push(defaultArgs[_.keys(defaultArgs)[0]])
}
}
Promise.resolve(rippleAPI[fnName](...args))
.then(res => callback(null, res))
.catch(err => {
callback({code: 99, message: err.message, data: {name: err.name}})
})
} catch (err) {
callback({code: 99, message: err.message, data: {name: err.name}})
}
}
const methods = {}
_.forEach(methodNames, fn => {
methods[fn] = jayson.Method((args, cb) => {
applyPromiseWithCallback(fn, cb, args)
}, {collect: true})
})
const server = jayson.server(methods)
let httpServer = null
return {
server: server,
start: function() {
if (httpServer !== null) {
return Promise.reject('Already started')
}
return new Promise(resolve => {
rippleAPI.connect().then(() => {
httpServer = server.http()
httpServer.listen(httpPort, resolve)
})
})
},
stop: function() {
if (httpServer === null) {
return Promise.reject('Not started')
}
return new Promise(resolve => {
rippleAPI.disconnect()
httpServer.close(() => {
httpServer = null
resolve()
})
})
}
}
}
export {
createHTTPServer
}

View File

@@ -1,7 +1,7 @@
import * as assert from 'assert'
function parseOrderCancellation(tx: any): object {
assert(tx.TransactionType === 'OfferCancel')
assert.ok(tx.TransactionType === 'OfferCancel')
return {
orderSequence: tx.OfferSequence
}

View File

@@ -8,7 +8,7 @@ export type FormattedCheckCancel = {
}
function parseCheckCancel(tx: any): FormattedCheckCancel {
assert(tx.TransactionType === 'CheckCancel')
assert.ok(tx.TransactionType === 'CheckCancel')
return removeUndefined({
checkID: tx.CheckID

View File

@@ -23,7 +23,7 @@ export type FormattedCheckCash = {
}
function parseCheckCash(tx: any): FormattedCheckCash {
assert(tx.TransactionType === 'CheckCash')
assert.ok(tx.TransactionType === 'CheckCash')
return removeUndefined({
checkID: tx.CheckID,

View File

@@ -24,7 +24,7 @@ export type FormattedCheckCreate = {
}
function parseCheckCreate(tx: any): FormattedCheckCreate {
assert(tx.TransactionType === 'CheckCreate')
assert.ok(tx.TransactionType === 'CheckCreate')
return removeUndefined({
destination: tx.Destination,

View File

@@ -10,7 +10,7 @@ export type FormattedDepositPreauth = {
}
function parseDepositPreauth(tx: any): FormattedDepositPreauth {
assert(tx.TransactionType === 'DepositPreauth')
assert.ok(tx.TransactionType === 'DepositPreauth')
return removeUndefined({
authorize: tx.Authorize,

View File

@@ -3,7 +3,7 @@ import {parseMemos} from './utils'
import {removeUndefined} from '../../common'
function parseEscrowCancellation(tx: any): object {
assert(tx.TransactionType === 'EscrowCancel')
assert.ok(tx.TransactionType === 'EscrowCancel')
return removeUndefined({
memos: parseMemos(tx),

View File

@@ -4,7 +4,7 @@ import {parseTimestamp, parseMemos} from './utils'
import {removeUndefined} from '../../common'
function parseEscrowCreation(tx: any): object {
assert(tx.TransactionType === 'EscrowCreate')
assert.ok(tx.TransactionType === 'EscrowCreate')
return removeUndefined({
amount: parseAmount(tx.Amount).value,

View File

@@ -3,7 +3,7 @@ import {parseMemos} from './utils'
import {removeUndefined} from '../../common'
function parseEscrowExecution(tx: any): object {
assert(tx.TransactionType === 'EscrowFinish')
assert.ok(tx.TransactionType === 'EscrowFinish')
return removeUndefined({
memos: parseMemos(tx),

View File

@@ -10,7 +10,7 @@ import {
const flags = txFlags.OfferCreate
function parseOrder(tx: OfferCreateTransaction): FormattedOrderSpecification {
assert(tx.TransactionType === 'OfferCreate')
assert.ok(tx.TransactionType === 'OfferCreate')
const direction = (tx.Flags & flags.Sell) === 0 ? 'buy' : 'sell'
const takerGetsAmount = parseAmount(tx.TakerGets)

View File

@@ -4,7 +4,7 @@ import parseAmount from './amount'
const claimFlags = txFlags.PaymentChannelClaim
function parsePaymentChannelClaim(tx: any): object {
assert(tx.TransactionType === 'PaymentChannelClaim')
assert.ok(tx.TransactionType === 'PaymentChannelClaim')
return removeUndefined({
channel: tx.Channel,

View File

@@ -4,7 +4,7 @@ import {removeUndefined} from '../../common'
import parseAmount from './amount'
function parsePaymentChannelCreate(tx: any): object {
assert(tx.TransactionType === 'PaymentChannelCreate')
assert.ok(tx.TransactionType === 'PaymentChannelCreate')
return removeUndefined({
amount: parseAmount(tx.Amount).value,

View File

@@ -4,7 +4,7 @@ import {removeUndefined} from '../../common'
import parseAmount from './amount'
function parsePaymentChannelFund(tx: any): object {
assert(tx.TransactionType === 'PaymentChannelFund')
assert.ok(tx.TransactionType === 'PaymentChannelFund')
return removeUndefined({
channel: tx.Channel,

View File

@@ -19,7 +19,7 @@ function removeGenericCounterparty(amount, address) {
// Payment specification
function parsePayment(tx: any): object {
assert(tx.TransactionType === 'Payment')
assert.ok(tx.TransactionType === 'Payment')
const source = {
address: tx.Account,

View File

@@ -7,7 +7,7 @@ import parseFields from './fields'
function getAccountRootModifiedNode(tx: any) {
const modifiedNodes = tx.meta.AffectedNodes.filter(node =>
node.ModifiedNode.LedgerEntryType === 'AccountRoot')
assert(modifiedNodes.length === 1)
assert.ok(modifiedNodes.length === 1)
return modifiedNodes[0].ModifiedNode
}
@@ -51,7 +51,7 @@ function parseFlags(tx: any): any {
function parseSettings(tx: any) {
const txType = tx.TransactionType
assert(txType === 'AccountSet' || txType === 'SetRegularKey' ||
assert.ok(txType === 'AccountSet' || txType === 'SetRegularKey' ||
txType === 'SignerListSet')
return _.assign({}, parseFlags(tx), parseFields(tx))

View File

@@ -14,7 +14,7 @@ function parseFlag(flagsValue, trueValue, falseValue) {
}
function parseTrustline(tx: any): object {
assert(tx.TransactionType === 'TrustSet')
assert.ok(tx.TransactionType === 'TrustSet')
return removeUndefined({
limit: tx.LimitAmount.value,

View File

@@ -1,5 +1,5 @@
import * as _ from 'lodash'
import transactionParser = require('ripple-lib-transactionparser')
import transactionParser from 'ripple-lib-transactionparser'
import BigNumber from 'bignumber.js'
import * as common from '../../common'
import parseAmount from './amount'

View File

@@ -1,6 +1,6 @@
import * as _ from 'lodash'
import binary = require('ripple-binary-codec')
const {computeTransactionHash} = require('ripple-hashes')
import binary from 'ripple-binary-codec';
import {computeTransactionHash} from '../common/hashes'
import * as utils from './utils'
import parseTransaction from './parse/transaction'
import getTransaction from './transaction'

View File

@@ -4,7 +4,7 @@ import * as common from '../common'
import {Connection} from '../common'
import {FormattedTransactionType} from '../transaction/types'
import {Issue} from '../common/types/objects'
import {RippleAPI} from '..'
import {RippleAPI} from '..'
export type RecursiveData = {
marker: string,
@@ -14,7 +14,7 @@ export type RecursiveData = {
export type Getter = (marker?: string, limit?: number) => Promise<RecursiveData>
function clamp(value: number, min: number, max: number): number {
assert(min <= max, 'Illegal clamp bounds')
assert.ok(min <= max, 'Illegal clamp bounds')
return Math.min(Math.max(value, min), max)
}

View File

@@ -1,4 +1,4 @@
import keypairs = require('ripple-keypairs')
import keypairs from 'ripple-keypairs'
import * as common from '../common'
const {errors, validate} = common

View File

@@ -1,5 +1,5 @@
import * as _ from 'lodash'
import hashes = require('ripple-hashes')
import {computeLedgerHash, computeTransactionTreeHash, computeStateTreeHash} from '../common/hashes'
import * as common from '../common'
function convertLedgerHeader(header): any {
@@ -22,11 +22,11 @@ function convertLedgerHeader(header): any {
function hashLedgerHeader(ledgerHeader) {
const header = convertLedgerHeader(ledgerHeader)
return hashes.computeLedgerHash(header)
return computeLedgerHash(header)
}
function computeTransactionHash(ledger, version,
options: ComputeLedgerHashOptions) {
function computeTransactionHash(ledger,
options: ComputeLedgerHeaderHashOptions) {
let transactions: any[]
if (ledger.rawTransactions) {
transactions = JSON.parse(ledger.rawTransactions)
@@ -56,7 +56,7 @@ function computeTransactionHash(ledger, version,
tx.meta ? {metaData: tx.meta} : {})
return renameMeta
})
const transactionHash = hashes.computeTransactionTreeHash(txs, version)
const transactionHash = computeTransactionTreeHash(txs)
if (ledger.transactionHash !== undefined
&& ledger.transactionHash !== transactionHash) {
throw new common.errors.ValidationError('transactionHash in header'
@@ -68,8 +68,8 @@ function computeTransactionHash(ledger, version,
return transactionHash
}
function computeStateHash(ledger, version,
options: ComputeLedgerHashOptions) {
function computeStateHash(ledger,
options: ComputeLedgerHeaderHashOptions) {
if (ledger.rawState === undefined) {
if (options.computeTreeHashes) {
throw new common.errors.ValidationError('rawState'
@@ -78,7 +78,7 @@ function computeStateHash(ledger, version,
return ledger.stateHash
}
const state = JSON.parse(ledger.rawState)
const stateHash = hashes.computeStateTreeHash(state, version)
const stateHash = computeStateTreeHash(state)
if (ledger.stateHash !== undefined && ledger.stateHash !== stateHash) {
throw new common.errors.ValidationError('stateHash in header'
+ ' does not match computed hash of state')
@@ -86,20 +86,17 @@ function computeStateHash(ledger, version,
return stateHash
}
const sLCF_SHAMapV2 = 0x02
export type ComputeLedgerHashOptions = {
export type ComputeLedgerHeaderHashOptions = {
computeTreeHashes?: boolean
}
function computeLedgerHash(ledger: any,
options: ComputeLedgerHashOptions = {}): string {
const version = ((ledger.closeFlags & sLCF_SHAMapV2) === 0) ? 1 : 2
function computeLedgerHeaderHash(ledger: any,
options: ComputeLedgerHeaderHashOptions = {}): string {
const subhashes = {
transactionHash: computeTransactionHash(ledger, version, options),
stateHash: computeStateHash(ledger, version, options)
transactionHash: computeTransactionHash(ledger, options),
stateHash: computeStateHash(ledger, options)
}
return hashLedgerHeader(_.assign({}, ledger, subhashes))
}
export default computeLedgerHash
export default computeLedgerHeaderHash

View File

@@ -1,6 +1,6 @@
import * as common from '../common'
import keypairs = require('ripple-keypairs')
import binary = require('ripple-binary-codec')
import keypairs from 'ripple-keypairs'
import binary from 'ripple-binary-codec'
const {validate, xrpToDrops} = common
function signPaymentChannelClaim(channel: string, amount: string,

View File

@@ -1,5 +1,5 @@
import keypairs = require('ripple-keypairs')
import binary = require('ripple-binary-codec')
import keypairs from 'ripple-keypairs'
import binary from 'ripple-binary-codec'
import {validate, xrpToDrops} from '../common'
function verifyPaymentChannelClaim(channel: string, amount: string,

View File

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

View File

@@ -10,7 +10,7 @@ import {RippleAPI} from '..'
function setTransactionFlags(txJSON: utils.TransactionJSON, values: FormattedSettings) {
const keys = Object.keys(values)
assert(keys.length === 1, 'ERROR: can only set one setting per transaction')
assert.ok(keys.length === 1, 'ERROR: can only set one setting per transaction')
const flagName = keys[0]
const value = values[flagName]
const index = AccountFlagIndices[flagName]

View File

@@ -1,13 +1,13 @@
import * as isEqual from '../common/js/lodash.isequal'
import isEqual from 'lodash.isequal'
import * as utils from './utils'
import keypairs = require('ripple-keypairs')
import binaryCodec = require('ripple-binary-codec')
import {computeBinaryTransactionHash} from 'ripple-hashes'
import keypairs from 'ripple-keypairs'
import binaryCodec from 'ripple-binary-codec'
import {computeBinaryTransactionHash} from '../common/hashes'
import {SignOptions, KeyPair} from './types'
import {BigNumber} from 'bignumber.js'
import {xrpToDrops} from '../common'
import {RippleAPI} from '..'
const validate = utils.common.validate
const validate = utils.common.validate
function computeSignature(tx: object, privateKey: string, signAs?: string) {
const signingData = signAs
@@ -60,6 +60,72 @@ function signWithKeypair(
}
}
/**
* Compares two objects and creates a diff.
*
* @param a An object to compare.
* @param b The other object to compare with.
*
* @returns An object containing the differences between the two objects.
*/
function objectDiff(a: object, b: object): object {
const diffs = {}
// Compare two items and push non-matches to object
const compare = function (i1: any, i2: any, k: string): void {
const type1 = Object.prototype.toString.call(i1)
const type2 = Object.prototype.toString.call(i2)
if (type2 === '[object Undefined]') {
diffs[k] = null // Indicate that the item has been removed
return
}
if (type1 !== type2) {
diffs[k] = i2 // Indicate that the item has changed types
return
}
if (type1 === '[object Object]') {
const objDiff = objectDiff(i1, i2)
if (Object.keys(objDiff).length > 0) {
diffs[k] = objDiff
}
return
}
if (type1 === '[object Array]') {
if (!isEqual(i1, i2)) {
diffs[k] = i2 // If arrays do not match, add second item to diffs
}
return
}
if (type1 === '[object Function]') {
if (i1.toString() !== i2.toString()) {
diffs[k] = i2 // If functions differ, add second one to diffs
}
return
}
if (i1 !== i2) {
diffs[k] = i2
}
}
// Check items in first object
for (const key in a) {
if (a.hasOwnProperty(key)) {
compare(a[key], b[key], key)
}
}
// Get items that are in the second object but not the first
for (const key in b) {
if (b.hasOwnProperty(key)) {
if (!a[key] && a[key] !== b[key]) {
diffs[key] = b[key]
}
}
}
return diffs
}
/**
* Decode a serialized transaction, remove the fields that are added during the signing process,
* and verify that it matches the transaction prior to signing.
@@ -93,11 +159,12 @@ function checkTxSerialization(serialized: string, tx: utils.TransactionJSON): vo
if (!isEqual(decoded, tx)) {
const error = new utils.common.errors.ValidationError(
'Serialized transaction does not match original txJSON'
'Serialized transaction does not match original txJSON. See `error.data`'
)
error.data = {
decoded,
tx
tx,
diff: objectDiff(tx, decoded)
}
throw error
}

View File

@@ -70,7 +70,7 @@ function prepareTransaction(txJSON: TransactionJSON, api: RippleAPI,
// To remove the signer list, SignerEntries field should be omitted.
if (txJSON['SignerQuorum'] === 0) {
delete txJSON.SignerEntries;
delete txJSON.SignerEntries
}
const account = txJSON.Account

View File

@@ -2679,6 +2679,37 @@ describe('RippleAPI', function () {
);
});
it('sign - throws when encoded tx does not match decoded tx - prepared order', async function () {
const order = {
direction: 'sell',
quantity: {
currency: 'USD',
counterparty: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B',
value: '3.140000'
},
totalPrice: {
currency: 'XRP',
value: '31415'
}
};
const prepared = await this.api.prepareOrder('r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', order, {
sequence: 123
});
const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV';
try {
this.api.sign(prepared.txJSON, secret);
return Promise.reject(new Error('api.sign should have thrown'));
} catch (error) {
assert.equal(error.name, 'ValidationError');
assert.equal(error.message, 'Serialized transaction does not match original txJSON. See `error.data`');
assert.deepEqual(error.data.diff, {
TakerGets: {
value: '3.14'
}
});
}
});
it('sign - throws when encoded tx does not match decoded tx - AccountSet', function () {
const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV';
const request = {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

127
test/hashes-test.js Normal file
View File

@@ -0,0 +1,127 @@
/* eslint-disable max-len, valid-jsdoc */
'use strict';
var assert = require('assert');
var fs = require('fs');
var hashes = require('../src/common/hashes');
/**
* @param ledgerIndex {Number}
* Expects a corresponding ledger dump in $repo/test/fixtures/rippled folder
*/
function createLedgerTest(ledgerIndex) {
describe(String(ledgerIndex), function() {
var path = __dirname + '/fixtures/rippled/ledger-full-' + ledgerIndex + '.json';
var ledgerRaw = fs.readFileSync(path);
var ledgerJSON = JSON.parse(ledgerRaw);
var hasAccounts = Array.isArray(ledgerJSON.accountState)
&& ledgerJSON.accountState.length > 0;
if (hasAccounts) {
it('has account_hash of ' + ledgerJSON.account_hash, function() {
assert.equal(ledgerJSON.account_hash,
hashes.computeStateTreeHash(ledgerJSON.accountState, 1));
});
}
it('has transaction_hash of ' + ledgerJSON.transaction_hash, function() {
assert.equal(ledgerJSON.transaction_hash,
hashes.computeTransactionTreeHash(ledgerJSON.transactions, 1));
});
});
}
describe('Ledger', function() {
// This is the first recorded ledger with a non empty transaction set
createLedgerTest(38129);
// Because, why not.
createLedgerTest(40000);
// 1311 AffectedNodes, no accounts
createLedgerTest(7501326);
describe('calcAccountRootEntryHash', function() {
it('will calculate the AccountRoot entry hash for rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', function() {
var account = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh';
var expectedEntryHash = '2B6AC232AA4C4BE41BF49D2459FA4A0347E1B543A4C92FCEE0821C0201E2E9A8';
var actualEntryHash = hashes.computeAccountHash(account);
assert.equal(actualEntryHash, expectedEntryHash);
});
});
describe('calcRippleStateEntryHash', function() {
it('will calculate the RippleState entry hash for rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh and rB5TihdPbKgMrkFqrqUC3yLdE8hhv4BdeY in USD', function() {
var account1 = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh';
var account2 = 'rB5TihdPbKgMrkFqrqUC3yLdE8hhv4BdeY';
var currency = 'USD';
var expectedEntryHash = 'C683B5BB928F025F1E860D9D69D6C554C2202DE0D45877ADB3077DA4CB9E125C';
var actualEntryHash1 = hashes.computeTrustlineHash(
account1, account2, currency);
var actualEntryHash2 = hashes.computeTrustlineHash(
account2, account1, currency);
assert.equal(actualEntryHash1, expectedEntryHash);
assert.equal(actualEntryHash2, expectedEntryHash);
});
it('will calculate the RippleState entry hash for r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV and rUAMuQTfVhbfqUDuro7zzy4jj4Wq57MPTj in UAM', function() {
var account1 = 'r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV';
var account2 = 'rUAMuQTfVhbfqUDuro7zzy4jj4Wq57MPTj';
var currency = 'UAM';
var expectedEntryHash = 'AE9ADDC584358E5847ADFC971834E471436FC3E9DE6EA1773DF49F419DC0F65E';
var actualEntryHash1 = hashes.computeTrustlineHash(
account1, account2, currency);
var actualEntryHash2 = hashes.computeTrustlineHash(
account2, account1, currency);
assert.equal(actualEntryHash1, expectedEntryHash);
assert.equal(actualEntryHash2, expectedEntryHash);
});
});
describe('calcOfferEntryHash', function() {
it('will calculate the Offer entry hash for r32UufnaCGL82HubijgJGDmdE5hac7ZvLw, sequence 137', function() {
var account = 'r32UufnaCGL82HubijgJGDmdE5hac7ZvLw';
var sequence = 137;
var expectedEntryHash = '03F0AED09DEEE74CEF85CD57A0429D6113507CF759C597BABB4ADB752F734CE3';
var actualEntryHash = hashes.computeOrderHash(account, sequence);
assert.equal(actualEntryHash, expectedEntryHash);
});
});
describe('computeSignerListHash', function() {
it('will calculate the SignerList index for r32UufnaCGL82HubijgJGDmdE5hac7ZvLw', function() {
var account = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh';
var expectedEntryHash = '778365D5180F5DF3016817D1F318527AD7410D83F8636CF48C43E8AF72AB49BF';
var actualEntryHash = hashes.computeSignerListHash(account);
assert.equal(actualEntryHash, expectedEntryHash);
});
});
describe('calcEscrowEntryHash', function() {
it('will calculate the Escrow entry hash for rDx69ebzbowuqztksVDmZXjizTd12BVr4x, sequence 84', function() {
var account = 'rDx69ebzbowuqztksVDmZXjizTd12BVr4x';
var sequence = 84;
var expectedEntryHash = '61E8E8ED53FA2CEBE192B23897071E9A75217BF5A410E9CB5B45AAB7AECA567A';
var actualEntryHash = hashes.computeEscrowHash(account, sequence);
assert.equal(actualEntryHash, expectedEntryHash);
});
});
describe('calcPaymentChannelEntryHash', function() {
it('will calculate the PaymentChannel entry hash for rDx69ebzbowuqztksVDmZXjizTd12BVr4x and rLFtVprxUEfsH54eCWKsZrEQzMDsx1wqso, sequence 82', function() {
var account = 'rDx69ebzbowuqztksVDmZXjizTd12BVr4x';
var dstAccount = 'rLFtVprxUEfsH54eCWKsZrEQzMDsx1wqso'
var sequence = 82;
var expectedEntryHash = 'E35708503B3C3143FB522D749AAFCC296E8060F0FB371A9A56FAE0B1ED127366';
var actualEntryHash = hashes.computePaymentChannelHash(account, dstAccount, sequence);
assert.equal(actualEntryHash, expectedEntryHash);
});
});
});

View File

@@ -1,202 +0,0 @@
/* eslint-disable max-nested-callbacks */
'use strict';
const assert = require('assert-diff');
const _ = require('lodash');
const jayson = require('jayson');
const RippleAPI = require('../../src').RippleAPI;
const createHTTPServer = require('../../src/http').createHTTPServer;
const {payTo, ledgerAccept} = require('./utils');
const apiFixtures = require('../fixtures');
const apiRequests = apiFixtures.requests;
const apiResponses = apiFixtures.responses;
const TIMEOUT = 20000; // how long before each test case times out
const serverUri = 'ws://127.0.0.1:6006';
const apiOptions = {
server: serverUri
};
const httpPort = 3000;
function createClient() {
return jayson.client.http({port: httpPort, hostname: 'localhost'});
}
const address = 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59';
function makePositionalParams(params) {
return params.map(value => value[_.keys(value)[0]]);
}
function makeNamedParams(params) {
return _.reduce(params, _.assign, {});
}
function random() {
return _.fill(Array(16), 0);
}
describe('http server integration tests', function() {
this.timeout(TIMEOUT);
let server = null;
let client = null;
let paymentId = null;
let newWallet = null;
function createTestInternal(testName, methodName, params, testFunc, id) {
it(testName, function() {
return new Promise((resolve, reject) => {
client.request(methodName, params, id,
(err, result) => err ? reject(err) : resolve(testFunc(result)));
});
});
}
function createTest(name, params, testFunc, id) {
createTestInternal(name + ' - positional params', name,
makePositionalParams(params), testFunc, id);
createTestInternal(name + ' - named params', name,
makeNamedParams(params), testFunc, id);
}
before(() => {
this.api = new RippleAPI({server: serverUri});
console.log('CONNECTING...');
return this.api.connect().then(() => {
console.log('CONNECTED...');
})
.then(() => ledgerAccept(this.api))
.then(() => newWallet = this.api.generateAddress())
.then(() => ledgerAccept(this.api))
.then(() => payTo(this.api, newWallet.address))
.then(paymentId_ => {
paymentId = paymentId_;
});
});
beforeEach(function() {
server = createHTTPServer(apiOptions, httpPort);
return server.start().then(() => {
this.client = createClient();
client = this.client;
});
});
afterEach(function() {
return server.stop();
});
createTest(
'getLedgerVersion',
[],
result => assert(_.isNumber(result.result))
);
createTest(
'getServerInfo',
[],
result => assert(_.isNumber(result.result.validatedLedger.ledgerVersion))
);
it('getTransaction', function() {
const params = [{id: paymentId}];
return new Promise((resolve, reject) => {
client.request('getTransaction', makePositionalParams(params),
(err, result) => {
if (err) {
reject(err);
}
assert.strictEqual(result.result.id, paymentId);
const outcome = result.result.outcome;
assert.strictEqual(outcome.result, 'tesSUCCESS');
assert.strictEqual(outcome.balanceChanges
.rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh[0].value, '-4003218.000012');
resolve(result);
});
});
});
it('getTransactions', function() {
const params = [{address: newWallet.address}, {
options: {
binary: true,
limit: 1
}
}];
return new Promise((resolve, reject) => {
client.request('getTransactions', makeNamedParams(params),
(err, result) => {
if (err) {
reject(err);
}
assert.strictEqual(result.result.length, 1);
assert.strictEqual(result.result[0].id, paymentId);
resolve(result);
});
});
});
createTest(
'prepareSettings',
[
{address},
{settings: apiRequests.prepareSettings.domain},
{instructions: {
maxFee: '0.000012',
sequence: 23,
maxLedgerVersion: 8820051
}}
],
result => {
const got = JSON.parse(result.result.txJSON);
const expected = JSON.parse(apiResponses.prepareSettings.flags.txJSON);
assert.deepEqual(got, expected);
}
);
createTest(
'sign',
[{txJSON: apiRequests.sign.normal.txJSON},
{secret: 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV'}],
result => assert.deepEqual(result.result, apiResponses.sign.normal)
);
createTest(
'sign',
[{txJSON: apiRequests.sign.normal.txJSON},
{keypair: {
privateKey: '00ACCD3309DB14D1A4FC9B1DAE608031F4408C85C73EE05E035B7DC8B25840107A',
publicKey: '02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8' }}],
result => assert.deepEqual(result.result, apiResponses.sign.normal)
);
createTest(
'generateAddress',
[{options: {entropy: random()}}],
result => assert.deepEqual(result.result, apiResponses.generateAddress)
);
createTest(
'computeLedgerHash',
[{ledger: _.assign({}, apiResponses.getLedger.full,
{parentCloseTime: apiResponses.getLedger.full.closeTime})
}],
result => {
assert.strictEqual(result.result,
'E6DB7365949BF9814D76BCC730B01818EB9136A89DB224F3F9F5AAE4569D758E');
}
);
createTest(
'isConnected',
[],
result => assert(result.result)
);
});

71
test/shamap-test.js Normal file
View File

@@ -0,0 +1,71 @@
/* eslint-disable max-len, valid-jsdoc */
'use strict';
var assert = require('assert');
var SHAMap = require('../src/common/hashes/shamap').SHAMap;
var TYPE_TRANSACTION_NO_METADATA = require('../src/common/hashes/shamap').NodeType.TRANSACTION_NO_METADATA
var HEX_ZERO = '00000000000000000000000000000000' +
'00000000000000000000000000000000';
/**
* Generates data to hash for testing
* @param {number} v int value
* @returns {string} 64 length hex string
*/
function intToVuc(v) {
var ret = '';
for (var i = 0; i < 32; i++) {
ret += '0';
ret += v.toString(16).toUpperCase();
}
return ret;
}
/**
* @param shamap {Object}
* @param keys {Array}
* @param hashes {Array}
*/
function fillShamapTest(shamap, keys, hashes) {
for (var i = 0; i < keys.length; i++) {
var data = intToVuc(i);
shamap.addItem(keys[i].toUpperCase(), data, TYPE_TRANSACTION_NO_METADATA);
assert.equal(shamap.hash, hashes[i]);
}
}
describe('SHAMap', function() {
describe('#addItem', function() {
it('will add new nodes to v1', function() {
var keys = [
'b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8',
'b92881fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8',
'b92691fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8',
'b92791fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8',
'b91891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8',
'b99891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8',
'f22891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8',
'292891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8'
];
var hashesv1 = [
'B7387CFEA0465759ADC718E8C42B52D2309D179B326E239EB5075C64B6281F7F',
'FBC195A9592A54AB44010274163CB6BA95F497EC5BA0A8831845467FB2ECE266',
'4E7D2684B65DFD48937FFB775E20175C43AF0C94066F7D5679F51AE756795B75',
'7A2F312EB203695FFD164E038E281839EEF06A1B99BFC263F3CECC6C74F93E07',
'395A6691A372387A703FB0F2C6D2C405DAF307D0817F8F0E207596462B0E3A3E',
'D044C0A696DE3169CC70AE216A1564D69DE96582865796142CE7D98A84D9DDE4',
'76DCC77C4027309B5A91AD164083264D70B77B5E43E08AEDA5EBF94361143615',
'DF4220E93ADC6F5569063A01B4DC79F8DB9553B6A3222ADE23DEA02BBE7230E5'
];
var shamapv1 = new SHAMap();
assert.equal(shamapv1.hash, HEX_ZERO);
fillShamapTest(shamapv1, keys, hashesv1);
});
});
});

View File

@@ -1,26 +1,22 @@
{
"compilerOptions": {
"pretty": true,
"lib": [
"es2017"
],
"target": "es6",
"target": "es2017",
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"declarationMap": true /* Added 2019-04-13 */,
"sourceMap": true,
"noEmitOnError": true /* Added 2019-04-13 */,
"strict": true /* Enable all strict type-checking options. */,
"strictNullChecks": false,
"noImplicitAny": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"removeComments": true,
"strict": true /* Enable all strict type-checking options. */,
"preserveConstEnums": false,
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"suppressImplicitAnyIndexErrors": false
}
}

1370
yarn.lock

File diff suppressed because it is too large Load Diff