Merge pull request #504 from clark800/get-ledger

Add getLedger method, remove getLedgerHeader method
This commit is contained in:
Chris Clark
2015-08-18 10:01:22 -07:00
23 changed files with 269 additions and 99 deletions

View File

@@ -1,11 +1,11 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "getLedgerHeader",
"title": "getLedger",
"type": "object",
"properties": {
"accepted": {"type": "boolean"},
"closed": {"type": "boolean"},
"accountHash": {"$ref": "hash256"},
"stateHash": {"$ref": "hash256"},
"closeTime": {"type": "integer", "minimum": 0},
"closeTimeResolution": {"type": "integer", "minimum": 1},
"closeFlags": {"type": "integer", "minimum": 0},
@@ -14,12 +14,17 @@
"parentLedgerHash": {"$ref": "hash256"},
"parentCloseTime": {"type": "integer", "minimum": 0},
"totalDrops": {"$ref": "value"},
"transactionHash": {"$ref": "hash256"}
"transactionHash": {"$ref": "hash256"},
"transactions": {"type": "array", "items": {"type": "object"}},
"rawTransactions": {"type": "string"},
"transactionHashes": {"type": "array", "items": {"$ref": "hash256"}},
"rawState": {"type": "string"},
"stateHashes": {"type": "array", "items": {"$ref": "hash256"}}
},
"required": [
"accepted",
"closed",
"accountHash",
"stateHash",
"closeTime",
"closeTimeResolution",
"closeFlags",

View File

@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "ledger-options",
"description": "Options for getLedger",
"type": "object",
"properties": {
"ledgerVersion": {"$ref": "ledgerVersion"},
"includeAllData": {"type": "boolean"},
"includeTransactions": {"type": "boolean"},
"includeState": {"type": "boolean"}
},
"additionalProperties": false
}

View File

