Move ripple-rest/api into src/api, exposing RippleAPI

This commit is contained in:
Chris Clark
2015-05-22 13:57:41 -07:00
parent 16e3541a10
commit 278331cc4a
100 changed files with 5653 additions and 72 deletions

View File

@@ -0,0 +1,60 @@
'use strict';
const _ = require('lodash');
const utils = require('./utils');
const ripple = utils.common.core;
const validate = utils.common.validate;
function renameCounterpartyToIssuer(amount) {
if (amount === undefined) {
return undefined;
}
const issuer = amount.counterparty === undefined ?
amount.issuer : amount.counterparty;
const withIssuer = _.assign({}, amount, {issuer: issuer});
return _.omit(withIssuer, 'counterparty');
}
function renameCounterpartyToIssuerInOrder(order) {
const taker_gets = renameCounterpartyToIssuer(order.taker_gets);
const taker_pays = renameCounterpartyToIssuer(order.taker_pays);
const changes = {taker_gets: taker_gets, taker_pays: taker_pays};
return _.assign({}, order, _.omit(changes, _.isUndefined));
}
const OfferCreateFlags = {
Passive: {name: 'passive', set: 'Passive'},
ImmediateOrCancel: {name: 'immediate_or_cancel', set: 'ImmediateOrCancel'},
FillOrKill: {name: 'fill_or_kill', set: 'FillOrKill'}
};
function createOrderTransaction(account, order) {
validate.address(account);
validate.order(order);
const _order = renameCounterpartyToIssuerInOrder(order);
const transaction = new ripple.Transaction();
const takerPays = _order.taker_pays.currency !== 'XRP'
? _order.taker_pays : utils.xrpToDrops(_order.taker_pays.value);
const takerGets = _order.taker_gets.currency !== 'XRP'
? _order.taker_gets : utils.xrpToDrops(_order.taker_gets.value);
transaction.offerCreate(account, ripple.Amount.from_json(takerPays),
ripple.Amount.from_json(takerGets));
utils.setTransactionBitFlags(transaction, {
input: _order,
flags: OfferCreateFlags
});
if (_order.type === 'sell') {
transaction.setFlags('Sell');
}
return transaction;
}
function prepareOrder(account, order, instructions, callback) {
const transaction = createOrderTransaction(account, order);
utils.createTxJSON(transaction, this.remote, instructions, callback);
}
module.exports = utils.wrapCatch(prepareOrder);

View File

@@ -0,0 +1,20 @@
'use strict';
const utils = require('./utils');
const validate = utils.common.validate;
const ripple = utils.common.core;
function createOrderCancellationTransaction(account, sequence) {
validate.address(account);
validate.sequence(sequence);
const transaction = new ripple.Transaction();
transaction.offerCancel(account, sequence);
return transaction;
}
function prepareOrderCancellation(account, sequence, instructions, callback) {
const transaction = createOrderCancellationTransaction(account, sequence);
utils.createTxJSON(transaction, this.remote, instructions, callback);
}
module.exports = utils.wrapCatch(prepareOrderCancellation);

View File

