ripplelib
Version:
A JavaScript API for interacting with Ripple in Node.js and the browser
513 lines (424 loc) • 13.7 kB
JavaScript
// Routines for working with an account.
//
// You should not instantiate this class yourself, instead use Remote#account.
//
// Events:
// wallet_clean : True, iff the wallet has been updated.
// wallet_dirty : True, iff the wallet needs to be updated.
// balance: The current stamp balance.
// balance_proposed
//
// var network = require('./network.js');
'use strict';
var async = require('async');
var util = require('util');
var extend = require('extend');
var EventEmitter = require('events').EventEmitter;
var Amount = require('./amount').Amount;
var UInt160 = require('./uint160').UInt160;
var TransactionManager = require('./transactionmanager').TransactionManager;
var sjcl = require('./utils').sjcl;
var Base = require('./base').Base;
var Listener = require('./listener');
/**
* @constructor Account
* @param {Remote} remote
* @param {String} account
*/
function Account(remote, account) {
EventEmitter.call(this);
var self = this;
this._remote = remote;
this._account = UInt160.from_json(account);
this._account_id = this._account.to_json();
this._subs = 0;
// Ledger entry object
// Important: This must never be overwritten, only extend()-ed
this._entry = {};
function listenerAdded(type, listener) {
if (~Account.subscribeEvents.indexOf(type)) {
if (!self._subs && self._remote._connected) {
self._remote.request_subscribe().add_account(self._account_id).broadcast().request();
}
self._subs += 1;
}
};
this.on('newListener', listenerAdded);
function listenerRemoved(type, listener) {
if (~Account.subscribeEvents.indexOf(type)) {
self._subs -= 1;
if (!self._subs && self._remote._connected) {
self._remote.request_unsubscribe().add_account(self._account_id).broadcast().request();
}
}
};
this.on('removeListener', listenerRemoved);
function attachAccount(request) {
if (self._account.is_valid() && self._subs) {
request.add_account(self._account_id);
}
};
this._remote.on('prepare_subscribe', attachAccount);
function handleTransaction(transaction) {
if (!transaction.mmeta) {
return;
}
var changed = false;
transaction.mmeta.each(function (an) {
var isAccount = an.fields.Account === self._account_id;
var isAccountRoot = isAccount && an.entryType === 'AccountRoot';
if (isAccountRoot) {
extend(self._entry, an.fieldsNew, an.fieldsFinal);
Object.keys(an.fieldsPrev).forEach(function(field){
if (!an.fieldsFinal.hasOwnProperty(field)) delete self._entry[field];
})
changed = true;
}
if (an.entryType === 'SignerList') {
switch (an.nodeType) {
case 'CreatedNode':
case 'ModifiedNode':
if (!(self._entry.signer_lists && self._entry.signer_lists[0])) self._entry.signer_lists = [{}];
extend(self._entry.signer_lists[0], an.fieldsNew, an.fieldsFinal);
break;
case 'DeletedNode':
self._entry.signer_lists = [];
break;
}
changed = true;
}
});
if (changed) {
self.emit('entry', self._entry, transaction.ledger_index);
}
};
this.on('transaction', handleTransaction);
this._listener = new Listener(this);
this._transactionManager = new TransactionManager(this);
return this;
};
util.inherits(Account, EventEmitter);
/**
* List of events that require a remote subscription to the account.
*/
Account.subscribeEvents = ['transaction', 'entry'];
Account.prototype.getStream = function () {
return this._listener;
};
Account.prototype.toJson = function () {
return this._account.to_json();
};
/**
* Whether the AccountId is valid.
*
* Note: This does not tell you whether the account exists in the ledger.
*/
Account.prototype.isValid = function () {
return this._account.is_valid();
};
/**
* Request account info
*
* @param {Function} callback
*/
Account.prototype.getInfo = function (callback) {
return this._remote.requestAccountInfo({account: this._account_id, signer_lists: true, ledger:'validated'}, callback);
};
/**
* Retrieve the current AccountRoot entry.
*
* To keep up-to-date with changes to the AccountRoot entry, subscribe to the
* 'entry' event.
*
* @param {Function} callback
*/
Account.prototype.entry = function (callback) {
var self = this;
var callback = typeof callback === 'function' ? callback : function () {};
function accountInfo(err, info) {
if (err) {
callback(err);
} else {
self._entry = extend({}, info.account_data);
self.emit('entry', self._entry, info.ledger_index);
callback(null, info);
}
};
this.getInfo(accountInfo);
return this;
};
Account.prototype.getNextSequence = function (callback) {
var callback = typeof callback === 'function' ? callback : function () {};
function isNotFound(err) {
return err && typeof err === 'object' && typeof err.remote === 'object' && err.remote.error === 'actNotFound';
};
function accountInfo(err, info) {
if (isNotFound(err)) {
// New accounts will start out as sequence one
callback(null, 1);
} else if (err) {
callback(err);
} else {
callback(null, info.account_data.Sequence);
}
};
this.getInfo(accountInfo);
return this;
};
/**
* Retrieve this account's Ripple trust lines.
*
* To keep up-to-date with changes to the AccountRoot entry, subscribe to the
* 'lines' event. (Not yet implemented.)
*
* @param {function(err, lines)} callback Called with the result
*/
Account.prototype.lines = function (callback) {
var self = this;
var callback = typeof callback === 'function' ? callback : function () {};
function accountLines(err, res) {
if (err) {
callback(err);
} else {
res.lines = Lines;
self._lines = res.lines;
self.emit('lines', self._lines);
callback(null, res);
}
}
var Count = 0;
var Lines = [];
var opts = {
account: this._account_id,
ledger: 'validated'
}
this._remote.requestAccountLines(opts, function handle_lines (err, res) {
if (err) return accountLines(err);
Lines = Lines.concat(res.lines);
var marker = res.marker;
if (marker) {
self.emit('lines_marker', marker, ++Count);
opts.marker = marker;
opts.ledger = res.ledger_index;
self._remote.requestAccountLines(opts, handle_lines);
} else {
accountLines(null, res);
}
});
return this;
};
/**
* Retrieve this account's single trust line.
*
* @param {string} currency Currency
* @param {string} address Ripple address
* @param {function(err, line)} callback Called with the result
* @returns {Account}
*/
Account.prototype.line = function (currency, address, callback) {
var self = this;
var callback = typeof callback === 'function' ? callback : function () {};
this._remote.requestRippleBalance({
account: this._account_id,
issuer: address,
currency: currency,
ledger: 'validated'
}, function (err, res) {
if (err) return callback(err);
var line = {
account: address,
currency: currency,
balance: res.account_balance.to_text(),
limit: res.account_limit.to_text(),
limit_peer: res.peer_limit.to_text(),
quality_in: res.account_quality_in,
quality_out: res.account_quality_out,
no_ripple: res.account_no_ripple,
no_ripple_peer: res.peer_no_ripple,
freeze: res.account_freeze,
freeze_peer: res.peer_freeze,
authorized: res.account_authorized,
peer_authorized: res.peer_authorized,
}
callback(null, line);
});
return this;
};
/**
* Retrieve this account's Offers.
*
* @param {function(err, lines)} callback Called with the result
*/
Account.prototype.offers = function (callback) {
var self = this;
var callback = typeof callback === 'function' ? callback : function () {};
function accountOffers(err, res) {
if (err) {
callback(err);
} else {
res.offers = Offers;
self._offers = Offers;
self.emit('offers', Offers);
callback(null, res);
}
}
var Count = 0;
var Offers = [];
var opts = {
account: this._account_id,
ledger: 'validated'
}
this._remote.requestAccountOffers(opts, function handle_response (err, res) {
if (err) return accountOffers(err);
Offers = Offers.concat(res.offers);
var marker = res.marker;
if (marker) {
self.emit('offers_marker', marker, ++Count);
opts.marker = marker;
opts.ledger = res.ledger_index;
self._remote.requestAccountOffers(opts, handle_response);
} else {
accountOffers(null, res);
}
});
return this;
};
/**
* Retrieve offer by sequence.
*
* @param {Number} sequence
* @returns {Account}
*/
Account.prototype.offer = function (sequence, callback) {
var self = this;
var callback = typeof callback === 'function' ? callback : function () {};
var opts = {
account: this._account_id,
sequence: sequence
};
this._remote.requestOffer(opts, function (err, res){
if (err) return callback(err);
var node = res.node;
var offer = {
taker_gets: node.TakerGets,
taker_pays: node.TakerPays,
expiration: node.Expiration,
flags: node.Flags,
quality: Amount.from_json(node.TakerPays).divide(node.TakerGets).to_text()
}
callback(null, offer);
});
return this;
};
/**
* Notify object of a relevant transaction.
*
* This is only meant to be called by the Remote class. You should never have to
* call this yourself.
*
* @param {Object} message
*/
Account.prototype.notify = Account.prototype.notifyTx = function (transaction) {
// Only trigger the event if the account object is actually
// subscribed - this prevents some weird phantom events from
// occurring.
if (!this._subs) {
return;
}
this.emit('transaction', transaction);
var account = transaction.transaction.Account;
if (!account) {
return;
}
var isThisAccount = account === this._account_id;
this.emit(isThisAccount ? 'transaction-outbound' : 'transaction-inbound', transaction);
};
/**
* Submit a transaction to an account's
* transaction manager
*
* @param {Transaction} transaction
*/
Account.prototype.submit = function (transaction) {
this._transactionManager.submit(transaction);
};
/**
* Check whether the given public key is valid for this account
*
* @param {Hex-encoded String|RippleAddress} public_key
* @param {Function} callback
*
* @callback
* @param {Error} err
* @param {Boolean} true if the public key is valid and active, false otherwise
*/
Account.prototype.publicKeyIsActive = function (public_key, callback) {
var self = this;
var public_key_as_uint160;
try {
public_key_as_uint160 = Account._publicKeyToAddress(public_key);
} catch (err) {
return callback(err);
}
function getAccountInfo(async_callback) {
self.getInfo(function (err, account_info_res) {
// If the remote responds with an Account Not Found error then the account
// is unfunded and thus we can assume that the master key is active
if (err && err.remote && err.remote.error === 'actNotFound') {
async_callback(null, null);
} else {
async_callback(err, account_info_res);
}
});
};
function publicKeyIsValid(account_info_res, async_callback) {
// Catch the case of unfunded accounts
if (!account_info_res) {
if (public_key_as_uint160 === self._account_id) {
async_callback(null, true);
} else {
async_callback(null, false);
}
return;
}
var account_info = account_info_res.account_data;
// Respond with true if the RegularKey is set and matches the given public key or
// if the public key matches the account address and the lsfDisableMaster is not set
if (account_info.RegularKey && account_info.RegularKey === public_key_as_uint160) {
async_callback(null, true);
} else if (account_info.Account === public_key_as_uint160 && (account_info.Flags & 0x00100000) === 0) {
async_callback(null, true);
} else {
async_callback(null, false);
}
};
var steps = [getAccountInfo, publicKeyIsValid];
async.waterfall(steps, callback);
};
/**
* Convert a hex-encoded public key to a Ripple Address
*
* @static
*
* @param {Hex-encoded string|RippleAddress} public_key
* @returns {RippleAddress}
*/
Account._publicKeyToAddress = function (public_key) {
// Based on functions in /src/js/ripple/keypair.js
function hexToUInt160(public_key) {
var bits = sjcl.codec.hex.toBits(public_key);
var hash = sjcl.hash.ripemd160.hash(sjcl.hash.sha256.hash(bits));
var address = UInt160.from_bits(hash);
address.set_version(Base.VER_ACCOUNT_ID);
return address.to_json();
};
if (UInt160.is_valid(public_key)) {
return public_key;
} else if (/^[0-9a-fA-F]+$/.test(public_key)) {
return hexToUInt160(public_key);
} else {
throw new Error('Public key is invalid. Must be a UInt160 or a hex string');
}
};
exports.Account = Account;
// vim:sw=2:sts=2:ts=8:et