@@ -9,7 +9,8 @@ function error(text) {
return new ValidationError(text);
}
function validateAddressAndSecret(obj: {address: string, secret: string}): void {
function validateAddressAndSecret(obj: {address: string, secret: string}
): void {
const address = obj.address;
const secret = obj.secret;
schemaValidate('address', address);
@@ -73,6 +74,7 @@ module.exports = {
getOrdersOptions: _.partial(validateOptions, 'orders-options'),
getOrderbookOptions: _.partial(validateOptions, 'orders-options'),
getTransactionOptions: _.partial(validateOptions, 'transaction-options'),
getLedgerOptions: _.partial(validateOptions, 'ledger-options'),
options: _.partial(validateOptions, 'options'),
instructions: _.partial(schemaValidate, 'instructions')
};

View File

@@ -29,8 +29,8 @@ const submit = require('./transaction/submit');
const errors = require('./common').errors;
const convertExceptions = require('./common').convertExceptions;
const generateWallet = convertExceptions(common.generateWallet);
const getLedgerHeader = require('./ledger/ledger-header');
const computeLedgerHash = require('./offline/ledgerhash');
const getLedger = require('./ledger/ledger');
function RippleAPI(options: {}) {
const _options = _.assign({}, options, {automatic_resubmission: false});
@@ -54,7 +54,7 @@ RippleAPI.prototype = {
getOrderbook,
getSettings,
getAccountInfo,
getLedgerHeader,
getLedger,
preparePayment,
prepareTrustline,

View File

@@ -71,7 +71,7 @@ function getAccountInfoAsync(account: string, options: AccountInfoOptions,
function getAccountInfo(account: string, options: AccountInfoOptions={}
): Promise<AccountInfoResponse> {
return utils.promisify(getAccountInfoAsync.bind(this))(account, options);
return utils.promisify(getAccountInfoAsync).call(this, account, options);
}
module.exports = getAccountInfo;

View File

@@ -25,7 +25,7 @@ function formatBalances(balances) {
}
function getTrustlinesAsync(account, options, callback) {
getTrustlines.bind(this)(account, options)
getTrustlines.call(this, account, options)
.then(data => callback(null, data))
.catch(callback);
}
@@ -43,7 +43,7 @@ function getBalancesAsync(account, options, callback) {
}
function getBalances(account: string, options={}) {
return utils.promisify(getBalancesAsync.bind(this))(account, options);
return utils.promisify(getBalancesAsync).call(this, account, options);
}
module.exports = getBalances;

View File

@@ -1,42 +0,0 @@
/* @flow */
'use strict';
const utils = require('./utils');
const validate = utils.common.validate;
const composeAsync = utils.common.composeAsync;
function formatLedgerHeader(response) {
const header = response.ledger;
return {
accepted: header.accepted,
closed: header.closed,
accountHash: header.account_hash,
closeTime: header.close_time,
closeTimeResolution: header.close_time_resolution,
closeFlags: header.close_flags,
ledgerHash: header.hash || header.ledger_hash,
ledgerVersion: parseInt(header.ledger_index || header.seqNum, 10),
parentLedgerHash: header.parent_hash,
parentCloseTime: header.parent_close_time,
totalDrops: header.total_coins || header.totalCoins,
transactionHash: header.transaction_hash
};
}
function getLedgerHeaderAsync(ledgerVersion, callback) {
if (ledgerVersion) {
validate.ledgerVersion(ledgerVersion);
}
const request = {
ledger: ledgerVersion || 'validated'
};
this.remote.requestLedger(request,
composeAsync(formatLedgerHeader, callback));
}
function getLedgerHeader(ledgerVersion?: number) {
return utils.promisify(getLedgerHeaderAsync.bind(this))(ledgerVersion);
}
module.exports = getLedgerHeader;

26
src/api/ledger/ledger.js Normal file
View File

@@ -0,0 +1,26 @@
/* @flow */
'use strict';
const utils = require('./utils');
const validate = utils.common.validate;
const composeAsync = utils.common.composeAsync;
const parseLedger = require('./parse/ledger');
function getLedgerAsync(options, callback) {
validate.getLedgerOptions(options);
const request = {
ledger: options.ledgerVersion || 'validated',
expand: options.includeAllData,
transactions: options.includeTransactions,
accounts: options.includeState
};
this.remote.requestLedger(request,
composeAsync(response => parseLedger(response.ledger), callback));
}
function getLedger(options={}) {
return utils.promisify(getLedgerAsync).call(this, options);
}
module.exports = getLedger;

View File

@@ -10,7 +10,8 @@ const composeAsync = utils.common.composeAsync;
// account is to specify a "perspective", which affects which unfunded offers
// are returned
function getBookOffers(remote, account, ledgerVersion, limit,
takerGets, takerPays, callback) {
takerGets, takerPays, callback
) {
remote.requestBookOffers(utils.renameCounterpartyToIssuerInOrder({
taker_gets: takerGets,
taker_pays: takerPays,
@@ -77,7 +78,7 @@ function getOrderbookAsync(account, orderbook, options, callback) {
}
function getOrderbook(account: string, orderbook: Object, options={}) {
return utils.promisify(getOrderbookAsync.bind(this))(
return utils.promisify(getOrderbookAsync).call(this,
account, orderbook, options);
}

View File

@@ -7,7 +7,8 @@ const composeAsync = utils.common.composeAsync;
const parseAccountOrder = require('./parse/account-order');
function requestAccountOffers(remote, address, ledgerVersion, options,
marker, limit, callback) {
marker, limit, callback
) {
remote.requestAccountOffers({
account: address,
marker: marker,
@@ -34,7 +35,7 @@ function getOrdersAsync(account, options, callback) {
}
function getOrders(account: string, options={}) {
return utils.promisify(getOrdersAsync.bind(this))(account, options);
return utils.promisify(getOrdersAsync).call(this, account, options);
}
module.exports = getOrders;

View File

@@ -0,0 +1,50 @@
/* @flow */
'use strict';
const _ = require('lodash');
const removeUndefined = require('./utils').removeUndefined;
const parseTransaction = require('./transaction');
function parseTransactions(transactions) {
if (_.isEmpty(transactions)) {
return {};
}
if (_.isString(transactions[0])) {
return {transactionHashes: transactions};
}
return {
transactions: _.map(transactions, parseTransaction),
rawTransactions: JSON.stringify(transactions)
};
}
function parseState(state) {
if (_.isEmpty(state)) {
return {};
}
if (_.isString(state[0])) {
return {stateHashes: state};
}
return {rawState: JSON.stringify(state)};
}
function parseLedger(ledger: Object): Object {
return removeUndefined(_.assign({
accepted: ledger.accepted,
closed: ledger.closed,
stateHash: ledger.account_hash,
closeTime: ledger.close_time,
closeTimeResolution: ledger.close_time_resolution,
closeFlags: ledger.close_flags,
ledgerHash: ledger.hash || ledger.ledger_hash,
ledgerVersion: parseInt(ledger.ledger_index || ledger.seqNum, 10),
parentLedgerHash: ledger.parent_hash,
parentCloseTime: ledger.parent_close_time,
totalDrops: ledger.total_coins || ledger.totalCoins,
transactionHash: ledger.transaction_hash
},
parseTransactions(ledger.transactions),
parseState(ledger.accountState)
));
}
module.exports = parseLedger;

View File

@@ -114,7 +114,7 @@ function getPathsAsync(pathfind, callback) {
}
function getPaths(pathfind: Object) {
return utils.promisify(getPathsAsync.bind(this))(pathfind);
return utils.promisify(getPathsAsync).call(this, pathfind);
}
module.exports = getPaths;

View File

@@ -38,7 +38,7 @@ function getSettingsAsync(account, options, callback) {
}
function getSettings(account: string, options={}) {
return utils.promisify(getSettingsAsync.bind(this))(account, options);
return utils.promisify(getSettingsAsync).call(this, account, options);
}
module.exports = getSettings;

View File

@@ -87,7 +87,7 @@ function getTransactionAsync(identifier: string, options: TransactionOptions,
function getTransaction(identifier: string,
options: TransactionOptions={}
): Promise<GetTransactionResponse> {
return utils.promisify(getTransactionAsync.bind(this))(identifier, options);
return utils.promisify(getTransactionAsync).call(this, identifier, options);
}
module.exports = getTransaction;

View File

@@ -104,7 +104,7 @@ function getTransactionsAsync(account, options, callback) {
const defaults = {maxLedgerVersion: this.remote.getLedgerSequence()};
if (options.start) {
getTransaction.bind(this)(options.start).then(tx => {
getTransaction.call(this, options.start).then(tx => {
const ledgerVersion = tx.outcome.ledgerVersion;
const bound = options.earliestFirst ?
{minLedgerVersion: ledgerVersion} : {maxLedgerVersion: ledgerVersion};
@@ -118,7 +118,7 @@ function getTransactionsAsync(account, options, callback) {
}
function getTransactions(account: string, options={}) {
return utils.promisify(getTransactionsAsync.bind(this))(account, options);
return utils.promisify(getTransactionsAsync).call(this, account, options);
}
module.exports = getTransactions;

View File

@@ -10,7 +10,8 @@ function currencyFilter(currency, trustline) {
}
function getAccountLines(remote, address, ledgerVersion, options, marker, limit,
callback) {
callback
) {
const requestOptions = {
account: address,
ledger: ledgerVersion,
@@ -31,7 +32,8 @@ function getAccountLines(remote, address, ledgerVersion, options, marker, limit,
function getTrustlinesAsync(account: string, options: {currency: string,
counterparty: string, limit: number, ledgerVersion: number},
callback: () => void): void {
callback: () => void
): void {
validate.address(account);
validate.getTrustlinesOptions(options);
@@ -43,7 +45,7 @@ function getTrustlinesAsync(account: string, options: {currency: string,
}
function getTrustlines(account: string, options={}) {
return utils.promisify(getTrustlinesAsync.bind(this))(account, options);
return utils.promisify(getTrustlinesAsync).call(this, account, options);
}
module.exports = getTrustlines;

View File

@@ -7,7 +7,7 @@ function convertLedgerHeader(header) {
return {
accepted: header.accepted,
closed: header.closed,
account_hash: header.accountHash,
account_hash: header.stateHash,
close_time: header.closeTime,
close_time_resolution: header.closeTimeResolution,
close_flags: header.closeFlags,
@@ -28,25 +28,47 @@ function hashLedgerHeader(ledgerHeader) {
return common.core.Ledger.calculateLedgerHash(header);
}
function computeLedgerHash(ledgerHeader: Object, transactions: Array<Object>
): string {
if (transactions) {
const txs = _.map(transactions, tx => {
const mergeTx = _.assign({}, _.omit(tx, 'tx'), tx.tx || {});
const renameMeta = _.assign({}, _.omit(mergeTx, 'meta'),
tx.meta ? {metaData: tx.meta} : {});
return renameMeta;
});
const ledger = common.core.Ledger.from_json({transactions: txs});
const transactionHash = ledger.calc_tx_hash().to_hex();
if (ledgerHeader.transaction_hash !== undefined
&& ledgerHeader.transaction_hash !== transactionHash) {
throw new common.errors.ValidationError('transaction_hash in header'
+ ' does not match computed hash of transactions');
}
return hashLedgerHeader(_.assign({}, ledgerHeader, {transactionHash}));
function computeTransactionHash(ledger) {
if (ledger.rawTransactions === undefined) {
return ledger.transactionHash;
}
return hashLedgerHeader(ledgerHeader);
const transactions = JSON.parse(ledger.rawTransactions);
const txs = _.map(transactions, tx => {
const mergeTx = _.assign({}, _.omit(tx, 'tx'), tx.tx || {});
const renameMeta = _.assign({}, _.omit(mergeTx, 'meta'),
tx.meta ? {metaData: tx.meta} : {});
return renameMeta;
});
const ledgerObject = common.core.Ledger.from_json({transactions: txs});
const transactionHash = ledgerObject.calc_tx_hash().to_hex();
if (ledger.transactionHash !== undefined
&& ledger.transactionHash !== transactionHash) {
throw new common.errors.ValidationError('transactionHash in header'
+ ' does not match computed hash of transactions');
}
return transactionHash;
}
function computeStateHash(ledger) {
if (ledger.rawState === undefined) {
return ledger.stateHash;
}
const state = JSON.parse(ledger.rawState);
const ledgerObject = common.core.Ledger.from_json({accountState: state});
const stateHash = ledgerObject.calc_account_hash().to_hex();
if (ledger.stateHash !== undefined && ledger.stateHash !== stateHash) {
throw new common.errors.ValidationError('stateHash in header'
+ ' does not match computed hash of state');
}
return stateHash;
}
function computeLedgerHash(ledger: Object): string {
const hashes = {
transactionHash: computeTransactionHash(ledger),
stateHash: computeStateHash(ledger)
};
return hashLedgerHeader(_.assign({}, ledger, hashes));
}
module.exports = computeLedgerHash;

View File

@@ -34,6 +34,7 @@ function checkResult(expected, schemaName, response) {
if (schemaName) {
schemaValidator.schemaValidate(schemaName, response);
}
return response;
}
describe('RippleAPI', function() {
@@ -547,9 +548,27 @@ describe('RippleAPI', function() {
assert.strictEqual(this.api.getLedgerVersion(), 8819951);
});
it('getLedgerHeader', function() {
return this.api.getLedgerHeader().then(
_.partial(checkResult, responses.getLedgerHeader, 'getLedgerHeader'));
it('getLedger', function() {
return this.api.getLedger().then(
_.partial(checkResult, responses.getLedger.header, 'getLedger'));
});
it('getLedger - full, then computeLedgerHash', function() {
const request = {
includeTransactions: true,
includeState: true,
includeAllData: true,
ledgerVersion: 38129
};
return this.api.getLedger(request).then(
_.partial(checkResult, responses.getLedger.full, 'getLedger'))
.then(response => {
const ledger = _.assign({}, response,
{parentCloseTime: response.closeTime});
const hash = this.api.computeLedgerHash(ledger);
assert.strictEqual(hash,
'E6DB7365949BF9814D76BCC730B01818EB9136A89DB224F3F9F5AAE4569D758E');
});
});
it('ledger utils - compareTransactions', function() {
@@ -757,9 +776,10 @@ describe('RippleAPI - offline', function() {
it('computeLedgerHash - with transactions', function() {
const api = new RippleAPI();
const header = _.omit(requests.computeLedgerHash.header,
'transaction_hash');
const transactions = requests.computeLedgerHash.transactions;
const ledgerHash = api.computeLedgerHash(header, transactions);
'transactionHash');
header.rawTransactions = JSON.stringify(
requests.computeLedgerHash.transactions);
const ledgerHash = api.computeLedgerHash(header);
assert.strictEqual(ledgerHash,
'F4D865D83EB88C1A1911B9E90641919A1314F36E1B099F8E95FE3B7C77BE3349');
});
@@ -767,9 +787,10 @@ describe('RippleAPI - offline', function() {
it('computeLedgerHash - incorrent transaction_hash', function() {
const api = new RippleAPI();
const header = _.assign({}, requests.computeLedgerHash.header,
{transaction_hash:
{transactionHash:
'325EACC5271322539EEEC2D6A5292471EF1B3E72AE7180533EFC3B8F0AD435C9'});
const transactions = requests.computeLedgerHash.transactions;
assert.throws(() => api.computeLedgerHash(header, transactions));
header.rawTransactions = JSON.stringify(
requests.computeLedgerHash.transactions);
assert.throws(() => api.computeLedgerHash(header));
});
});

View File

@@ -1,6 +1,6 @@
{
"accepted": true,
"accountHash": "D9ABF622DA26EEEE48203085D4BC23B0F77DC6F8724AC33D975DA3CA492D2E44",
"stateHash": "D9ABF622DA26EEEE48203085D4BC23B0F77DC6F8724AC33D975DA3CA492D2E44",
"closeTime": 492656470,
"parentCloseTime": 492656460,
"closeFlags": 0,

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,12 @@
{
"accepted": true,
"closed": true,
"accountHash": "EC028EC32896D537ECCA18D18BEBE6AE99709FEFF9EF72DBD3A7819E918D8B96",
"stateHash": "EC028EC32896D537ECCA18D18BEBE6AE99709FEFF9EF72DBD3A7819E918D8B96",
"closeTime": 464908910,
"closeTimeResolution": 10,
"closeFlags": 0,
"ledgerHash": "0F7ED9F40742D8A513AE86029462B7A6768325583DF8EE21B7EC663019DD6A0F",
"ledgerVersion": "9038214",
"ledgerVersion": 9038214,
"parentLedgerHash": "4BB9CBE44C39DC67A1BE849C7467FE1A6D1F73949EA163C38A0121A15E04FFDE",
"parentCloseTime": 464908900,
"totalDrops": "99999973964317514",

View File

@@ -27,7 +27,10 @@ module.exports = {
},
getTransactions: require('./get-transactions.json'),
getTrustlines: require('./get-trustlines.json'),
getLedgerHeader: require('./get-ledger-header'),
getLedger: {
header: require('./get-ledger'),
full: require('./get-ledger-full')
},
prepareOrderCancellation: require('./prepare-order-cancellation.json'),
prepareOrder: require('./prepare-order.json'),
prepareOrderSell: require('./prepare-order-sell.json'),

View File

@@ -8,6 +8,7 @@ const addresses = require('./fixtures/addresses');
const hashes = require('./fixtures/hashes');
const transactionsResponse = require('./fixtures/api/rippled/account-tx');
const accountLinesResponse = require('./fixtures/api/rippled/account-lines');
const fullLedger = require('./fixtures/ledger-full-38129.json');
function isUSD(json) {
return json === 'USD' || json === '0000000000000000000000005553440000000000';
@@ -24,6 +25,27 @@ function createResponse(request, response, overrides={}) {
return JSON.stringify(_.assign({}, response, change));
}
function createLedgerResponse(request, response) {
const newResponse = JSON.parse(createResponse(request, response));
if (newResponse.result && newResponse.result.ledger) {
if (!request.transactions) {
delete newResponse.result.ledger.transactions;
}
if (!request.accounts) {
delete newResponse.result.ledger.accountState;
}
// the following fields were not in the ledger response in the past
if (newResponse.result.ledger.close_flags === undefined) {
newResponse.result.ledger.close_flags = 0;
}
if (newResponse.result.ledger.parent_close_time === undefined) {
newResponse.result.ledger.parent_close_time =
newResponse.result.ledger.close_time - 10;
}
}
return JSON.stringify(newResponse);
}
module.exports = function(port) {
const mock = new WebSocketServer({port: port});
_.assign(mock, EventEmitter2.prototype);
@@ -119,11 +141,15 @@ module.exports = function(port) {
mock.on('request_ledger', function(request, conn) {
assert.strictEqual(request.command, 'ledger');
if (request.ledger_index === 34) {
conn.send(createResponse(request, fixtures.ledgerNotFound));
conn.send(createLedgerResponse(request, fixtures.ledgerNotFound));
} else if (request.ledger_index === 9038215) {
conn.send(createResponse(request, fixtures.ledgerWithoutCloseTime));
conn.send(createLedgerResponse(request, fixtures.ledgerWithoutCloseTime));
} else if (request.ledger_index === 38129) {
const response = _.assign({}, fixtures.ledger,
{result: {ledger: fullLedger}});
conn.send(createLedgerResponse(request, response));
} else {
conn.send(createResponse(request, fixtures.ledger));
conn.send(createLedgerResponse(request, fixtures.ledger));
}
});