@@ -0,0 +1,125 @@
/* eslint-disable valid-jsdoc */
'use strict';
const BigNumber = require('bignumber.js');
const utils = require('./utils');
const ripple = utils.common.core;
const validate = utils.common.validate;
const convertAmount = utils.common.convertAmount;
function isSendMaxAllowed(payment) {
const srcAmt = payment.source_amount;
const dstAmt = payment.destination_amount;
// Don't set SendMax for XRP->XRP payment
// temREDUNDANT_SEND_MAX removed in:
// https://github.com/ripple/rippled/commit/
// c522ffa6db2648f1d8a987843e7feabf1a0b7de8/
return srcAmt && !(srcAmt.currency === 'XRP' && dstAmt.currency === 'XRP');
}
function createPaymentTransaction(account, payment) {
validate.address(account);
validate.payment(payment);
// Convert blank issuer to sender's address
// (Ripple convention for 'any issuer')
// https://ripple.com/build/transactions/
// #special-issuer-values-for-sendmax-and-amount
// https://ripple.com/build/ripple-rest/#counterparties-in-payments
if (payment.source_amount && payment.source_amount.currency !== 'XRP'
&& payment.source_amount.issuer === '') {
payment.source_amount.issuer = payment.source_account;
}
// Convert blank issuer to destinations's address
// (Ripple convention for 'any issuer')
// https://ripple.com/build/transactions/
// #special-issuer-values-for-sendmax-and-amount
// https://ripple.com/build/ripple-rest/#counterparties-in-payments
if (payment.destination_amount
&& payment.destination_amount.currency !== 'XRP'
&& payment.destination_amount.issuer === '') {
payment.destination_amount.issuer = payment.destination_account;
}
// Uppercase currency codes
if (payment.source_amount) {
payment.source_amount.currency =
payment.source_amount.currency.toUpperCase();
}
if (payment.destination_amount) {
payment.destination_amount.currency =
payment.destination_amount.currency.toUpperCase();
}
/* Construct payment */
const transaction = new ripple.Transaction();
const transactionData = {
from: payment.source_account,
to: payment.destination_account,
amount: convertAmount(payment.destination_amount)
};
// invoice_id Because transactionData is a object, transaction.payment
// function is ignored invoiceID
if (payment.invoice_id) {
transaction.invoiceID(payment.invoice_id);
}
transaction.payment(transactionData);
// Tags
if (payment.source_tag) {
transaction.sourceTag(parseInt(payment.source_tag, 10));
}
if (payment.destination_tag) {
transaction.destinationTag(parseInt(payment.destination_tag, 10));
}
// SendMax
if (isSendMaxAllowed(payment)) {
const max_value = new BigNumber(payment.source_amount.value)
.plus(payment.source_slippage || 0).toString();
if (payment.source_amount.currency === 'XRP') {
transaction.sendMax(utils.xrpToDrops(max_value));
} else {
transaction.sendMax({
value: max_value,
currency: payment.source_amount.currency,
issuer: payment.source_amount.issuer
});
}
}
// Paths
if (typeof payment.paths === 'string') {
transaction.paths(JSON.parse(payment.paths));
} else if (typeof payment.paths === 'object') {
transaction.paths(payment.paths);
}
// Memos
if (payment.memos && Array.isArray(payment.memos)) {
for (let m = 0; m < payment.memos.length; m++) {
const memo = payment.memos[m];
transaction.addMemo(memo.MemoType, memo.MemoFormat, memo.MemoData);
}
}
// Flags
let flags = [];
if (payment.partial_payment) {
flags.push('PartialPayment');
}
if (payment.no_direct_ripple) {
flags.push('NoRippleDirect');
}
if (flags.length > 0) {
transaction.setFlags(flags);
}
return transaction;
}
function preparePayment(account, payment, instructions, callback) {
const transaction = createPaymentTransaction(account, payment);
utils.createTxJSON(transaction, this.remote, instructions, callback);
}
module.exports = utils.wrapCatch(preparePayment);

View File

