From 5abe6fac51d1dbda28bb3ed80e2837edfd678004 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Tue, 17 May 2016 13:13:21 -0300 Subject: [PATCH] edit/retrieve notes + tests --- lib/model/txnote.js | 20 ++++++- lib/server.js | 53 ++++++++++++++++- lib/storage.js | 29 ++++++++++ test/integration/server.js | 113 +++++++++++++++++++++++++++++++++++++ 4 files changed, 211 insertions(+), 4 deletions(-) diff --git a/lib/model/txnote.js b/lib/model/txnote.js index 3bb3fec..bdf5490 100644 --- a/lib/model/txnote.js +++ b/lib/model/txnote.js @@ -6,14 +6,17 @@ 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.lastEditedOn = Math.floor(Date.now() / 1000); - x.lastEditedById = opts.lastEditedById; + x.lastEditedOn = now; + x.lastEditedBy = opts.copayerId; return x; }; @@ -22,13 +25,24 @@ 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.lastEditedOn = obj.lastEditedOn; - x.lastEditedById = obj.lastEditedById; + x.lastEditedBy = obj.lastEditedBy; return x; }; +TxNote.prototype.edit = function(body, copayerId) { + this.body = body; + this.lastEditedBy = copayerId; + this.lastEditedOn = Math.floor(Date.now() / 1000); +}; + +TxNote.prototype.toObject = function() { + return this; +}; + module.exports = TxNote; diff --git a/lib/server.js b/lib/server.js index d1b301b..42cb388 100644 --- a/lib/server.js +++ b/lib/server.js @@ -2032,10 +2032,61 @@ 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); + + 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 (!Utils.checkRequired(opts, ['txid'])) + return cb(new ClientError('Required argument missing')); + + 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 (!Utils.checkRequired(opts, ['txid'])) + return cb(new ClientError('Required argument missing')); + + self.storage.fetchTxNote(self.walletId, opts.txid, cb); +}; /** * removeWallet diff --git a/lib/storage.js b/lib/storage.js index 3011cbb..fd0bfa5 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -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,29 @@ 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)); + }); +}; + +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() {}; diff --git a/test/integration/server.js b/test/integration/server.js index a5a450f..24511df 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -3833,6 +3833,119 @@ describe('Wallet service', function() { }); }); }); + + describe.only('#editTxNote', 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.lastEditedBy.should.equal(server.copayerId); + note.createdOn.should.equal(note.lastEditedOn); + 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.lastEditedBy.should.equal(server.copayerId); + note.createdOn.should.equal(note.lastEditedOn); + var creator = note.lastEditedBy; + 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.lastEditedBy.should.equal(server.copayerId); + note.createdOn.should.be.below(note.lastEditedOn); + creator.should.not.equal(note.lastEditedBy); + 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.lastEditedBy.should.equal(server.copayerId); + done(); + }); + }); + }); + }); + }); + }); + it.skip('should share notes between copayers', function(done) {}); + it.skip('should be possible to remove a note', function(done) {}); + }); }); describe('#getSendMaxInfo', function() {