commit
cf7d1cd55f
|
@ -514,6 +514,29 @@ ExpressApp.prototype.start = function(opts, cb) {
|
|||
});
|
||||
});
|
||||
|
||||
router.put('/v1/txnotes/:txid/', function(req, res) {
|
||||
req.body.txid = req.params['txid'];
|
||||
getServerWithAuth(req, res, function(server) {
|
||||
server.editTxNote(req.body, function(err) {
|
||||
if (err) return returnError(err, res, req);
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/v1/txnotes/', function(req, res) {
|
||||
getServerWithAuth(req, res, function(server) {
|
||||
var opts = {};
|
||||
if (_.isNumber(+req.query.minTs)) {
|
||||
opts.minTs = +req.query.minTs;
|
||||
}
|
||||
server.getTxNotes(opts, function(err, notes) {
|
||||
if (err) return returnError(err, res, req);
|
||||
res.json(notes);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/v1/fiatrates/:code/', function(req, res) {
|
||||
getServerWithAuth(req, res, function(server) {
|
||||
var opts = {
|
||||
|
|
|
@ -8,5 +8,6 @@ Model.Address = require('./address');
|
|||
Model.Notification = require('./notification');
|
||||
Model.Preferences = require('./preferences');
|
||||
Model.Email = require('./email');
|
||||
Model.TxNote = require('./txnote');
|
||||
|
||||
module.exports = Model;
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
var _ = require('lodash');
|
||||
var Uuid = require('uuid');
|
||||
|
||||
function TxNote() {};
|
||||
|
||||
TxNote.create = function(opts) {
|
||||
opts = opts || {};
|
||||
|
||||
var now = Math.floor(Date.now() / 1000);
|
||||
|
||||
var x = new TxNote();
|
||||
|
||||
x.version = 1;
|
||||
x.createdOn = now;
|
||||
x.walletId = opts.walletId;
|
||||
x.txid = opts.txid;
|
||||
x.body = opts.body;
|
||||
x.editedOn = now;
|
||||
x.editedBy = opts.copayerId;
|
||||
|
||||
return x;
|
||||
};
|
||||
|
||||
TxNote.fromObj = function(obj) {
|
||||
var x = new TxNote();
|
||||
|
||||
x.version = obj.version;
|
||||
x.createdOn = obj.createdOn;
|
||||
x.walletId = obj.walletId;
|
||||
x.txid = obj.txid;
|
||||
x.body = obj.body;
|
||||
x.editedOn = obj.editedOn;
|
||||
x.editedBy = obj.editedBy;
|
||||
|
||||
return x;
|
||||
};
|
||||
|
||||
TxNote.prototype.edit = function(body, copayerId) {
|
||||
this.body = body;
|
||||
this.editedBy = copayerId;
|
||||
this.editedOn = Math.floor(Date.now() / 1000);
|
||||
};
|
||||
|
||||
TxNote.prototype.toObject = function() {
|
||||
return this;
|
||||
};
|
||||
|
||||
module.exports = TxNote;
|
|
@ -2043,10 +2043,72 @@ WalletService.prototype.getTx = function(opts, cb) {
|
|||
self.storage.fetchTx(self.walletId, opts.txProposalId, function(err, txp) {
|
||||
if (err) return cb(err);
|
||||
if (!txp) return cb(Errors.TX_NOT_FOUND);
|
||||
return cb(null, txp);
|
||||
|
||||
if (!txp.txid) return cb(null, txp);
|
||||
|
||||
self.storage.fetchTxNote(self.walletId, txp.txid, function(err, note) {
|
||||
if (err) {
|
||||
log.warn('Error fetching tx note for ' + txp.txid);
|
||||
}
|
||||
txp.note = note;
|
||||
return cb(null, txp);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Edit note associated to a txid.
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.txid - The txid of the tx on the blockchain.
|
||||
* @param {string} opts.body - The contents of the note.
|
||||
*/
|
||||
WalletService.prototype.editTxNote = function(opts, cb) {
|
||||
var self = this;
|
||||
|
||||
if (!checkRequired(opts, 'txid', cb)) return;
|
||||
|
||||
self._runLocked(cb, function(cb) {
|
||||
self.storage.fetchTxNote(self.walletId, opts.txid, function(err, note) {
|
||||
if (err) return cb(err);
|
||||
|
||||
if (!note) {
|
||||
note = Model.TxNote.create({
|
||||
walletId: self.walletId,
|
||||
txid: opts.txid,
|
||||
copayerId: self.copayerId,
|
||||
body: opts.body,
|
||||
});
|
||||
} else {
|
||||
note.edit(opts.body, self.copayerId);
|
||||
}
|
||||
self.storage.storeTxNote(note, cb);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get tx notes.
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.txid - The txid associated with the note.
|
||||
*/
|
||||
WalletService.prototype.getTxNote = function(opts, cb) {
|
||||
var self = this;
|
||||
|
||||
if (!checkRequired(opts, 'txid', cb)) return;
|
||||
self.storage.fetchTxNote(self.walletId, opts.txid, cb);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get tx notes.
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.minTs[=0] - The start date used to filter notes.
|
||||
*/
|
||||
WalletService.prototype.getTxNotes = function(opts, cb) {
|
||||
var self = this;
|
||||
|
||||
opts = opts || {};
|
||||
self.storage.fetchTxNotes(self.walletId, opts, cb);
|
||||
};
|
||||
|
||||
/**
|
||||
* removeWallet
|
||||
|
@ -2502,10 +2564,11 @@ WalletService.prototype.getTxHistory = function(opts, cb) {
|
|||
if (opts.limit > Defaults.HISTORY_LIMIT)
|
||||
return cb(Errors.HISTORY_LIMIT_EXCEEDED);
|
||||
|
||||
function decorate(txs, addresses, proposals) {
|
||||
function decorate(txs, addresses, proposals, notes) {
|
||||
|
||||
var indexedAddresses = _.indexBy(addresses, 'address');
|
||||
var indexedProposals = _.indexBy(proposals, 'txid');
|
||||
var indexedNotes = _.indexBy(notes, 'txid');
|
||||
|
||||
function sum(items, isMine, isChange) {
|
||||
var filter = {};
|
||||
|
@ -2619,6 +2682,11 @@ WalletService.prototype.getTxHistory = function(opts, cb) {
|
|||
//newTx.paymentAckMemo = proposal.paymentAckMemo;
|
||||
}
|
||||
|
||||
var note = indexedNotes[tx.txid];
|
||||
if (note) {
|
||||
newTx.note = _.pick(note, ['body', 'editedBy', 'editedByName', 'editedOn']);
|
||||
}
|
||||
|
||||
return newTx;
|
||||
});
|
||||
};
|
||||
|
@ -2635,10 +2703,7 @@ WalletService.prototype.getTxHistory = function(opts, cb) {
|
|||
async.parallel([
|
||||
|
||||
function(next) {
|
||||
self.storage.fetchTxs(self.walletId, {}, function(err, txps) {
|
||||
if (err) return next(err);
|
||||
next(null, txps);
|
||||
});
|
||||
self.storage.fetchTxs(self.walletId, {}, next);
|
||||
},
|
||||
function(next) {
|
||||
var from = opts.skip || 0;
|
||||
|
@ -2648,13 +2713,17 @@ WalletService.prototype.getTxHistory = function(opts, cb) {
|
|||
next(null, self._normalizeTxHistory(txs));
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
self.storage.fetchTxNotes(self.walletId, {}, next);
|
||||
},
|
||||
], function(err, res) {
|
||||
if (err) return cb(err);
|
||||
|
||||
var proposals = res[0];
|
||||
var txs = res[1];
|
||||
var notes = res[2];
|
||||
|
||||
txs = decorate(txs, addresses, proposals);
|
||||
txs = decorate(txs, addresses, proposals, notes);
|
||||
|
||||
return cb(null, txs);
|
||||
});
|
||||
|
|
|
@ -22,6 +22,7 @@ var collections = {
|
|||
EMAIL_QUEUE: 'email_queue',
|
||||
CACHE: 'cache',
|
||||
FIAT_RATES: 'fiat_rates',
|
||||
TX_NOTES: 'tx_notes',
|
||||
};
|
||||
|
||||
var Storage = function(opts) {
|
||||
|
@ -69,6 +70,11 @@ Storage.prototype._createIndexes = function() {
|
|||
this.db.collection(collections.ADDRESSES).dropIndex({
|
||||
walletId: 1
|
||||
});
|
||||
|
||||
this.db.collection(collections.TX_NOTES).dropIndex({
|
||||
walletId: 1,
|
||||
txid: 1,
|
||||
});
|
||||
};
|
||||
|
||||
Storage.prototype.connect = function(opts, cb) {
|
||||
|
@ -611,6 +617,66 @@ Storage.prototype.fetchFiatRate = function(providerName, code, ts, cb) {
|
|||
});
|
||||
};
|
||||
|
||||
Storage.prototype.fetchTxNote = function(walletId, txid, cb) {
|
||||
var self = this;
|
||||
|
||||
this.db.collection(collections.TX_NOTES).findOne({
|
||||
walletId: walletId,
|
||||
txid: txid,
|
||||
}, function(err, result) {
|
||||
if (err) return cb(err);
|
||||
if (!result || !result.body) return cb();
|
||||
return cb(null, Model.TxNote.fromObj(result));
|
||||
});
|
||||
};
|
||||
|
||||
// TODO: should be done client-side
|
||||
Storage.prototype._completeTxNotesData = function(walletId, notes, cb) {
|
||||
var notesList = [].concat(notes);
|
||||
this.fetchWallet(walletId, function(err, wallet) {
|
||||
if (err) return cb(err);
|
||||
_.each(notesList, function(note) {
|
||||
note.editedByName = wallet.getCopayer(note.editedBy).name;
|
||||
});
|
||||
return cb(null, notes);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* fetchTxNotes. Times are in UNIX EPOCH (seconds)
|
||||
*
|
||||
* @param walletId
|
||||
* @param opts.minTs
|
||||
*/
|
||||
Storage.prototype.fetchTxNotes = function(walletId, opts, cb) {
|
||||
var self = this;
|
||||
|
||||
var filter = {
|
||||
walletId: walletId,
|
||||
};
|
||||
if (_.isNumber(opts.minTs)) filter.editedOn = {
|
||||
$gte: opts.minTs
|
||||
};
|
||||
this.db.collection(collections.TX_NOTES).find(filter).toArray(function(err, result) {
|
||||
if (err) return cb(err);
|
||||
var notes = _.compact(_.map(result, function(note) {
|
||||
if (!note.body) return;
|
||||
return Model.TxNote.fromObj(note);
|
||||
}));
|
||||
return self._completeTxNotesData(walletId, notes, cb);
|
||||
});
|
||||
};
|
||||
|
||||
Storage.prototype.storeTxNote = function(txNote, cb) {
|
||||
this.db.collection(collections.TX_NOTES).update({
|
||||
txid: txNote.txid,
|
||||
walletId: txNote.walletId
|
||||
}, txNote.toObject(), {
|
||||
w: 1,
|
||||
upsert: true,
|
||||
}, cb);
|
||||
};
|
||||
|
||||
Storage.prototype._dump = function(cb, fn) {
|
||||
fn = fn || console.log;
|
||||
cb = cb || function() {};
|
||||
|
|
|
@ -269,6 +269,83 @@ describe('Wallet service', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Address derivation strategy', function() {
|
||||
var server;
|
||||
beforeEach(function() {
|
||||
server = WalletService.getInstance();
|
||||
});
|
||||
it('should use BIP44 & P2PKH for 1-of-1 wallet if supported', function(done) {
|
||||
var walletOpts = {
|
||||
name: 'my wallet',
|
||||
m: 1,
|
||||
n: 1,
|
||||
pubKey: TestData.keyPair.pub,
|
||||
};
|
||||
server.createWallet(walletOpts, function(err, wid) {
|
||||
should.not.exist(err);
|
||||
server.storage.fetchWallet(wid, function(err, wallet) {
|
||||
should.not.exist(err);
|
||||
wallet.derivationStrategy.should.equal('BIP44');
|
||||
wallet.addressType.should.equal('P2PKH');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should use BIP45 & P2SH for 1-of-1 wallet if not supported', function(done) {
|
||||
var walletOpts = {
|
||||
name: 'my wallet',
|
||||
m: 1,
|
||||
n: 1,
|
||||
pubKey: TestData.keyPair.pub,
|
||||
supportBIP44AndP2PKH: false,
|
||||
};
|
||||
server.createWallet(walletOpts, function(err, wid) {
|
||||
should.not.exist(err);
|
||||
server.storage.fetchWallet(wid, function(err, wallet) {
|
||||
should.not.exist(err);
|
||||
wallet.derivationStrategy.should.equal('BIP45');
|
||||
wallet.addressType.should.equal('P2SH');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should use BIP44 & P2SH for shared wallet if supported', function(done) {
|
||||
var walletOpts = {
|
||||
name: 'my wallet',
|
||||
m: 2,
|
||||
n: 3,
|
||||
pubKey: TestData.keyPair.pub,
|
||||
};
|
||||
server.createWallet(walletOpts, function(err, wid) {
|
||||
should.not.exist(err);
|
||||
server.storage.fetchWallet(wid, function(err, wallet) {
|
||||
should.not.exist(err);
|
||||
wallet.derivationStrategy.should.equal('BIP44');
|
||||
wallet.addressType.should.equal('P2SH');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should use BIP45 & P2SH for shared wallet if supported', function(done) {
|
||||
var walletOpts = {
|
||||
name: 'my wallet',
|
||||
m: 2,
|
||||
n: 3,
|
||||
pubKey: TestData.keyPair.pub,
|
||||
supportBIP44AndP2PKH: false,
|
||||
};
|
||||
server.createWallet(walletOpts, function(err, wid) {
|
||||
should.not.exist(err);
|
||||
server.storage.fetchWallet(wid, function(err, wallet) {
|
||||
should.not.exist(err);
|
||||
wallet.derivationStrategy.should.equal('BIP45');
|
||||
wallet.addressType.should.equal('P2SH');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#joinWallet', function() {
|
||||
|
@ -619,79 +696,133 @@ describe('Wallet service', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Address derivation strategy', function() {
|
||||
var server;
|
||||
beforeEach(function() {
|
||||
server = WalletService.getInstance();
|
||||
});
|
||||
it('should use BIP44 & P2PKH for 1-of-1 wallet if supported', function(done) {
|
||||
var walletOpts = {
|
||||
name: 'my wallet',
|
||||
m: 1,
|
||||
n: 1,
|
||||
pubKey: TestData.keyPair.pub,
|
||||
};
|
||||
server.createWallet(walletOpts, function(err, wid) {
|
||||
should.not.exist(err);
|
||||
server.storage.fetchWallet(wid, function(err, wallet) {
|
||||
should.not.exist(err);
|
||||
wallet.derivationStrategy.should.equal('BIP44');
|
||||
wallet.addressType.should.equal('P2PKH');
|
||||
done();
|
||||
describe('#removeWallet', function() {
|
||||
var server, wallet, clock;
|
||||
|
||||
beforeEach(function(done) {
|
||||
helpers.createAndJoinWallet(1, 1, function(s, w) {
|
||||
server = s;
|
||||
wallet = w;
|
||||
|
||||
helpers.stubUtxos(server, wallet, _.range(2), function() {
|
||||
var txOpts = {
|
||||
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||
amount: helpers.toSatoshi(0.1),
|
||||
};
|
||||
async.eachSeries(_.range(2), function(i, next) {
|
||||
server.createTxLegacy(txOpts, function(err, tx) {
|
||||
next();
|
||||
});
|
||||
}, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should use BIP45 & P2SH for 1-of-1 wallet if not supported', function(done) {
|
||||
var walletOpts = {
|
||||
name: 'my wallet',
|
||||
m: 1,
|
||||
n: 1,
|
||||
pubKey: TestData.keyPair.pub,
|
||||
supportBIP44AndP2PKH: false,
|
||||
};
|
||||
server.createWallet(walletOpts, function(err, wid) {
|
||||
|
||||
it('should delete a wallet', function(done) {
|
||||
server.removeWallet({}, function(err) {
|
||||
should.not.exist(err);
|
||||
server.storage.fetchWallet(wid, function(err, wallet) {
|
||||
should.not.exist(err);
|
||||
wallet.derivationStrategy.should.equal('BIP45');
|
||||
wallet.addressType.should.equal('P2SH');
|
||||
done();
|
||||
server.getWallet({}, function(err, w) {
|
||||
should.exist(err);
|
||||
err.code.should.equal('WALLET_NOT_FOUND');
|
||||
should.not.exist(w);
|
||||
async.parallel([
|
||||
|
||||
function(next) {
|
||||
server.storage.fetchAddresses(wallet.id, function(err, items) {
|
||||
items.length.should.equal(0);
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
server.storage.fetchTxs(wallet.id, {}, function(err, items) {
|
||||
items.length.should.equal(0);
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
server.storage.fetchNotifications(wallet.id, null, 0, function(err, items) {
|
||||
items.length.should.equal(0);
|
||||
next();
|
||||
});
|
||||
},
|
||||
], function(err) {
|
||||
should.not.exist(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should use BIP44 & P2SH for shared wallet if supported', function(done) {
|
||||
var walletOpts = {
|
||||
name: 'my wallet',
|
||||
m: 2,
|
||||
n: 3,
|
||||
pubKey: TestData.keyPair.pub,
|
||||
};
|
||||
server.createWallet(walletOpts, function(err, wid) {
|
||||
|
||||
// creates 2 wallet, and deletes only 1.
|
||||
it('should delete a wallet, and only that wallet', function(done) {
|
||||
var server2, wallet2;
|
||||
async.series([
|
||||
|
||||
function(next) {
|
||||
helpers.createAndJoinWallet(1, 1, {
|
||||
offset: 1
|
||||
}, function(s, w) {
|
||||
server2 = s;
|
||||
wallet2 = w;
|
||||
|
||||
helpers.stubUtxos(server2, wallet2, _.range(1, 3), function() {
|
||||
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.1, TestData.copayers[1].privKey_1H_0, {
|
||||
message: 'some message'
|
||||
});
|
||||
async.eachSeries(_.range(2), function(i, next) {
|
||||
server2.createTxLegacy(txOpts, function(err, tx) {
|
||||
should.not.exist(err);
|
||||
next(err);
|
||||
});
|
||||
}, next);
|
||||
});
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
server.removeWallet({}, next);
|
||||
},
|
||||
function(next) {
|
||||
server.getWallet({}, function(err, wallet) {
|
||||
should.exist(err);
|
||||
err.code.should.equal('WALLET_NOT_FOUND');
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
server2.getWallet({}, function(err, wallet) {
|
||||
should.not.exist(err);
|
||||
should.exist(wallet);
|
||||
wallet.id.should.equal(wallet2.id);
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
server2.getMainAddresses({}, function(err, addresses) {
|
||||
should.not.exist(err);
|
||||
should.exist(addresses);
|
||||
addresses.length.should.above(0);
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
server2.getTxs({}, function(err, txs) {
|
||||
should.not.exist(err);
|
||||
should.exist(txs);
|
||||
txs.length.should.equal(2);
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
server2.getNotifications({}, function(err, notifications) {
|
||||
should.not.exist(err);
|
||||
should.exist(notifications);
|
||||
notifications.length.should.above(0);
|
||||
next();
|
||||
});
|
||||
},
|
||||
], function(err) {
|
||||
should.not.exist(err);
|
||||
server.storage.fetchWallet(wid, function(err, wallet) {
|
||||
should.not.exist(err);
|
||||
wallet.derivationStrategy.should.equal('BIP44');
|
||||
wallet.addressType.should.equal('P2SH');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should use BIP45 & P2SH for shared wallet if supported', function(done) {
|
||||
var walletOpts = {
|
||||
name: 'my wallet',
|
||||
m: 2,
|
||||
n: 3,
|
||||
pubKey: TestData.keyPair.pub,
|
||||
supportBIP44AndP2PKH: false,
|
||||
};
|
||||
server.createWallet(walletOpts, function(err, wid) {
|
||||
should.not.exist(err);
|
||||
server.storage.fetchWallet(wid, function(err, wallet) {
|
||||
should.not.exist(err);
|
||||
wallet.derivationStrategy.should.equal('BIP45');
|
||||
wallet.addressType.should.equal('P2SH');
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1433,7 +1564,6 @@ describe('Wallet service', function() {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -3687,20 +3817,6 @@ describe('Wallet service', function() {
|
|||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#createTx backoff time', function() {
|
||||
var server, wallet, txid;
|
||||
|
||||
beforeEach(function(done) {
|
||||
helpers.createAndJoinWallet(2, 2, function(s, w) {
|
||||
server = s;
|
||||
wallet = w;
|
||||
helpers.stubUtxos(server, wallet, _.range(2, 6), function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('should ignore small utxos if fee is higher', function(done) {
|
||||
helpers.stubUtxos(server, wallet, [].concat(_.times(10, function() {
|
||||
return '30bit';
|
||||
|
@ -3736,6 +3852,294 @@ describe('Wallet service', function() {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Transaction notes', function(done) {
|
||||
var server, wallet;
|
||||
beforeEach(function(done) {
|
||||
helpers.createAndJoinWallet(1, 2, function(s, w) {
|
||||
server = s;
|
||||
wallet = w;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should edit a note for an arbitrary txid', function(done) {
|
||||
server.editTxNote({
|
||||
txid: '123',
|
||||
body: 'note body'
|
||||
}, function(err) {
|
||||
should.not.exist(err);
|
||||
server.getTxNote({
|
||||
txid: '123',
|
||||
}, function(err, note) {
|
||||
should.not.exist(err);
|
||||
should.exist(note);
|
||||
note.txid.should.equal('123');
|
||||
note.walletId.should.equal(wallet.id);
|
||||
note.body.should.equal('note body');
|
||||
note.editedBy.should.equal(server.copayerId);
|
||||
note.createdOn.should.equal(note.editedOn);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should preserve last edit', function(done) {
|
||||
var clock = sinon.useFakeTimers('Date');
|
||||
server.editTxNote({
|
||||
txid: '123',
|
||||
body: 'note body'
|
||||
}, function(err) {
|
||||
should.not.exist(err);
|
||||
server.getTxNote({
|
||||
txid: '123',
|
||||
}, function(err, note) {
|
||||
should.not.exist(err);
|
||||
should.exist(note);
|
||||
note.editedBy.should.equal(server.copayerId);
|
||||
note.createdOn.should.equal(note.editedOn);
|
||||
var creator = note.editedBy;
|
||||
helpers.getAuthServer(wallet.copayers[1].id, function(server) {
|
||||
clock.tick(60 * 1000);
|
||||
server.editTxNote({
|
||||
txid: '123',
|
||||
body: 'edited text'
|
||||
}, function(err) {
|
||||
should.not.exist(err);
|
||||
server.getTxNote({
|
||||
txid: '123',
|
||||
}, function(err, note) {
|
||||
should.not.exist(err);
|
||||
should.exist(note);
|
||||
note.editedBy.should.equal(server.copayerId);
|
||||
note.createdOn.should.be.below(note.editedOn);
|
||||
creator.should.not.equal(note.editedBy);
|
||||
clock.restore();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should edit a note for an outgoing tx and retrieve it', function(done) {
|
||||
helpers.stubUtxos(server, wallet, 2, function() {
|
||||
var txOpts = {
|
||||
outputs: [{
|
||||
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||
amount: 1e8,
|
||||
}],
|
||||
message: 'some message',
|
||||
feePerKb: 100e2,
|
||||
};
|
||||
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) {
|
||||
should.exist(txp);
|
||||
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
|
||||
server.signTx({
|
||||
txProposalId: txp.id,
|
||||
signatures: signatures,
|
||||
}, function(err, txp) {
|
||||
should.not.exist(err);
|
||||
should.exist(txp);
|
||||
should.exist(txp.txid);
|
||||
server.editTxNote({
|
||||
txid: txp.txid,
|
||||
body: 'note body'
|
||||
}, function(err) {
|
||||
should.not.exist(err);
|
||||
server.getTx({
|
||||
txProposalId: txp.id,
|
||||
}, function(err, txp) {
|
||||
should.not.exist(err);
|
||||
should.exist(txp.note);
|
||||
txp.note.txid.should.equal(txp.txid);
|
||||
txp.note.walletId.should.equal(wallet.id);
|
||||
txp.note.body.should.equal('note body');
|
||||
txp.note.editedBy.should.equal(server.copayerId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should share notes between copayers', function(done) {
|
||||
server.editTxNote({
|
||||
txid: '123',
|
||||
body: 'note body'
|
||||
}, function(err) {
|
||||
should.not.exist(err);
|
||||
server.getTxNote({
|
||||
txid: '123',
|
||||
}, function(err, note) {
|
||||
should.not.exist(err);
|
||||
should.exist(note);
|
||||
note.editedBy.should.equal(server.copayerId);
|
||||
var creator = note.editedBy;
|
||||
helpers.getAuthServer(wallet.copayers[1].id, function(server) {
|
||||
server.getTxNote({
|
||||
txid: '123',
|
||||
}, function(err, note) {
|
||||
should.not.exist(err);
|
||||
should.exist(note);
|
||||
note.body.should.equal('note body');
|
||||
note.editedBy.should.equal(creator);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should be possible to remove a note', function(done) {
|
||||
server.editTxNote({
|
||||
txid: '123',
|
||||
body: 'note body'
|
||||
}, function(err) {
|
||||
should.not.exist(err);
|
||||
server.getTxNote({
|
||||
txid: '123',
|
||||
}, function(err, note) {
|
||||
should.not.exist(err);
|
||||
should.exist(note);
|
||||
server.editTxNote({
|
||||
txid: '123',
|
||||
body: null,
|
||||
}, function(err) {
|
||||
should.not.exist(err);
|
||||
server.getTxNote({
|
||||
txid: '123',
|
||||
}, function(err, note) {
|
||||
should.not.exist(err);
|
||||
should.not.exist(note);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should include the note in tx history listing', function(done) {
|
||||
helpers.createAddresses(server, wallet, 1, 1, function(mainAddresses, changeAddress) {
|
||||
server._normalizeTxHistory = sinon.stub().returnsArg(0);
|
||||
var txs = [{
|
||||
txid: '123',
|
||||
confirmations: 1,
|
||||
fees: 100,
|
||||
time: 20,
|
||||
inputs: [{
|
||||
address: 'external',
|
||||
amount: 500,
|
||||
}],
|
||||
outputs: [{
|
||||
address: mainAddresses[0].address,
|
||||
amount: 200,
|
||||
}],
|
||||
}];
|
||||
helpers.stubHistory(txs);
|
||||
server.editTxNote({
|
||||
txid: '123',
|
||||
body: 'just some note'
|
||||
}, function(err) {
|
||||
should.not.exist(err);
|
||||
server.getTxHistory({}, function(err, txs) {
|
||||
should.not.exist(err);
|
||||
should.exist(txs);
|
||||
txs.length.should.equal(1);
|
||||
var tx = txs[0];
|
||||
should.exist(tx.note);
|
||||
tx.note.body.should.equal('just some note');
|
||||
tx.note.editedBy.should.equal(server.copayerId);
|
||||
should.exist(tx.note.editedOn);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should get all notes edited past a given date', function(done) {
|
||||
var clock = sinon.useFakeTimers('Date');
|
||||
async.series([
|
||||
|
||||
function(next) {
|
||||
server.getTxNotes({}, function(err, notes) {
|
||||
should.not.exist(err);
|
||||
notes.should.be.empty;
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
server.editTxNote({
|
||||
txid: '123',
|
||||
body: 'note body'
|
||||
}, next);
|
||||
},
|
||||
function(next) {
|
||||
server.getTxNotes({
|
||||
minTs: 0,
|
||||
}, function(err, notes) {
|
||||
should.not.exist(err);
|
||||
notes.length.should.equal(1);
|
||||
notes[0].txid.should.equal('123');
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
clock.tick(60 * 1000);
|
||||
server.editTxNote({
|
||||
txid: '456',
|
||||
body: 'another note'
|
||||
}, next);
|
||||
},
|
||||
function(next) {
|
||||
server.getTxNotes({
|
||||
minTs: 0,
|
||||
}, function(err, notes) {
|
||||
should.not.exist(err);
|
||||
notes.length.should.equal(2);
|
||||
_.difference(_.pluck(notes, 'txid'), ['123', '456']).should.be.empty;
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
server.getTxNotes({
|
||||
minTs: 50,
|
||||
}, function(err, notes) {
|
||||
should.not.exist(err);
|
||||
notes.length.should.equal(1);
|
||||
notes[0].txid.should.equal('456');
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
clock.tick(60 * 1000);
|
||||
server.editTxNote({
|
||||
txid: '123',
|
||||
body: 'an edit'
|
||||
}, next);
|
||||
},
|
||||
function(next) {
|
||||
server.getTxNotes({
|
||||
minTs: 100,
|
||||
}, function(err, notes) {
|
||||
should.not.exist(err);
|
||||
notes.length.should.equal(1);
|
||||
notes[0].txid.should.equal('123');
|
||||
notes[0].body.should.equal('an edit');
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
server.getTxNotes({}, function(err, notes) {
|
||||
should.not.exist(err);
|
||||
notes.length.should.equal(2);
|
||||
next();
|
||||
});
|
||||
},
|
||||
], function(err) {
|
||||
should.not.exist(err);
|
||||
clock.restore();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getSendMaxInfo', function() {
|
||||
|
@ -5138,137 +5542,6 @@ describe('Wallet service', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#removeWallet', function() {
|
||||
var server, wallet, clock;
|
||||
|
||||
beforeEach(function(done) {
|
||||
helpers.createAndJoinWallet(1, 1, function(s, w) {
|
||||
server = s;
|
||||
wallet = w;
|
||||
|
||||
helpers.stubUtxos(server, wallet, _.range(2), function() {
|
||||
var txOpts = {
|
||||
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||
amount: helpers.toSatoshi(0.1),
|
||||
};
|
||||
async.eachSeries(_.range(2), function(i, next) {
|
||||
server.createTxLegacy(txOpts, function(err, tx) {
|
||||
next();
|
||||
});
|
||||
}, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete a wallet', function(done) {
|
||||
server.removeWallet({}, function(err) {
|
||||
should.not.exist(err);
|
||||
server.getWallet({}, function(err, w) {
|
||||
should.exist(err);
|
||||
err.code.should.equal('WALLET_NOT_FOUND');
|
||||
should.not.exist(w);
|
||||
async.parallel([
|
||||
|
||||
function(next) {
|
||||
server.storage.fetchAddresses(wallet.id, function(err, items) {
|
||||
items.length.should.equal(0);
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
server.storage.fetchTxs(wallet.id, {}, function(err, items) {
|
||||
items.length.should.equal(0);
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
server.storage.fetchNotifications(wallet.id, null, 0, function(err, items) {
|
||||
items.length.should.equal(0);
|
||||
next();
|
||||
});
|
||||
},
|
||||
], function(err) {
|
||||
should.not.exist(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// creates 2 wallet, and deletes only 1.
|
||||
it('should delete a wallet, and only that wallet', function(done) {
|
||||
var server2, wallet2;
|
||||
async.series([
|
||||
|
||||
function(next) {
|
||||
helpers.createAndJoinWallet(1, 1, {
|
||||
offset: 1
|
||||
}, function(s, w) {
|
||||
server2 = s;
|
||||
wallet2 = w;
|
||||
|
||||
helpers.stubUtxos(server2, wallet2, _.range(1, 3), function() {
|
||||
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.1, TestData.copayers[1].privKey_1H_0, {
|
||||
message: 'some message'
|
||||
});
|
||||
async.eachSeries(_.range(2), function(i, next) {
|
||||
server2.createTxLegacy(txOpts, function(err, tx) {
|
||||
should.not.exist(err);
|
||||
next(err);
|
||||
});
|
||||
}, next);
|
||||
});
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
server.removeWallet({}, next);
|
||||
},
|
||||
function(next) {
|
||||
server.getWallet({}, function(err, wallet) {
|
||||
should.exist(err);
|
||||
err.code.should.equal('WALLET_NOT_FOUND');
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
server2.getWallet({}, function(err, wallet) {
|
||||
should.not.exist(err);
|
||||
should.exist(wallet);
|
||||
wallet.id.should.equal(wallet2.id);
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
server2.getMainAddresses({}, function(err, addresses) {
|
||||
should.not.exist(err);
|
||||
should.exist(addresses);
|
||||
addresses.length.should.above(0);
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
server2.getTxs({}, function(err, txs) {
|
||||
should.not.exist(err);
|
||||
should.exist(txs);
|
||||
txs.length.should.equal(2);
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
server2.getNotifications({}, function(err, notifications) {
|
||||
should.not.exist(err);
|
||||
should.exist(notifications);
|
||||
notifications.length.should.above(0);
|
||||
next();
|
||||
});
|
||||
},
|
||||
], function(err) {
|
||||
should.not.exist(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#removePendingTx', function() {
|
||||
var server, wallet, txp;
|
||||
beforeEach(function(done) {
|
||||
|
@ -6409,7 +6682,7 @@ describe('Wallet service', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Subscribe/unsubscribe', function() {
|
||||
describe('Push notifications', function() {
|
||||
var server, wallet;
|
||||
beforeEach(function(done) {
|
||||
helpers.createAndJoinWallet(2, 3, function(s, w) {
|
||||
|
|
Loading…
Reference in New Issue