UNPKG

machinomy

Version:

Micropayments powered by Ethereum

446 lines 24.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const sinon = require("sinon"); const BigNumber = require("bignumber.js"); const PaymentChannel_1 = require("./PaymentChannel"); const payment_1 = require("./payment"); const Web3 = require("web3"); const expects_rejection_1 = require("./util/expects_rejection"); const ChannelContract_1 = require("./ChannelContract"); const uuid = require("uuid"); const contracts_1 = require("@machinomy/contracts"); const ChannelManager_1 = require("./ChannelManager"); const expect = require("expect"); const Signature_1 = require("./Signature"); describe('ChannelManager', () => { const fakeChan = new PaymentChannel_1.PaymentChannel('0xcafe', '0xbeef', '123', new BigNumber.BigNumber(10), new BigNumber.BigNumber(0), 0, '', ChannelManager_1.default.DEFAULT_SETTLEMENT_PERIOD); const fakeLog = { logs: [{ args: { channelId: '123' } }] }; let web3; let channelsDao; let paymentsDao; let tokensDao; let channelContract; let channelManager; let paymentManager; let deployed; let contractStub; let uuidStub; let machOpts; beforeEach(() => { const pseudoWeb3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')); web3 = { currentProvider: {}, eth: { getBlock: pseudoWeb3.eth.getBlock } }; deployed = {}; uuidStub = sinon.stub(uuid, 'v4').returns('0e29e61f-256b-40b2-a628-0f8181a1b5ff'); contractStub = sinon.stub(contracts_1.Unidirectional, 'contract'); contractStub.withArgs(web3.currentProvider).returns({ deployed: sinon.stub().resolves(deployed) }); paymentsDao = {}; tokensDao = {}; channelsDao = {}; paymentManager = {}; machOpts = { settlementPeriod: ChannelManager_1.default.DEFAULT_SETTLEMENT_PERIOD + 1, closeOnInvalidPayment: true }; // const channelEthContract = new ChannelEthContract(web3) // const channelTokenContract = new ChannelTokenContract(web3) const channelEthContract = {}; const channelTokenContract = {}; channelContract = new ChannelContract_1.default(web3, channelsDao, channelEthContract, channelTokenContract); channelManager = new ChannelManager_1.default('0xcafe', web3, channelsDao, paymentsDao, tokensDao, channelContract, paymentManager, machOpts); channelEthContract.getSettlementPeriod = sinon.stub().resolves(ChannelManager_1.default.DEFAULT_SETTLEMENT_PERIOD); channelTokenContract.getSettlementPeriod = sinon.stub().resolves(ChannelManager_1.default.DEFAULT_SETTLEMENT_PERIOD); }); afterEach(() => { contractStub.restore(); uuidStub.restore(); }); describe('openChannel', () => { beforeEach(() => { channelsDao.save = sinon.stub().resolves(); channelContract.open = sinon.stub().resolves(fakeLog); }); it('puts a new channel on the blockchain', () => { return channelManager.openChannel('0xcafe', '0xbeef', new BigNumber.BigNumber(10)) .then(() => { expect(channelContract.open .calledWith('0xcafe', '0xbeef', new BigNumber.BigNumber(100), ChannelManager_1.default.DEFAULT_SETTLEMENT_PERIOD + 1)) .toBe(true); }); }); it('saves the new payment channel in the database', () => { return channelManager.openChannel('0xcafe', '0xbeef', new BigNumber.BigNumber(1)) .then(() => { const fakeChan2 = Object.assign({}, fakeChan, { settlementPeriod: fakeChan.settlementPeriod + 1 }); expect(channelsDao.save.calledWith(fakeChan2)).toBe(true); }); }); it('emits willOpenChannel and didOpenChannel', async () => { let n = 0; channelManager.addListener('willOpenChannel', () => { expect(n).toBe(0); n += 1; }); channelManager.addListener('didOpenChannel', () => { expect(n).toBe(1); }); await channelManager.openChannel(fakeChan.sender, fakeChan.receiver, fakeChan.value); }); it('only allows one call at once', () => { const order = []; return Promise.all([ channelManager.openChannel('0xcafe', '0xbeef', new BigNumber.BigNumber(10)).then(() => order.push(1)), channelManager.openChannel('0xcafe', '0xbeef', new BigNumber.BigNumber(10)).then(() => order.push(2)), channelManager.openChannel('0xcafe', '0xbeef', new BigNumber.BigNumber(10)).then(() => order.push(3)) ]).then(() => expect(order).toEqual([1, 2, 3])); }); }); describe('closeChannel', () => { const id = '0xbeef'; const startSettleResult = {}; const finishSettleResult = {}; const claimResult = {}; beforeEach(() => { channelContract.startSettle = sinon.stub().resolves(startSettleResult); channelContract.finishSettle = sinon.stub().resolves(finishSettleResult); deployed.channels = sinon.stub().resolves(['0', '0', new BigNumber.BigNumber(10), new BigNumber.BigNumber(0), new BigNumber.BigNumber(0)]); }); it('throws an error when no channels are found', () => { channelsDao.firstById = sinon.stub().resolves(null); channelContract.channelById = sinon.stub().resolves(Promise.resolve([])); return expects_rejection_1.default(channelManager.closeChannel('nope')); }); it('throws an error if the channel is already settled', () => { channelsDao.firstById = sinon.stub().resolves(fakeChan); channelContract.getState = sinon.stub().resolves(2); channelContract.channelById = sinon.stub().withArgs(id).resolves([fakeChan.sender, fakeChan.receiver, fakeChan.value, ChannelManager_1.default.DEFAULT_SETTLEMENT_PERIOD, 0]); return expects_rejection_1.default(channelManager.closeChannel(id)); }); it('starts settling the contract when the sender is the current account and state is 0', () => { const channel = new PaymentChannel_1.PaymentChannel('0xcafe', '0xbeef', id, new BigNumber.BigNumber(1), new BigNumber.BigNumber(0), 0, ''); channelsDao.firstById = sinon.stub().withArgs(id).resolves(channel); channelContract.getState = sinon.stub().resolves(0); channelContract.channelById = sinon.stub().withArgs(id).resolves([fakeChan.sender, fakeChan.receiver, fakeChan.value, ChannelManager_1.default.DEFAULT_SETTLEMENT_PERIOD, 0]); channelsDao.updateState = sinon.stub().withArgs(id, 1).resolves(); channelsDao.updateSettlingUntil = sinon.stub().withArgs(id, 100).resolves(); return channelManager.closeChannel(id).then((res) => { expect(res).toBe(startSettleResult); expect(channelsDao.updateState.calledWith(id, 1)).toBe(true); }); }); it('finishes settling the contract when the sender is the current account and state is 1', () => { const channel = new PaymentChannel_1.PaymentChannel('0xcafe', '0xbeef', id, new BigNumber.BigNumber(1), new BigNumber.BigNumber(0), 1, ''); channelsDao.firstById = sinon.stub().withArgs(id).resolves(channel); channelContract.getState = sinon.stub().resolves(1); channelContract.channelById = sinon.stub().withArgs(id).resolves([fakeChan.sender, fakeChan.receiver, fakeChan.value, ChannelManager_1.default.DEFAULT_SETTLEMENT_PERIOD, 0]); channelsDao.updateState = sinon.stub().withArgs(id, 2).resolves(); channelsDao.updateSettlingUntil = sinon.stub().withArgs(id, 100).resolves(); return channelManager.closeChannel(id).then((res) => { expect(res).toBe(finishSettleResult); expect(channelsDao.updateState.calledWith(id, 2)).toBe(true); }); }); it('claims the contract when the sender is not the current account', () => { const channel = new PaymentChannel_1.PaymentChannel('0xdead', '0xbeef', id, new BigNumber.BigNumber(1), new BigNumber.BigNumber(0), 1, ''); channelsDao.firstById = sinon.stub().withArgs(id).resolves(channel); channelContract.channelById = sinon.stub().withArgs(id).resolves([fakeChan.sender, fakeChan.receiver, fakeChan.value, ChannelManager_1.default.DEFAULT_SETTLEMENT_PERIOD, 0]); paymentsDao.firstMaximum = sinon.stub().withArgs(id).resolves(new payment_1.default({ channelId: id, sender: channel.sender, receiver: channel.receiver, price: channel.spent, value: channel.value, channelValue: channel.value, signature: Signature_1.default.fromParts({ v: 27, r: '0x01', s: '0x02' }), meta: '', token: undefined, tokenContract: '' })); channelContract.claim = sinon.stub().withArgs(channel.receiver, channel, channel.value, 1, '0x01', '0x02') .resolves(claimResult); channelsDao.updateState = sinon.stub().withArgs(id, 2).resolves(); return channelManager.closeChannel(id).then((res) => { expect(res).toBe(claimResult); expect(channelsDao.updateState.calledWith(id, 2)).toBe(true); }); }); it('emits willCloseChannel and didCloseChannel', () => { const channel = new PaymentChannel_1.PaymentChannel('0xcafe', '0xbeef', id, new BigNumber.BigNumber(1), new BigNumber.BigNumber(0), 0, ''); channelsDao.firstById = sinon.stub().withArgs(id).resolves(channel); channelContract.getState = sinon.stub().resolves(0); channelContract.channelById = sinon.stub().withArgs(id).resolves([fakeChan.sender, fakeChan.receiver, fakeChan.value, ChannelManager_1.default.DEFAULT_SETTLEMENT_PERIOD, 0]); channelsDao.updateState = sinon.stub().withArgs(id, 1).resolves(); channelsDao.updateSettlingUntil = sinon.stub().withArgs(id, 100).resolves(); const will = sinon.stub(); const did = sinon.stub(); channelManager.addListener('willCloseChannel', will); channelManager.addListener('didCloseChannel', did); return channelManager.closeChannel(id).then((res) => { expect(will.calledWith(channel)).toBe(true); expect(did.calledWith(channel)).toBe(true); }); }); it('only allows one call at once', () => { const channel = new PaymentChannel_1.PaymentChannel('0xcafe', '0xbeef', id, new BigNumber.BigNumber(1), new BigNumber.BigNumber(0), 0, ''); channelsDao.firstById = sinon.stub().withArgs(id).resolves(channel); channelContract.getState = sinon.stub().resolves(0); channelsDao.updateState = sinon.stub().withArgs(id, 1).resolves(); channelContract.channelById = sinon.stub().withArgs(id).resolves([fakeChan.sender, fakeChan.receiver, fakeChan.value, ChannelManager_1.default.DEFAULT_SETTLEMENT_PERIOD, 0]); channelsDao.updateSettlingUntil = sinon.stub().withArgs(id, 100).resolves(); const order = []; return Promise.all([ channelManager.closeChannel(id).then(() => order.push(1)), channelManager.closeChannel(id).then(() => order.push(2)), channelManager.closeChannel(id).then(() => order.push(3)) ]).then(() => expect(order).toEqual([1, 2, 3])); }); describe('not in DB but on-chain channels', () => { let savedChannel; function setup(settlingUntil) { savedChannel = new PaymentChannel_1.PaymentChannel('0xcafe', '0xbeef', id, new BigNumber.BigNumber(1), new BigNumber.BigNumber(0), 0, ''); channelsDao.firstById = sinon.stub().withArgs(id).resolves(null); channelContract.channelById = sinon.stub().withArgs(id).resolves([savedChannel.sender, savedChannel.receiver, savedChannel.value, ChannelManager_1.default.DEFAULT_SETTLEMENT_PERIOD, new BigNumber.BigNumber(settlingUntil)]); channelContract.claim = sinon.stub().resolves(claimResult); channelsDao.save = sinon.stub().resolves(); channelContract.getState = sinon.stub().resolves(0); channelsDao.updateState = sinon.stub().withArgs(id, 1).resolves(); channelsDao.updateSettlingUntil = sinon.stub().withArgs(id, 100).resolves(); } beforeEach(() => { setup(0); return channelManager.closeChannel(id); }); it('should close channels that exist on-chain but not in the database', () => { expect(channelContract.channelById.calledWith(id)); }); it('should save the channel in the database', () => { expect(channelsDao.save.lastCall.args[0]).toEqual(savedChannel); }); it('should set the state correctly based on the settlingUntil parameter', async () => { setup(1); await channelManager.closeChannel(id); expect(channelsDao.save.lastCall.args[0]) .toEqual(new PaymentChannel_1.PaymentChannel('0xcafe', '0xbeef', id, new BigNumber.BigNumber(1), new BigNumber.BigNumber(0), 1, '', undefined, new BigNumber.BigNumber(1))); }); }); }); describe('deposit', () => { let id = '0xdead'; beforeEach(() => { deployed.channels = sinon.stub().resolves(['0', '0', new BigNumber.BigNumber(10), new BigNumber.BigNumber(0), new BigNumber.BigNumber(0)]); }); it('should throw an error if no channel is found', () => { channelsDao.firstById = sinon.stub().withArgs(id).resolves(null); channelContract.channelById = sinon.stub().resolves(Promise.resolve([])); return expects_rejection_1.default(channelManager.deposit(id, new BigNumber.BigNumber(6))); }); it('should not update the database if depositing into the channel contract fails', () => { channelsDao.firstById = sinon.stub().withArgs(id).resolves({}); channelsDao.deposit = sinon.stub(); channelContract.channelById = sinon.stub().withArgs(id).resolves([fakeChan.sender, fakeChan.receiver, fakeChan.value, ChannelManager_1.default.DEFAULT_SETTLEMENT_PERIOD, 0]); channelContract.deposit = sinon.stub().rejects('oh no'); return expects_rejection_1.default(channelManager.deposit(id, new BigNumber.BigNumber(1))).then(() => { expect(channelsDao.deposit.notCalled).toBe(true); }); }); it('should return a transaction receipt on success', () => { const value = new BigNumber.BigNumber(10); channelsDao.firstById = sinon.stub().withArgs(id).resolves({}); channelsDao.deposit = sinon.stub().withArgs('0xcafe', id, new BigNumber.BigNumber(10)).resolves(); channelContract.channelById = sinon.stub().withArgs(id).resolves([fakeChan.sender, fakeChan.receiver, fakeChan.value, ChannelManager_1.default.DEFAULT_SETTLEMENT_PERIOD, 0]); channelContract.deposit = sinon.stub().resolves({ tx: '123abc' }); return channelManager.deposit(id, value).then((res) => { expect(res.tx).toEqual('123abc'); }); }); }); describe('nextPayment', () => { const id = '0xdead'; let channel; beforeEach(() => { channel = new PaymentChannel_1.PaymentChannel('0xcafe', '0xbeef', id, new BigNumber.BigNumber(10), new BigNumber.BigNumber(2), 0, ''); channelsDao.firstById = sinon.stub().withArgs(id).resolves(channel); deployed.channels = sinon.stub().resolves(['0', '0', new BigNumber.BigNumber(8), new BigNumber.BigNumber(0), new BigNumber.BigNumber(0)]); }); it('should throw an error if no channel is found', () => { channelsDao.firstById = sinon.stub().withArgs(id).resolves(null); channelContract.channelById = sinon.stub().resolves(Promise.resolve([])); return expects_rejection_1.default(channelManager.nextPayment(id, new BigNumber.BigNumber(6), '')); }); it('should throw an error if the amount to spend is more than the remaining channel value', () => { channelContract.channelById = sinon.stub().withArgs(id).resolves([fakeChan.sender, fakeChan.receiver, fakeChan.value, ChannelManager_1.default.DEFAULT_SETTLEMENT_PERIOD, 0]); return expects_rejection_1.default(channelManager.nextPayment(id, new BigNumber.BigNumber(9), '')); }); it('should return a new payment whose spend is the sum of the existing spend plus amount', () => { channelContract.channelById = sinon.stub().withArgs(id).resolves([fakeChan.sender, fakeChan.receiver, fakeChan.value, ChannelManager_1.default.DEFAULT_SETTLEMENT_PERIOD, 0]); paymentManager.buildPaymentForChannel = sinon.stub().withArgs(channel, sinon.match.object, sinon.match.object, '').callsFake((channel, price, value, meta) => { return new payment_1.default({ channelId: channel.channelId, sender: 'send', receiver: 'recv', price, value, channelValue: new BigNumber.BigNumber(100), signature: Signature_1.default.fromParts({ v: 27, r: '0x01', s: '0x02' }), meta, tokenContract: '', token: undefined }); }); deployed.channels = sinon.stub().resolves(['0', '0', new BigNumber.BigNumber(10), new BigNumber.BigNumber(0), new BigNumber.BigNumber(0)]); return channelManager.nextPayment(id, new BigNumber.BigNumber(8), '').then((payment) => { expect(payment.value.eq(new BigNumber.BigNumber(10))).toBe(true); expect(payment.price.eq(new BigNumber.BigNumber(8))).toBe(true); }); }); }); describe('spendChannel', () => { it('should save the channel in the database', async () => { const payment = new payment_1.default({ channelId: '0xdead', sender: 'send', receiver: 'recv', price: new BigNumber.BigNumber(10), value: new BigNumber.BigNumber(10), channelValue: new BigNumber.BigNumber(100), signature: Signature_1.default.fromParts({ v: 27, r: '0x01', s: '0x02' }), meta: '', tokenContract: '', token: undefined }); channelsDao.saveOrUpdate = sinon.stub().resolves(); paymentsDao.save = sinon.stub().resolves(); await channelManager.spendChannel(payment); expect(channelsDao.saveOrUpdate.calledWith({ sender: 'send', receiver: 'recv', channelId: '0xdead', value: new BigNumber.BigNumber(10), spent: new BigNumber.BigNumber(10), state: undefined, tokenContract: undefined })); expect(paymentsDao.save.called); }); }); describe('acceptPayment', () => { let payment; beforeEach(() => { const id = '0xdead'; payment = { channelId: id, sender: '0xcafe', receiver: '0xbeef', price: new BigNumber.BigNumber(1), value: new BigNumber.BigNumber(1), channelValue: new BigNumber.BigNumber(10), signature: Signature_1.default.fromParts({ v: 27, r: '0x01', s: '0x02' }), meta: '', tokenContract: '', token: '' }; deployed.channels = sinon.stub().resolves(['0', '0', new BigNumber.BigNumber(8), new BigNumber.BigNumber(0), new BigNumber.BigNumber(0)]); channelsDao.findBySenderReceiverChannelId = sinon.stub().resolves(null); }); it('should save the payment to the database and return the token when valid', () => { web3.sha3 = sinon.stub().returns('token'); channelsDao.saveOrUpdate = sinon.stub().withArgs(channelsDao).resolves(); tokensDao.save = sinon.stub().withArgs('token', payment.channelId).resolves(); paymentsDao.save = sinon.stub().withArgs('token', payment).resolves(); paymentManager.isValid = sinon.stub().resolves(true); return channelManager.acceptPayment(payment).then((token) => { expect(token).toBe('token'); }); }); function testNextPayment() { const signature = Signature_1.default.fromParts({ v: 27, r: '0x02', s: '0x03' }); const newChan = Object.assign({}, fakeChan, { sender: '0xbeef', channelId: '456' }); paymentManager.isValid = sinon.stub().resolves(false); channelsDao.findBySenderReceiverChannelId = sinon.stub().resolves(newChan); paymentsDao.firstMaximum = sinon.stub().resolves({ price: new BigNumber.BigNumber(1), value: new BigNumber.BigNumber(0.5), signature }); channelContract.claim = sinon.stub().resolves({}); channelContract.getState = sinon.stub().resolves(0); channelContract.channelById = sinon.stub().resolves([fakeChan.sender, fakeChan.receiver, fakeChan.value, ChannelManager_1.default.DEFAULT_SETTLEMENT_PERIOD, 0]); channelsDao.updateState = sinon.stub().resolves(); channelsDao.firstById = sinon.stub().withArgs(newChan.channelId).resolves(newChan); return expects_rejection_1.default(channelManager.acceptPayment(payment)) .then(() => ({ signature, newChan })); } it('should close the channel if the payment is invalid and a channel exists', () => { return testNextPayment() .then((res) => expect(channelContract.claim .calledWith(fakeChan.receiver, res.newChan.channelId, new BigNumber.BigNumber(0.5), res.signature)).toBe(true)); }); it('should not close the channel if options.closeOnInvalidPayment is false', () => { machOpts.closeOnInvalidPayment = false; return testNextPayment() .then(() => expect(channelContract.claim.notCalled).toBe(true)); }); }); describe('requireOpenChannel', () => { beforeEach(() => { channelsDao.save = sinon.stub().resolves(); channelContract.open = sinon.stub().resolves(fakeLog); }); it('returns any usable channels if found', () => { channelsDao.findUsable = sinon.stub().resolves(fakeChan); return channelManager.requireOpenChannel('0xcafe', '0xbeef', new BigNumber.BigNumber(1)) .then((chan) => { expect(chan).toEqual(fakeChan); expect(channelContract.open.called).toBe(false); }); }); it('creates a new channel if no usable channels are found', () => { channelsDao.findUsable = sinon.stub().resolves(null); return channelManager.requireOpenChannel('0xcafe', '0xbeef', new BigNumber.BigNumber(1)) .then((chan) => { const fakeChan2 = Object.assign({}, fakeChan, { settlementPeriod: fakeChan.settlementPeriod + 1 }); expect(chan).toEqual(fakeChan2); expect(channelContract.open.called).toBe(true); expect(channelsDao.save.calledWith(fakeChan2)).toBe(true); }); }); }); }); //# sourceMappingURL=ChannelManager.test.js.map