@@ -0,0 +1,159 @@
/* eslint-disable valid-jsdoc */
'use strict';
const _ = require('lodash');
const assert = require('assert');
const utils = require('./utils');
const ripple = utils.common.core;
const validate = utils.common.validate;
const constants = utils.common.constants;
const InvalidRequestError = utils.common.errors.InvalidRequestError;
// Emptry string passed to setting will clear it
const CLEAR_SETTING = '';
/**
* Pad the value of a fixed-length field
*
* @param {String} value
* @param {Number} length
* @return {String}
*/
function padValue(value, length) {
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof length, 'number');
let result = value;
while (result.length < length) {
result = '0' + result;
}
return result;
}
/**
* Set integer flags on a transaction based on input and a flag map
*
* @param {Transaction} transaction
* @param {Object} input - Object whose properties determine whether
* to update the transaction's SetFlag or ClearFlag property
* @param {Object} flags - Object that maps property names to transaction
* integer flag values
*
* @returns undefined
*/
function setTransactionIntFlags(transaction, input, flags) {
for (let flagName in flags) {
const flag = flags[flagName];
if (!input.hasOwnProperty(flag.name)) {
continue;
}
const value = input[flag.name];
if (value) {
transaction.tx_json.SetFlag = flag.value;
} else {
transaction.tx_json.ClearFlag = flag.value;
}
}
}
/**
* Set fields on a transaction based on input and fields schema object
*
* @param {Transaction} transaction
* @param {Object} input - Object whose properties are used to set fields on
* the transaction
* @param {Object} fieldSchema - Object that holds the schema of each field
*
* @returns undefined
*/
function setTransactionFields(transaction, input, fieldSchema) {
for (let fieldName in fieldSchema) {
const field = fieldSchema[fieldName];
let value = input[field.name];
if (typeof value === 'undefined') {
continue;
}
// The value required to clear an account root field varies
if (value === CLEAR_SETTING && field.hasOwnProperty('defaults')) {
value = field.defaults;
}
if (field.encoding === 'hex') {
// If the field is supposed to be hex, why don't we do a
// toString('hex') on it?
if (field.length) {
// Field is fixed length, why are we checking here though?
// We could move this to validateInputs
if (value.length > field.length) {
throw new InvalidRequestError(
'Parameter length exceeded: ' + fieldName);
} else if (value.length < field.length) {
value = padValue(value, field.length);
}
} else {
// Field is variable length. Expecting an ascii string as input.
// This is currently only used for Domain field
value = new Buffer(value, 'ascii').toString('hex');
}
value = value.toUpperCase();
}
transaction.tx_json[fieldName] = value;
}
}
/**
* Convert a numerical transfer rate in ripple-rest format to ripple-lib
*
* Note: A fee of 1% requires 101% of the destination to be sent for the
* destination to receive 100%.
* The transfer rate is specified as the input amount as fraction of 1.
* To specify the default rate of 0%, a 100% input amount, specify 1.
* To specify a rate of 1%, a 101% input amount, specify 1.01
*
* @param {Number|String} transferRate
*
* @returns {Number|String} numbers will be converted while strings
* are returned
*/
function convertTransferRate(transferRate) {
if (_.isNumber(transferRate)) {
return transferRate * Math.pow(10, 9);
}
return transferRate;
}
function createSettingsTransaction(account, settings) {
validate.address(account);
validate.settings(settings);
const transaction = new ripple.Transaction();
transaction.accountSet(account);
utils.setTransactionBitFlags(transaction, {
input: settings,
flags: constants.AccountSetFlags,
clear_setting: CLEAR_SETTING
});
setTransactionIntFlags(transaction, settings, constants.AccountSetIntFlags);
setTransactionFields(transaction, settings, constants.AccountRootFields);
transaction.tx_json.TransferRate = convertTransferRate(
transaction.tx_json.TransferRate);
return transaction;
}
function prepareSettings(account, settings, instructions, callback) {
const transaction = createSettingsTransaction(account, settings);
utils.createTxJSON(transaction, this.remote, instructions, callback);
}
module.exports = utils.wrapCatch(prepareSettings);

View File

@@ -0,0 +1,65 @@
'use strict';
const utils = require('./utils');
const ripple = utils.common.core;
const validate = utils.common.validate;
/**
* 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".
*/
const HASH_TX_ID = 0x54584E00; // 'TXN'
const HASH_TX_SIGN = 0x53545800; // 'STX'
const HASH_TX_SIGN_TESTNET = 0x73747800; // 'stx'
function getKeyPair(address, secret) {
return ripple.Seed.from_json(secret).get_key(address);
}
function getPublicKeyHex(keypair) {
return keypair.to_hex_pub();
}
function serialize(txJSON) {
return ripple.SerializedObject.from_json(txJSON);
}
function hashSerialization(serialized, prefix) {
return serialized.hash(prefix || HASH_TX_ID).to_hex();
}
function hashJSON(txJSON, prefix) {
return hashSerialization(serialize(txJSON), prefix);
}
function signingHash(txJSON, isTestNet) {
return hashJSON(txJSON, isTestNet ? HASH_TX_SIGN_TESTNET : HASH_TX_SIGN);
}
function computeSignature(txJSON, keypair) {
const signature = keypair.sign(signingHash(txJSON));
return ripple.sjcl.codec.hex.fromBits(signature).toUpperCase();
}
function sign(txJSON, secret) {
validate.txJSON(txJSON);
validate.addressAndSecret({address: txJSON.Account, secret: secret});
const keypair = getKeyPair(txJSON.Acccount, secret);
if (txJSON.SigningPubKey === undefined) {
txJSON.SigningPubKey = getPublicKeyHex(keypair);
}
txJSON.TxnSignature = computeSignature(txJSON, keypair);
const serialized = serialize(txJSON);
return {
tx_blob: serialized.to_hex(),
hash: hashSerialization(serialized, HASH_TX_ID)
};
}
module.exports = sign;

View File

@@ -0,0 +1,13 @@
'use strict';
const utils = require('./utils');
const ripple = utils.common.core;
const validate = utils.common.validate;
function submit(tx_blob, callback) {
validate.blob(tx_blob);
const request = new ripple.Request(this.remote, 'submit');
request.message.tx_blob = tx_blob;
request.request(callback);
}
module.exports = submit;

