ilp-plugin-xrp-paychan
Version:
Uses payment channels on ripple to do fast ILP transactions between you and a peer. Current in-flight payments are at risk (your peer can choose not to give you claims for them), but are secured against the ledger as soon as you get a claim.
939 lines (772 loc) • 34.9 kB
JavaScript
'use strict' /* eslint-env mocha */
const Plugin = require('..')
const Store = require('ilp-store-memory')
const BtpPacket = require('btp-packet')
const { util } = require('ilp-plugin-xrp-paychan-shared')
const nacl = require('tweetnacl')
const { MoneyNotSentError } = require('../src/lib/constants')
const EventEmitter = require('events')
const chai = require('chai')
const chaiAsPromised = require('chai-as-promised')
chai.use(chaiAsPromised)
const sinon = require('sinon')
const sinonChai = require('sinon-chai')
chai.use(sinonChai)
const assert = chai.assert
// peer secretn spCWi5W9SmYYsaDin9Zqe64C27i
describe('Plugin XRP Paychan Symmetric', function () {
beforeEach(function () {
this.sinon = sinon.sandbox.create()
this.plugin = new Plugin({
xrpServer: 'wss://s.altnet.rippletest.net:51233',
secret: 'sahNtietWCRzmX7Z2Zy7Z3EsvFDjv',
address: 'ra3h9tzcipHTZCdQesMthfx4iBZNEEuHXG',
peerAddress: 'rKwCnwtM6et7BVaCZm97hbU8oXkoohReea',
_store: new Store()
})
this.encodeStub = this.sinon.stub(util, 'encodeClaim').returns('abcdefg')
this.sinon.stub(nacl.sign, 'detached').returns('abcdefg')
this.ilpData = {
protocolData: [{
protocolName: 'ilp',
contentType: BtpPacket.MIME_APPLICATION_OCTET_STREAM,
data: Buffer.alloc(0)
}]
}
this.channel = {
settleDelay: util.MIN_SETTLE_DELAY + 1,
destination: this.plugin._address,
publicKey: 'abcdefg',
balance: '100',
amount: '1000'
}
this.submitterStub = this.sinon.stub(this.plugin._txSubmitter, 'submit').resolves({
transaction: {
Account: 'ra3h9tzcipHTZCdQesMthfx4iBZNEEuHXG',
Destination: 'rKwCnwtM6et7BVaCZm97hbU8oXkoohReea',
Sequence: 1
}
})
})
afterEach(function () {
this.sinon.restore()
})
describe('constructor', function () {
beforeEach(function () {
this.opts = {
xrpServer: 'wss://s.altnet.rippletest.net:51233',
secret: 'sahNtietWCRzmX7Z2Zy7Z3EsvFDjv',
address: 'ra3h9tzcipHTZCdQesMthfx4iBZNEEuHXG',
peerAddress: 'rKwCnwtM6et7BVaCZm97hbU8oXkoohReea',
_store: new Store()
}
})
it('should throw an error on non-number currencyScale', function () {
this.opts.currencyScale = 'foo'
assert.throws(() => new Plugin(this.opts),
/currency scale must be a number if specified/)
})
it('should not throw an error on number currencyScale', function () {
this.opts.currencyScale = 6
const plugin = new Plugin(this.opts)
assert.isOk(plugin)
})
})
describe('_handleData', function () {
describe('ilp data', function () {
beforeEach(function () {
this.plugin._paychanReady = true
this.plugin._incomingChannel = 'ASDF1234'
})
it('should throw if paychan is not ready', async function () {
this.plugin._paychanReady = false
await assert.isRejected(this.plugin._handleData(null, {
requestId: 1,
data: this.ilpData
}), /paychan initialization has not completed or has failed/)
})
it('should handle ilp data', async function () {
let handled = false
this.plugin.registerDataHandler(data => {
assert.deepEqual(data, Buffer.alloc(0))
handled = true
return Buffer.from('test_result')
})
const result = await this.plugin._handleData(null, {
requestId: 1,
data: this.ilpData
})
assert.isTrue(handled, 'handler should have been called')
assert.deepEqual(result, [{
protocolName: 'ilp',
contentType: BtpPacket.MIME_APPLICATION_OCTET_STREAM,
data: Buffer.from('test_result')
}], 'result should contain the buffer returned by data handler')
})
it('should throw an error if there is no data handler', function () {
return assert.isRejected(this.plugin._handleData(null, {
requestId: 1,
data: this.ilpData
}), /no request handler registered/)
})
it('should throw an error if there is no ilp data', async function () {
this.plugin.registerDataHandler(() => {})
await assert.isRejected(this.plugin._handleData(null, {
requestId: 1,
data: { protocolData: [] }
}), /no ilp protocol on request/)
})
})
describe('xrp_address subprotocol', function () {
beforeEach(function () {
this.plugin._paychanReady = true
this.plugin._incomingChannel = 'ASDF1234'
this.setPeerAddressStub = this.sinon.stub(this.plugin, '_setPeerAddress')
})
it('should handle xrp_address request', async function () {
const result = await this.plugin._handleData(null, {
requestId: 1,
data: {
protocolData: [{
protocolName: 'xrp_address',
contentType: BtpPacket.MIME_TEXT_PLAIN_UTF8,
data: Buffer.from('peer_xrp_address')
}]
}
})
assert.deepEqual(result, [{
protocolName: 'xrp_address',
contentType: BtpPacket.MIME_TEXT_PLAIN_UTF8,
data: Buffer.from(this.plugin._address)
}])
assert.isTrue(this.setPeerAddressStub.calledWith('peer_xrp_address'))
})
})
describe('info subprotocol', function () {
beforeEach(function () {
this.plugin._paychanReady = true
this.plugin._incomingChannel = 'ASDF1234'
})
it('should handle info request', async function () {
const result = await this.plugin._handleData(null, {
requestId: 1,
data: {
protocolData: [{
protocolName: 'info',
contentType: BtpPacket.MIME_APPLICATION_OCTET_STREAM,
data: Buffer.from([ util.INFO_REQUEST_ALL ])
}]
}
})
assert.deepEqual(result, [{
protocolName: 'info',
contentType: BtpPacket.MIME_APPLICATION_JSON,
data: Buffer.from(JSON.stringify({ currencyScale: 6 }))
}])
})
})
describe('ripple_channel_id subprotocol', function () {
beforeEach(function () {
this.plugin._paychanReady = true
this.getPaymentChannelStub = this.sinon.stub(this.plugin._api, 'getPaymentChannel').resolves(this.channel)
this.validateChannelDetailsStub = this.sinon.stub(this.plugin, '_validateChannelDetails').returns()
this.channelIdRequest = {
requestId: 1,
data: {
protocolData: [{
protocolName: 'ripple_channel_id',
contentType: BtpPacket.MIME_TEXT_PLAIN_UTF8,
data: Buffer.from('peer_channel_id')
}]
}
}
this.sinon.stub(this.plugin, '_call').rejects(new Error('info protocol is not supported'))
this.plugin._incomingClaim = {
amount: '100',
signature: 'some signature'
}
// no need to look at the ledger in this test
this.claimStub = this.sinon.stub(this.plugin, '_claimFunds').resolves()
this.watchStub = this.sinon.stub(this.plugin._watcher, 'watch').resolves()
this.plugin._outgoingChannel = 'my_channel_id'
})
it('should handle ripple_channel_id protocol', async function () {
const result = await this.plugin._handleData(null, this.channelIdRequest)
assert.equal(this.plugin._incomingChannel, 'peer_channel_id', 'incoming channel should be set')
assert.deepEqual(this.plugin._incomingChannelDetails, this.channel, 'incoming channel details should be set')
assert.isTrue(this.watchStub.called, 'should be watching channel')
assert.deepEqual(result, [{
protocolName: 'ripple_channel_id',
contentType: BtpPacket.MIME_TEXT_PLAIN_UTF8,
data: Buffer.from(this.plugin._outgoingChannel)
}], 'result should contain outgoing channel')
})
it('should throw on invalid channel details', async function () {
// no need to look at the ledger in this test
this.validateChannelDetailsStub.throws()
await assert.isRejected(this.plugin._handleData(null, this.channelIdRequest), /Error/)
assert.equal(this.plugin._incomingChannel, null, 'incoming channel should not be set')
})
it('should not race two requests', async function () {
const storeSpy = sinon.spy(this.plugin._store, 'set').withArgs('incoming_channel')
await Promise.all([
this.plugin._handleData(null, this.channelIdRequest),
this.plugin._handleData(null, this.channelIdRequest)
])
assert.equal(storeSpy.callCount, 1,
'incoming channel should only be written once to the store, otherwise there was a race')
})
it('should reset existing channel with ripple_channel_id, after claiming', async function () {
// no need to look at the ledger in this test
this.plugin._incomingChannel = 'peer_channel_id'
this.plugin._outgoingChannel = 'my_channel_id'
this.channelIdRequest.data.protocolData[0].data = Buffer.from('fake_peer_channel_id')
await this.plugin._handleData(null, this.channelIdRequest)
assert.equal(this.plugin._incomingChannel, 'fake_peer_channel_id', 'incoming channel should be set')
assert.isTrue(this.watchStub.called, 'should be watching new channel')
assert.isFalse(this.claimStub.called, 'should not have issued claim')
})
it('should issue claim is there are unclaimed funds while resetting channel id', async function () {
// no need to look at the ledger in this test
this.plugin._incomingChannel = 'peer_channel_id'
this.plugin._outgoingChannel = 'my_channel_id'
// set claim amount higher than channel balance
this.plugin._incomingClaim.amount = '101000000'
this.channelIdRequest.data.protocolData[0].data = Buffer.from('fake_peer_channel_id')
await this.plugin._handleData(null, this.channelIdRequest)
assert.equal(this.plugin._incomingChannel, 'fake_peer_channel_id', 'incoming channel should be set')
assert.isTrue(this.watchStub.called, 'should be watching new channel')
assert.isTrue(this.claimStub.called, 'should have issued claim')
})
it('should continue if a claim cannot be performed', async function () {
// no need to look at the ledger in this test
this.plugin._incomingChannel = 'peer_channel_id'
this.plugin._outgoingChannel = 'my_channel_id'
this.claimStub.rejects(new Error('error!'))
// set claim amount higher than channel balance
this.plugin._incomingClaim.amount = '101000000'
this.channelIdRequest.data.protocolData[0].data = Buffer.from('fake_peer_channel_id')
await this.plugin._handleData(null, this.channelIdRequest)
assert.equal(this.plugin._incomingChannel, 'fake_peer_channel_id', 'incoming channel should be set')
assert.isTrue(this.watchStub.called, 'should be watching new channel')
assert.isTrue(this.claimStub.called, 'should have issued claim')
})
it('should not allow ILP packets while channel is reloading', async function () {
// no need to look at the ledger in this test
this.plugin._incomingChannel = 'peer_channel_id'
this.plugin._outgoingChannel = 'my_channel_id'
let claimAssertionSucceeded = false
this.claimStub.callsFake(async () => {
await assert.isRejected(this.plugin._handleData(null, {
requestId: 1,
data: this.ilpData
}), /channel details are being reloaded/)
claimAssertionSucceeded = true
})
// set claim amount higher than channel balance
this.plugin._incomingClaim.amount = '101000000'
this.channelIdRequest.data.protocolData[0].data = Buffer.from('fake_peer_channel_id')
await this.plugin._handleData(null, this.channelIdRequest)
assert.equal(this.plugin._incomingChannel, 'fake_peer_channel_id', 'incoming channel should be set')
assert.isTrue(this.watchStub.called, 'should be watching new channel')
assert.isTrue(this.claimStub.called, 'should have issued claim')
assert.isTrue(claimAssertionSucceeded)
})
it('should not reset channel when the channel stays the same', async function () {
// no need to look at the ledger in this test
this.plugin._incomingChannel = 'peer_channel_id'
this.plugin._outgoingChannel = 'my_channel_id'
await this.plugin._setIncomingChannel('peer_channel_id')
assert.equal(this.plugin._incomingChannel, 'peer_channel_id', 'incoming channel should not change')
assert.isFalse(this.watchStub.called, 'should be watching new channel')
assert.isFalse(this.claimStub.called, 'should not have issued claim')
})
})
})
describe('_reloadIncomingChannelDetails', function () {
beforeEach(function () {
this.plugin._outgoingChannel = 'my_channel_id'
this.callStub = this.sinon.stub(this.plugin, '_call')
this.callStub.callsFake(async (...args) => {
const {protocolMap} = this.plugin.protocolDataToIlpAndCustom(args[1].data)
if (protocolMap.ripple_channel_id) {
return {
protocolData: [{
protocolName: 'ripple_channel_id',
contentType: BtpPacket.MIME_TEXT_PLAIN_UTF8,
data: Buffer.from('peer_channel_id')
}]
}
} else if (protocolMap.info) {
return this.callStub.__info()
}
})
// bind to callStub so that we can change this function for the high
// scale version
this.callStub.__info = () => {
throw new Error('info protocol is not supported')
}
this.getChanStub = this.sinon.stub(this.plugin._api, 'getPaymentChannel').resolves(this.channel)
this.sinon.stub(this.plugin._watcher.api, 'connect').resolves()
})
it('should return if it cannot query the peer for a channel', async function () {
// simulate the plugin not being able to get incoming channel
this.callStub.resolves({ protocolData: [] })
await this.plugin._reloadIncomingChannelDetails()
assert.isTrue(this.callStub.called, 'should have tried to query peer for balance')
assert.isTrue(this.getChanStub.notCalled, 'should not have reached getPaymentChannel')
})
it('should return if getPaymentChannel gives a rippled error', async function () {
this.plugin._incomingChannel = 'peer_channel_id'
this.getChanStub.rejects(new Error('errrror!'))
await this.plugin._reloadIncomingChannelDetails()
assert.isTrue(this.getChanStub.called, 'should have queried ledger for paychan')
})
it('should set last claimed amount', async function () {
this.plugin._incomingChannel = 'peer_channel_id'
this.getChanStub.resolves(this.channel)
await this.plugin._reloadIncomingChannelDetails()
assert.isTrue(this.getChanStub.called, 'should have queried ledger for paychan')
assert.equal(this.plugin._lastClaimedAmount.toString(), this.plugin.xrpToBase(this.channel.balance))
})
it('should throw if settleDelay is too soon', async function () {
this.plugin._incomingChannel = null
this.channel.settleDelay = util.MIN_SETTLE_DELAY - 1
await assert.isRejected(this.plugin._reloadIncomingChannelDetails(),
/settle delay of incoming payment channel too low/)
})
it('should throw if cancelAfter is specified', async function () {
this.channel.cancelAfter = Date.now() + 1000
await assert.isRejected(this.plugin._reloadIncomingChannelDetails(),
/cancelAfter must not be set/)
})
it('should throw if expiration is specified', async function () {
this.channel.expiration = Date.now() + 1000
await assert.isRejected(this.plugin._reloadIncomingChannelDetails(),
/expiration must not be set/)
})
it('should throw if destination does not match our account', async function () {
this.channel.destination = this.plugin._peerAddress
await assert.isRejected(this.plugin._reloadIncomingChannelDetails(),
/Channel destination address wrong/)
})
it('should setup auto claim if all details are ok', async function () {
const clock = sinon.useFakeTimers({toFake: ['setInterval']})
this.sinon.stub(this.plugin._api, 'getFee').resolves('0.000001')
await this.plugin._reloadIncomingChannelDetails()
assert.isTrue(!!this.plugin._claimIntervalId,
'claim interval should be started if reload was successful')
this.plugin._incomingClaim = {
amount: this.plugin._lastClaimedAmount.plus(101).toString()
}
const stub = sinon.stub(this.plugin, '_claimFunds').resolves()
clock.tick(util.DEFAULT_CLAIM_INTERVAL)
await new Promise(resolve => setImmediate(resolve))
assert(stub.calledOnce, 'Expected claimFunds to be called once')
clock.restore()
})
it('should not auto claim if fee is too high', async function () {
const clock = sinon.useFakeTimers({toFake: ['setInterval']})
this.sinon.stub(this.plugin._api, 'getFee').resolves('0.000011')
await this.plugin._reloadIncomingChannelDetails()
assert.isTrue(!!this.plugin._claimIntervalId,
'claim interval should be started if reload was successful')
this.plugin._incomingClaim = {
amount: this.plugin._lastClaimedAmount.plus(1000).toString()
}
const stub = sinon.stub(this.plugin, '_claimFunds').resolves()
clock.tick(util.DEFAULT_CLAIM_INTERVAL)
await new Promise(resolve => setImmediate(resolve))
assert.isFalse(stub.called, 'claim should not have been called')
clock.restore()
})
describe('with high scale', function () {
beforeEach(function () {
this.plugin._currencyScale = 9
this.plugin._channelAmount = 1e9
this.plugin._outgoingClaim = {
amount: '990',
signature: '61626364656667'
}
this.callStub.__info = () => ({
protocolData: [{
protocolName: 'info',
contentType: BtpPacket.MIME_APPLICATION_JSON,
data: Buffer.from(JSON.stringify({ currencyScale: 9 }))
}]
})
})
it('should use correct units when determining if claim is profitable', async function () {
const clock = sinon.useFakeTimers({toFake: ['setInterval']})
this.sinon.stub(this.plugin._api, 'getFee').resolves('0.000010')
await this.plugin._reloadIncomingChannelDetails()
assert.isTrue(!!this.plugin._claimIntervalId,
'claim interval should be started if reload was successful')
this.plugin._incomingClaim = {
amount: this.plugin._lastClaimedAmount.plus(1000000).toString()
}
const stub = sinon.stub(this.plugin, '_claimFunds').resolves()
const spy = sinon.spy(this.plugin, '_isClaimProfitable')
clock.tick(util.DEFAULT_CLAIM_INTERVAL)
await new Promise(resolve => setImmediate(resolve))
assert(stub.calledOnce, 'Expected claimFunds to be called once')
assert(spy.calledOnce, 'Expected profitability to be checked')
this.plugin._incomingClaim = {
amount: this.plugin._lastClaimedAmount.plus(999999).toString()
}
clock.tick(util.DEFAULT_CLAIM_INTERVAL)
await new Promise(resolve => setImmediate(resolve))
assert(stub.calledOnce, 'Expected claimFunds to still only be called once')
assert(spy.calledTwice, 'Expected profitability to be checked twice')
clock.restore()
})
})
})
describe('_getPeerInfo', function () {
beforeEach(function () {
this.plugin._incomingChannel = 'peer_channel_id'
this.plugin._currencyScale = 9
this.callStub = this.sinon.stub(this.plugin, '_call')
})
it('should throw an error if the peer doesn\'t support info', async function () {
this.callStub.rejects(new Error('no ilp protocol on request'))
return assert.isRejected(this.plugin._getPeerInfo(),
/peer is unable to accomodate our currencyScale; they are on an out of date version of this plugin/)
})
it('should not throw an error if the peer doesn\'t support info but our scale is 6', function () {
this.plugin._currencyScale = 6
this.callStub.rejects(new Error('no ilp protocol on request'))
return assert.isFulfilled(this.plugin._getPeerInfo())
})
it('should throw an error if the peer scale does not match ours', function () {
this.callStub.resolves({ protocolData: [{
protocolName: 'info',
contentType: BtpPacket.MIME_APPLICATION_JSON,
data: Buffer.from(JSON.stringify({ currencyScale: 8 }))
}]})
return assert.isRejected(this.plugin._getPeerInfo(),
/Fatal! Currency scale mismatch./)
})
it('should succeed if scales match', async function () {
this.callStub.resolves({ protocolData: [{
protocolName: 'info',
contentType: BtpPacket.MIME_APPLICATION_JSON,
data: Buffer.from(JSON.stringify({ currencyScale: 9 }))
}]})
return assert.isFulfilled(this.plugin._getPeerInfo())
})
})
describe('_connect', function () {
beforeEach(function () {
// mock out the rippled connection
this.plugin._api.connection = new EventEmitter()
this.plugin._api.connection.request = () => Promise.resolve(null)
this.sinon.stub(this.plugin._api, 'connect').resolves(null)
this.channelId = '945BB98D2F03DFA2AED810F8917B2BC344C0AA182A5DB506C16F84593C24244F'
this.tagStub = this.sinon.stub(util, 'randomTag').returns(1)
this.loadStub = this.sinon.stub(this.plugin._api, 'getPaymentChannel')
.callsFake(id => {
assert.equal(id, this.channelId)
return Promise.resolve(this.channel)
})
})
describe('high scale', function () {
beforeEach(function () {
this.plugin._currencyScale = 9
this.plugin._channelAmount = 1e9
this.plugin._outgoingClaim = {
amount: '990',
signature: '61626364656667'
}
})
it('should use the right units for payment channel create', async function () {
await this.plugin._connect()
assert.isTrue(this.submitterStub.called, 'should have submitted tx to ledger')
assert.deepEqual(this.submitterStub.firstCall.args, [
'preparePaymentChannelCreate',
{
amount: '1.000000',
destination: 'rKwCnwtM6et7BVaCZm97hbU8oXkoohReea',
settleDelay: 3600,
publicKey: 'ED9BE8997EFFA0A6C6FE9244D0FF8B47D7BEE85A7AF2BC8390FA29474A6D085164',
sourceTag: 1
}
])
})
})
it('should fetch peer address from peer', async function () {
const xrpAddressStub = this.sinon.stub(this.plugin, '_sendXrpAddressRequest')
.resolves({ protocolData: [
{
protocolName: 'xrp_address',
data: Buffer.from('peer_xrp_address')
}
]})
const setAddressStub = this.sinon.stub(this.plugin, '_setPeerAddress')
delete this.plugin._peerAddress
await this.plugin._connect()
assert.isTrue(xrpAddressStub.called)
assert.isTrue(setAddressStub.calledWith('peer_xrp_address'))
})
it('should throw if peer address cannot be fetched', async function () {
const xrpAddressStub = this.sinon.stub(this.plugin, '_sendXrpAddressRequest')
.rejects(new Error('cannot get address from peer'))
const setAddressStub = this.sinon.stub(this.plugin, '_setPeerAddress')
delete this.plugin._peerAddress
await assert.isRejected(this.plugin._connect(), /cannot get address from peer/)
assert.isTrue(xrpAddressStub.called)
assert.isFalse(setAddressStub.called)
})
it('should load outgoing channel if exists', async function () {
this.plugin._store.load('outgoing_channel')
this.plugin._store.set('outgoing_channel', this.channelId)
await this.plugin._connect()
assert.isTrue(this.loadStub.called, 'should have loaded outgoing channel')
})
it('should load incoming channel details even if incoming channel already exists', async function () {
this.plugin._store.load('incoming_channel')
this.plugin._store.set('incoming_channel', this.channelId)
const spy = this.sinon.spy(this.plugin, '_reloadIncomingChannelDetails')
await this.plugin._connect()
assert.deepEqual(this.loadStub.firstCall.args, [ this.channelId ])
assert.isTrue(spy.calledOnce)
})
it('should prepare a payment channel', async function () {
await this.plugin._connect()
assert.isTrue(this.plugin._paychanReady)
assert.isTrue(this.tagStub.called, 'should have generated source tag')
assert.isTrue(this.submitterStub.called, 'should have submitted tx to ledger')
assert.isTrue(this.loadStub.called, 'should have loaded outgoing channel')
})
})
describe('_claimFunds', function () {
beforeEach(function () {
this.plugin._incomingClaim = {
amount: '100',
signature: 'some signature'
}
this.plugin._incomingChannelDetails = this.channel
})
it('should return if incomingClaim has no signature', async function () {
delete this.plugin._incomingClaim.signature
await this.plugin._claimFunds()
assert.isFalse(this.submitterStub.called, 'should not have prepared a claim tx with no signature')
})
it('should submit tx if incomingClaim is valid', async function () {
await this.plugin._claimFunds()
assert.isTrue(this.submitterStub.calledWith('preparePaymentChannelClaim'))
})
describe('high scale', function () {
beforeEach(function () {
this.plugin._currencyScale = 9
this.plugin._incomingClaim = {
amount: '3000',
signature: 'some signature'
}
})
it('should use the right units for payment channel create', async function () {
await this.plugin._claimFunds()
assert.isTrue(this.submitterStub.called, 'should have submitted tx to ledger')
assert.deepEqual(this.submitterStub.firstCall.args, [
'preparePaymentChannelClaim',
{
balance: '0.000003',
channel: null,
signature: 'SOME SIGNATURE',
publicKey: 'abcdefg'
}
])
})
})
})
describe('_disconnect', function () {
beforeEach(function () {
this.plugin._claimIntervalId = setInterval(() => {}, 5000)
this.claimStub = this.sinon.stub(this.plugin, '_claimFunds')
this.disconnectStub = this.sinon.stub(this.plugin._api, 'disconnect')
})
afterEach(function () {
assert.isTrue(this.claimStub.called, 'should have claimed funds')
assert.isTrue(this.disconnectStub.called, 'should have disconnected api')
})
it('should claim and disconnect the api', async function () {
await this.plugin._disconnect()
})
it('should still disconnect if claim fails', async function () {
this.claimStub.throws()
await this.plugin._disconnect()
})
it('should still disconnect if api disconnect fails', async function () {
this.disconnectStub.throws()
await this.plugin._disconnect()
})
})
describe('_sendMoney', function () {
beforeEach(function () {
this.plugin._funding = true // turn off the funding path
this.plugin._outgoingChannel = 'my_channel_id'
this.plugin._outgoingClaim = { amount: '0' }
this.plugin._outgoingChannelDetails = {
amount: '10'
}
this._outgoingClaim = {
amount: '100',
signature: '61626364656667'
}
})
describe('with low scale', function () {
beforeEach(function () {
this.plugin._currencyScale = 2
this.plugin._outgoingClaim = {
amount: '9',
signature: '61626364656667'
}
})
it('should multiply base to get drops', async function () {
this.sinon.stub(this.plugin, '_call').resolves(null)
await this.plugin.sendMoney(2)
assert.deepEqual(this.encodeStub.getCall(0).args, [ '110000', 'my_channel_id' ])
})
})
describe('funding', function () {
beforeEach(function () {
this.plugin._funding = false
this.fundStub = this.sinon.stub(util, 'fundChannel').resolves()
this.sinon.stub(this.plugin, '_call').resolves(null)
this.plugin._outgoingChannelDetails = {
settleDelay: util.MIN_SETTLE_DELAY + 1,
destination: this.plugin._address,
publicKey: 'abcdefg',
balance: '0',
amount: '10'
}
})
it('should not issue fund if claim is below threshold', async function () {
const sendRippleChannelIdStub = this.sinon.stub(this.plugin, '_sendRippleChannelIdRequest')
const reloadOutgoingStub = this.sinon.stub(this.plugin, '_reloadOutgoingChannelDetails')
await this.plugin.sendMoney('5000000')
assert.isFalse(this.fundStub.called, 'fund should not have been called')
assert.isFalse(sendRippleChannelIdStub.called, 'should not tell peer to reload channel')
assert.isFalse(reloadOutgoingStub.called, 'should not reload outgoing channel')
})
it('should issue fund if claim is above threshold', async function () {
await this.plugin.sendMoney('5000001')
assert.isTrue(this.fundStub.called, 'fund should have been called')
assert.deepEqual(this.fundStub.firstCall.args, [{
api: this.plugin._api,
channel: 'my_channel_id',
amount: '10000000',
address: this.plugin._address,
secret: this.plugin._secret
}])
})
it('should reload details after fund', async function () {
const sendRippleChannelIdStub = this.sinon.stub(this.plugin, '_sendRippleChannelIdRequest')
const reloadOutgoingStub = this.sinon.stub(this.plugin, '_reloadOutgoingChannelDetails')
await this.plugin.sendMoney('5000001')
assert.isTrue(this.fundStub.called, 'fund should have been called')
assert.isTrue(sendRippleChannelIdStub.called, 'should tell peer to reload channel')
assert.isTrue(reloadOutgoingStub.called, 'should reload outgoing channel')
})
})
describe('with high scale', function () {
beforeEach(function () {
this.plugin._currencyScale = 9
this.plugin._outgoingClaim = {
amount: '990',
signature: '61626364656667'
}
})
it('should round high-scale amount up to next drop', async function () {
this.sinon.stub(this.plugin, '_call').resolves(null)
await this.plugin.sendMoney(100)
assert.deepEqual(this.encodeStub.getCall(0).args, [ '2', 'my_channel_id' ])
})
it('should keep error under a drop even on repeated roundings', async function () {
this.sinon.stub(this.plugin, '_call').resolves(null)
await this.plugin.sendMoney(100)
await this.plugin.sendMoney(100)
assert.deepEqual(this.encodeStub.getCall(0).args, [ '2', 'my_channel_id' ])
assert.deepEqual(this.encodeStub.getCall(1).args, [ '2', 'my_channel_id' ])
})
})
it('should throw an error signing a claim higher than channel amount', async function () {
await assert.isRejected(this.plugin.sendMoney(11 * 10e6), MoneyNotSentError,
/claim amount exceeds channel balance. claimAmount=110000000 channelAmount=10000000 channel=my_channel_id/)
})
it('should sign a claim and submit it to the other side', async function () {
this.sinon.stub(this.plugin, '_call').callsFake((from, data) => {
// forgive me
assert.deepEqual(data.data.protocolData[0].data,
Buffer.from(JSON.stringify(this._outgoingClaim)))
})
await this.plugin.sendMoney(100)
})
})
describe('_handleMoney', function () {
beforeEach(function () {
this.plugin._paychanReady = true
this.claimAmount = '100'
this.claimSignature = 'abcdefg'
this.claimData = () => ({
amount: '100',
protocolData: [{
protocolName: 'claim',
contentType: BtpPacket.MIME_APPLICATION_JSON,
data: JSON.stringify({
amount: this.claimAmount,
signature: this.claimSignature
})
}]
})
this.plugin._incomingChannelDetails = this.channel
this.plugin._incomingClaim = {
amount: '0',
signature: 'abcdefg'
}
this.naclStub = this.sinon.stub(nacl.sign.detached, 'verify').returns(true)
})
it('throws an error if new claim is less than old claim', async function () {
this.plugin._incomingClaim.amount = '100'
await assert.isRejected(
this.plugin._handleMoney(null, { requestId: 1, data: this.claimData() }),
/new claim is less than old claim. new=100 old=100/)
})
it('throws an error if the signature is not valid', async function () {
this.naclStub.throws()
await assert.isRejected(
this.plugin._handleMoney(null, { requestId: 1, data: this.claimData() }),
/got invalid claim signature abcdefg for amount 100 drops total/)
})
it('throws an error if the claim is for more than the channel capacity', async function () {
this.plugin._incomingChannelDetails.amount = '0.000001'
await assert.isRejected(
this.plugin._handleMoney(null, { requestId: 1, data: this.claimData() }),
/got claim for amount higher than channel balance. amount: 100 incoming channel amount: 1/)
})
it('calls the money handler on success', async function () {
let handled = false
this.plugin.registerMoneyHandler(amount => {
assert.deepEqual(amount, '100')
handled = true
return Buffer.from('test_result')
})
await this.plugin._handleMoney(null, { requestId: 1, data: this.claimData() })
assert.isTrue(handled, 'handler should have been called')
})
it('should handle a claim with high scale', async function () {
this.claimAmount = 1160
this.plugin._incomingClaim = {
amount: '990',
signature: 'some signature'
}
this.plugin._incomingChannel = 'abcdef'
this.plugin._currencyScale = 9
await this.plugin._handleMoney(null, {
requestId: 1,
data: this.claimData()
})
assert.deepEqual(this.encodeStub.getCall(0).args, [ '2', 'abcdef' ])
})
})
})