ilp-plugin-virtual
Version:
ILP virtual ledger plugin for directly transacting connectors
301 lines (231 loc) • 11.2 kB
JavaScript
'use strict'
const nock = require('nock')
const uuid = require('uuid4')
const crypto = require('crypto')
const base64url = require('base64url')
const chai = require('chai')
chai.use(require('chai-as-promised'))
const assert = chai.assert
const expect = chai.expect
const ObjStore = require('./helpers/objStore')
const PluginVirtual = require('..')
const info = {
currencyCode: 'USD',
currencyScale: 2,
connectors: [ { id: 'other', name: 'other', connector: 'peer.usd.other' } ]
}
const peerAddress = 'peer.NavKx.usd.2.Ivsltficn6wCUiDAoo8gCR0CO5yWb3KBED1a9GrHGwk'
const options = {
currencyCode: 'USD',
currencyScale: 2,
secret: 'seeecret',
maxBalance: '10',
peerPublicKey: 'Ivsltficn6wCUiDAoo8gCR0CO5yWb3KBED1a9GrHGwk',
rpcUri: 'https://example.com/rpc',
info: info
}
describe('Conditional Transfers', () => {
beforeEach(function * () {
this.plugin = new PluginVirtual(Object.assign({},
options, { _store: new ObjStore() }))
this.fulfillment = 'gHJ2QeIZpstXaGZVCSq4d3vkrMSChNYKriefys3KMtI'
const hash = crypto.createHash('sha256')
hash.update(this.fulfillment, 'base64')
this.condition = base64url(hash.digest())
const expiry = new Date()
expiry.setSeconds(expiry.getSeconds() + 5)
this.transfer = {
id: uuid(),
ledger: this.plugin.getInfo().prefix,
from: this.plugin.getAccount(),
to: peerAddress,
amount: '5.0',
data: {
field: 'some stuff'
},
executionCondition: this.condition,
expiresAt: expiry.toISOString()
}
this.incomingTransfer = Object.assign({}, this.transfer, {
from: peerAddress,
to: this.plugin.getAccount()
})
yield this.plugin.connect()
})
afterEach(function * () {
assert(nock.isDone(), 'nocks should all have been called')
})
describe('sendTransfer (conditional)', () => {
it('allows an outgoing transfer to be fulfilled', function * () {
nock('https://example.com')
.post('/rpc?method=send_transfer&prefix=peer.NavKx.usd.2.', [this.transfer])
.reply(200, true)
const sent = new Promise((resolve) => this.plugin.on('outgoing_prepare', resolve))
const fulfilled = new Promise((resolve) => this.plugin.on('outgoing_fulfill', resolve))
yield this.plugin.sendTransfer(this.transfer)
yield sent
yield this.plugin.receive('fulfill_condition', [this.transfer.id, this.fulfillment])
yield fulfilled
assert.equal((yield this.plugin.getBalance()), '-5', 'balance should decrease by amount')
assert.deepEqual(this.plugin._transfers._storeCache, {}, 'transfer cache should be clear')
})
it('fulfills an incoming transfer', function * () {
nock('https://example.com')
.post('/rpc?method=fulfill_condition&prefix=peer.NavKx.usd.2.', [this.transfer.id, this.fulfillment])
.reply(200, true)
const fulfilled = new Promise((resolve) => this.plugin.on('incoming_fulfill', resolve))
yield this.plugin.receive('send_transfer', [this.incomingTransfer])
yield this.plugin.fulfillCondition(this.transfer.id, this.fulfillment)
yield fulfilled
assert.equal((yield this.plugin.getBalance()), '5', 'balance should increase by amount')
assert.deepEqual(this.plugin._transfers._storeCache, {}, 'transfer cache should be clear')
})
it('cancels an incoming transfer for too much money', function * () {
this.incomingTransfer.amount = 100
let incomingPrepared = false
this.plugin.on('incoming_prepare', () => (incomingPrepared = true))
yield expect(this.plugin.receive('send_transfer', [this.incomingTransfer]))
.to.eventually.be.rejectedWith(/adding amount .* to balance .* exceeds maximum/)
assert.isFalse(incomingPrepared, 'incoming_prepare should not be emitted')
assert.equal((yield this.plugin.getBalance()), '0', 'balance should not change')
assert.deepEqual(this.plugin._transfers._storeCache, {}, 'transfer cache should be clear')
const stored = JSON.parse(yield this.plugin._store.get('transfer_' + this.incomingTransfer.id))
assert.equal(stored.state, 'cancelled')
assert.equal(stored.transfer.id, this.incomingTransfer.id)
})
it('should fulfill a transfer even if inital RPC failed', function * () {
nock('https://example.com')
.post('/rpc?method=send_transfer&prefix=peer.NavKx.usd.2.', [this.transfer])
.reply(500)
const fulfilled = new Promise((resolve) => this.plugin.on('outgoing_fulfill', resolve))
const sent = new Promise((resolve) => this.plugin.on('outgoing_prepare', resolve))
yield this.plugin.sendTransfer(this.transfer)
yield sent
yield this.plugin.receive('fulfill_condition', [this.transfer.id, this.fulfillment])
yield fulfilled
assert.equal((yield this.plugin.getBalance()), '-5', 'balance should decrease by amount')
})
it('doesn\'t fulfill a transfer with invalid fulfillment', function * () {
nock('https://example.com')
.post('/rpc?method=send_transfer&prefix=peer.NavKx.usd.2.', [this.transfer])
.reply(200, true)
yield this.plugin.sendTransfer(this.transfer)
yield expect(this.plugin.fulfillCondition(this.transfer.id, 'Garbage'))
.to.eventually.be.rejected
})
it('doesn\'t fulfill an outgoing transfer', function * () {
nock('https://example.com')
.post('/rpc?method=send_transfer&prefix=peer.NavKx.usd.2.', [this.transfer])
.reply(200, true)
yield this.plugin.sendTransfer(this.transfer)
yield expect(this.plugin.fulfillCondition(this.transfer.id, this.fulfillment))
.to.eventually.be.rejected
})
it('should not send a transfer with condition and no expiry', function () {
this.transfer.executionCondition = undefined
return expect(this.plugin.sendTransfer(this.transfer)).to.eventually.be.rejected
})
it('should not send a transfer with expiry and no condition', function () {
this.transfer.expiresAt = undefined
return expect(this.plugin.sendTransfer(this.transfer)).to.eventually.be.rejected
})
it('should resolve even if the event notification handler takes forever', function * () {
nock('https://example.com')
.post('/rpc?method=send_transfer&prefix=peer.NavKx.usd.2.', [this.transfer])
.reply(200, true)
this.plugin.on('outgoing_prepare', () => new Promise((resolve, reject) => {}))
yield this.plugin.sendTransfer(this.transfer)
})
it('should resolve even if the event notification handler throws an error', function * () {
nock('https://example.com')
.post('/rpc?method=send_transfer&prefix=peer.NavKx.usd.2.', [this.transfer])
.reply(200, true)
this.plugin.on('outgoing_prepare', () => {
throw new Error('blah')
})
yield this.plugin.sendTransfer(this.transfer)
})
it('should resolve even if the event notification handler rejects', function * () {
nock('https://example.com')
.post('/rpc?method=send_transfer&prefix=peer.NavKx.usd.2.', [this.transfer])
.reply(200, true)
this.plugin.on('outgoing_prepare', function * () {
throw new Error('blah')
})
yield this.plugin.sendTransfer(this.transfer)
})
})
describe('expireTransfer', () => {
it('expires a transfer', function * () {
nock('https://example.com')
.post('/rpc?method=expire_transfer&prefix=peer.NavKx.usd.2.', [this.transfer.id])
.reply(200, true)
this.incomingTransfer.expiresAt = (new Date()).toISOString()
const cancel = new Promise((resolve) => this.plugin.on('incoming_cancel', resolve))
yield this.plugin.receive('send_transfer', [this.incomingTransfer])
yield cancel
assert.equal((yield this.plugin.getBalance()), '0', 'balance should not change')
assert.deepEqual(this.plugin._transfers._storeCache, {}, 'transfer cache should be clear')
})
it('expires an outgoing transfer', function * () {
nock('https://example.com')
.post('/rpc?method=expire_transfer&prefix=peer.NavKx.usd.2.', [this.transfer.id])
.reply(200, true)
nock('https://example.com')
.post('/rpc?method=send_transfer&prefix=peer.NavKx.usd.2.', [this.transfer])
.reply(200, true)
this.transfer.expiresAt = (new Date()).toISOString()
const cancel = new Promise((resolve) => this.plugin.on('outgoing_cancel', resolve))
yield this.plugin.sendTransfer(this.transfer)
yield cancel
assert.equal((yield this.plugin.getBalance()), '0', 'balance should not change')
})
it('doesn\'t expire an executed transfer', function * () {
nock('https://example.com')
.post('/rpc?method=send_transfer&prefix=peer.NavKx.usd.2.', [this.transfer])
.reply(200, true)
const sent = new Promise((resolve) => this.plugin.on('outgoing_prepare', resolve))
const fulfilled = new Promise((resolve) => this.plugin.on('outgoing_fulfill', resolve))
yield this.plugin.sendTransfer(this.transfer)
yield sent
yield this.plugin.receive('fulfill_condition', [this.transfer.id, this.fulfillment])
yield fulfilled
yield this.plugin._expireTransfer(this.transfer.id)
assert.equal((yield this.plugin.getBalance()), '-5', 'balance should not be rolled back')
})
})
describe('rejectIncomingTransfer', () => {
it('rejects an incoming transfer', function * () {
nock('https://example.com')
.post('/rpc?method=reject_incoming_transfer&prefix=peer.NavKx.usd.2.', [this.transfer.id, 'reason'])
.reply(200, true)
const rejected = new Promise((resolve) => this.plugin.on('incoming_reject', resolve))
yield this.plugin.receive('send_transfer', [this.incomingTransfer])
yield this.plugin.rejectIncomingTransfer(this.transfer.id, 'reason')
yield rejected
assert.equal((yield this.plugin.getBalance()), '0', 'balance should not change')
})
it('should allow an outgoing transfer to be rejected', function * () {
nock('https://example.com')
.post('/rpc?method=send_transfer&prefix=peer.NavKx.usd.2.', [this.transfer])
.reply(200, true)
const rejected = new Promise((resolve) => this.plugin.on('outgoing_reject', resolve))
yield this.plugin.sendTransfer(this.transfer)
yield this.plugin.receive('reject_incoming_transfer', [this.transfer.id, 'reason'])
yield rejected
})
it('should not reject an outgoing transfer', function * () {
nock('https://example.com')
.post('/rpc?method=send_transfer&prefix=peer.NavKx.usd.2.', [this.transfer])
.reply(200, true)
yield this.plugin.sendTransfer(this.transfer)
yield expect(this.plugin.rejectIncomingTransfer(this.transfer.id, 'reason'))
.to.eventually.be.rejected
})
it('should not allow an incoming transfer to be rejected by sender', function * () {
yield this.plugin.receive('send_transfer', [this.incomingTransfer])
yield expect(this.plugin.receive('reject_transfer', [this.transfer.id, 'reason']))
.to.eventually.be.rejected
})
})
})