View File

@@ -0,0 +1,51 @@
'use strict';
const utils = require('./utils');
const ripple = utils.common.core;
const validate = utils.common.validate;
const TrustSetFlags = {
SetAuth: {name: 'authorized', set: 'SetAuth'},
ClearNoRipple: {name: 'account_allows_rippling', set: 'ClearNoRipple',
unset: 'NoRipple'},
SetFreeze: {name: 'account_trustline_frozen', set: 'SetFreeze',
unset: 'ClearFreeze'}
};
function createTrustLineTransaction(account, trustline) {
validate.address(account);
validate.trustline(trustline);
if (trustline && trustline.limit) {
trustline.limit = String(trustline.limit);
}
const transaction = new ripple.Transaction();
const limit = [
trustline.limit,
trustline.currency,
trustline.counterparty
].join('/');
transaction.trustSet(account, limit);
if (typeof trustline.quality_in === 'number') {
transaction.tx_json.QualityIn = trustline.quality_in;
}
if (typeof trustline.quality_out === 'number') {
transaction.tx_json.QualityOut = trustline.quality_out;
}
utils.setTransactionBitFlags(transaction, {
input: trustline,
flags: TrustSetFlags,
clear_setting: ''
});
return transaction;
}
function prepareTrustLine(account, trustline, instructions, callback) {
const transaction = createTrustLineTransaction(account, trustline);
utils.createTxJSON(transaction, this.remote, instructions, callback);
}
module.exports = utils.wrapCatch(prepareTrustLine);

View File

@@ -0,0 +1,104 @@
/* eslint-disable valid-jsdoc */
'use strict';
const BigNumber = require('bignumber.js');
const common = require('../common');
/**
* Helper that sets bit flags on transactions
*
* @param {Transaction} transaction - Transaction object that is used to submit
* requests to ripple
* @param {Object} options
* @param {Object} options.flags - Holds flag names to set on transaction when
* parameter values are true or false on input
* @param {Object} options.input - Holds parameter values
* @param {String} options.clear_setting - Used to check if parameter values
* besides false mean false
*
*
* @returns undefined
*/
function setTransactionBitFlags(transaction, options) {
for (let flagName in options.flags) {
const flag = options.flags[flagName];
// Set transaction flags
if (!(flag.name in options.input)) {
continue;
}
let value = options.input[flag.name];
if (value === options.clear_setting) {
value = false;
}
if (flag.unset) {
transaction.setFlags(value ? flag.set : flag.unset);
} else if (flag.set && value) {
transaction.setFlags(flag.set);
}
}
}
function getFeeDrops(remote) {
const feeUnits = 10; // all transactions currently have a fee of 10 fee units
return remote.feeTx(feeUnits).to_text();
}
function createTxJSON(transaction, remote, instructions, callback) {
common.validate.options(instructions);
transaction.complete();
const account = transaction.getAccount();
const tx_json = transaction.tx_json;
if (instructions.last_ledger_sequence !== undefined) {
tx_json.LastLedgerSequence =
parseInt(instructions.last_ledger_sequence, 10);
} else {
const offset = instructions.last_ledger_offset !== undefined ?
parseInt(instructions.last_ledger_offset, 10) : 3;
tx_json.LastLedgerSequence = remote.getLedgerSequence() + offset;
}
if (instructions.fixed_fee !== undefined) {
tx_json.Fee = common.xrpToDrops(instructions.fixed_fee);
} else {
const serverFeeDrops = getFeeDrops(remote);
if (instructions.max_fee !== undefined) {
const maxFeeDrops = common.xrpToDrops(instructions.max_fee);
tx_json.Fee = BigNumber.min(serverFeeDrops, maxFeeDrops).toString();
} else {
tx_json.Fee = serverFeeDrops;
}
}
if (instructions.sequence !== undefined) {
tx_json.Sequence = parseInt(instructions.sequence, 10);
callback(null, {tx_json: tx_json});
} else {
remote.findAccount(account).getNextSequence(function(error, sequence) {
tx_json.Sequence = sequence;
callback(null, {tx_json: tx_json});
});
}
}
function wrapCatch(asyncFunction) {
return function() {
const callback = arguments[arguments.length - 1];
try {
asyncFunction.apply(this, arguments);
} catch (error) {
callback(error);
}
};
}
module.exports = {
setTransactionBitFlags: setTransactionBitFlags,
createTxJSON: createTxJSON,
wrapCatch: wrapCatch,
common: common
};