From 5d42cf060ddcabd842efc83498bcf0a43b050067 Mon Sep 17 00:00:00 2001 From: Christophe Diederichs Date: Fri, 17 Apr 2020 21:29:49 +0200 Subject: [PATCH 1/2] tests added --- index.js | 6 +- package.json | 3 + test.js | 205 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 test.js diff --git a/index.js b/index.js index 3829b32..433b11b 100644 --- a/index.js +++ b/index.js @@ -44,6 +44,7 @@ function configure (opts) { function pay (destination, amount, memo, cb) { if (!api) throw new Error('opts.privateKey must be provided in the constructor') + if (typeof (amount) === 'number') amount = amount.toFixed(4) + ' EOS' api.transact({ actions: [{ @@ -67,6 +68,7 @@ function configure (opts) { } function subscription (filter, rate) { + const self = this let perSecond = 0 if (typeof rate === 'object' && rate) { // dazaar card @@ -113,7 +115,9 @@ function configure (opts) { if (!minSeconds) minSeconds = 0 let overflow = 0 - const now = Date.now() + (minSeconds * 1000) + let now = Date.now() + (minSeconds * 1000) + + now -= 5000 // compensate delay for the seller to receive block for (let i = 0; i < activePayments.length; i++) { const { amount, time } = activePayments[i] diff --git a/package.json b/package.json index ab11d2a..7c99f35 100644 --- a/package.json +++ b/package.json @@ -24,5 +24,8 @@ "eosjs": "^20.0.0", "from2": "^2.3.0", "node-fetch": "^2.6.0" + }, + "devDependencies": { + "tape": "^4.13.2" } } diff --git a/test.js b/test.js new file mode 100644 index 0000000..e4c5d15 --- /dev/null +++ b/test.js @@ -0,0 +1,205 @@ +const test = require('tape') +const deos = require('./') + +var buyerOpts = { + privateKey: '5KDiuujiPNpTEZ1zJ3NNCHDMq8C3SeAmHMbhxv5MGkphTYAHy7s', + account: 'alice', + rpc: 'http://localhost:8888', + chainId: 'cf057bbfb72640471fd910bcb67639c22df9f92470936cddc1ade0e2f2e7dc4f' +} + +var sellerOpts = { + account: 'bob', + privateKey: '5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3', + chainId: 'cf057bbfb72640471fd910bcb67639c22df9f92470936cddc1ade0e2f2e7dc4f', + rpc: 'http://localhost:8888' +} + +var buyer +var seller + +test('configure', t => { + buyer = deos(buyerOpts) + seller = deos(sellerOpts) + + t.assert(buyer.pay && seller.pay) + t.assert(buyer.subscription && seller.subscription) + t.assert(buyer.createTransactionStream && seller.createTransactionStream) + + t.end() +}) + +test('configure testnet', t => { + var testnet = deos.testnet({ account: 'test', privateKey: '5KDiuujiPNpTEZ1zJ3NNCHDMq8C3SeAmHMbhxv5MGkphTYAHy7s' }) + + t.assert(testnet.pay) + t.assert(testnet.subscription) + t.assert(testnet.createTransactionStream) + + t.end() +}) + +test('create transaction stream & pay', t => { + var str = seller.createTransactionStream() + var label = 'pay ' + Math.random().toString(10) + var synced = false + + str.on('synced', function () { + synced = true + buyer.pay(sellerOpts.account, '0.1000 EOS', label, () => {}) + }) + + str.on('data', function (data) { + if (!synced) return + t.equal(label, data.act.data.memo) + str.destroy() + t.end() + }) +}) + +test('subscription & pay', t => { + var amount = 20 + var rate = 0.05 + + // random label prevents update events from historic transactions + var label = 'sub ' + Math.random().toFixed(10) + + const sub = seller.subscription(label, `${rate.toFixed(4)} EOS/s`) + + sub.on('update', function (data) { + t.ok(sub.active()) + + var times = [] + var funds = [] + + // check time/funds are depleting correctly + repeat(50, 200, function () { + var dTime = delta(times) + var dFunds = delta(funds) + + t.assert(avg(dTime) - 200 < 5) + // funds deplete to within 1% of expected rate + t.assert(Math.abs(avg(dFunds) - rate / 5) < 0.01 * rate) + + sub.destroy() + t.end() + }) + + function repeat (n, t, cb) { + if (!sub.active() || n === 0) return cb() + + times.push(sub.remainingTime()) + funds.push(sub.remainingFunds()) + + return setTimeout(repeat, t, --n, t, cb) + } + }) + + sub.on('synced', () => { + buyer.pay(sellerOpts.account, `${amount}.0000 EOS`, label, () => {}) + }) +}) + +test('subscription: sync', t => { + var amount = 0.01 + + // random label prevents update events from historic transactions + var label = 'sync ' + Math.random().toFixed(10) + + buyer.pay(sellerOpts.account, `${amount.toFixed(4)} EOS`, label, function (err) { + if (err) console.error(err.json.error) + + var sub = seller.subscription(label, `0.0001 EOS/s`) + + // before sync complete + t.notOk(sub.active()) + + // wait for sync + sub.on('synced', () => { + t.ok(sub.active()) + t.assert(sub.remainingTime() > 0) + t.assert(amount - sub.remainingFunds() < 110) + + sub.destroy() + t.end() + }) + }) +}) + +test('subscription: long sync', t => { + var amount = 0.01 + + // random label prevents update events from historic transactions + var label = 'long ' + Math.random().toFixed(10) + + buyer.pay(sellerOpts.account, `${amount.toFixed(4)} EOS`, label, function (err) { + if (err) console.error(err) + + repeat(500, function () { + var sub = seller.subscription(label, `0.0001 EOS/s`) + + // before sync complete + t.notOk(sub.active()) + + // wait for sync + sub.once('synced', () => { + t.ok(sub.active()) + t.assert(sub.remainingTime() > 0) + t.assert(sub.remainingFunds() > 0) + + sub.destroy() + t.end() + }) + }) + + function repeat (n, cb) { + if (n === 0) return cb() + + buyer.pay(sellerOpts.account, `0.0001 EOS`, `ignore this${n}`, (err) => { + if (err) return console.error(err) + return setImmediate(repeat, --n, cb) + }) + } + }) +}) + +test('subscription runs out', t => { + var amount = 0.02 + + // random label prevents update events from historic transactions + var label = 'run out ' + Math.random().toFixed(10) + var synced = false + + var rate = amount / 2 + var sub = seller.subscription(label, `${rate} EOS/s`) + + sub.once('synced', function () { + synced = true + t.notOk(sub.active()) + + buyer.pay(sellerOpts.account, `${amount.toFixed(4)} EOS`, label, function (err) { + if (err) console.error(err) + }) + }) + + sub.on('update', function () { + if (!synced) return + t.ok(sub.active()) + + setTimeout(() => { + t.notOk(sub.active()) + + sub.destroy() + t.end() + }, 3000) + }) +}) + +function delta (arr) { + return arr.slice(0, arr.length - 1).map((val, i) => val - arr[i + 1]) +} + +function avg (arr) { + var sum = arr.reduce((acc, val) => acc + val, 0) + return sum / arr.length +} From bcebe29deed7f73f83f2b1c93b21fba244f1e6e4 Mon Sep 17 00:00:00 2001 From: Christophe Diederichs Date: Wed, 22 Apr 2020 19:50:01 +0200 Subject: [PATCH 2/2] move payment-tracker to separate module --- index.js | 55 +++++++++++++--------------------------------------- package.json | 3 ++- test.js | 24 +++++++++++++---------- 3 files changed, 30 insertions(+), 52 deletions(-) diff --git a/index.js b/index.js index 433b11b..79dd6a8 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,5 @@ const from = require('from2') +const clerk = require('payment-tracker') const { EventEmitter } = require('events') const { Api, JsonRpc } = require('eosjs') @@ -67,14 +68,17 @@ function configure (opts) { }).then(() => process.nextTick(cb, null)).catch((err) => process.nextTick(cb, err)) } - function subscription (filter, rate) { + // include 2000ms payment delay to account for block latency + function subscription (filter, paymentInfo, minSeconds, paymentDelay) { const self = this let perSecond = 0 - if (typeof rate === 'object' && rate) { // dazaar card - perSecond = convertDazaarPayment(rate) + if (typeof paymentInfo === 'object' && paymentInfo) { // dazaar card + perSecond = convertDazaarPayment(paymentInfo) + minSeconds = paymentInfo.minSeconds + paymentDelay = paymentInfo.paymentDelay } else { - const match = rate.trim().match(/^(\d(?:\.\d+)?)\s*EOS\s*\/\s*s$/i) + const match = paymentInfo.trim().match(/^(\d(?:\.\d+)?)\s*EOS\s*\/\s*s$/i) if (!match) throw new Error('rate should have the form "n....nn EOS/s"') perSecond = Number(match[1]) } @@ -82,7 +86,7 @@ function configure (opts) { const sub = new EventEmitter() const stream = createTransactionStream() - const activePayments = [] + let payments = clerk(perSecond, minSeconds, paymentDelay) sub.synced = false stream.once('synced', function () { @@ -97,47 +101,16 @@ function configure (opts) { const amount = parseQuantity(data.act.data.quantity) const time = new Date(data.block_time + 'Z').getTime() // The EOS timestamps don't have the ISO Z at the end? - activePayments.push({ amount, time }) + payments.add({ amount, time }) sub.emit('update') }) - sub.active = function (minSeconds) { - return sub.remainingFunds(minSeconds) > 0 - } - - sub.remainingTime = function (minSeconds) { - const funds = sub.remainingFunds(minSeconds) - if (funds <= 0) return 0 - return Math.floor(Math.max(0, funds / perSecond * 1000)) - } - - sub.remainingFunds = function (minSeconds) { - if (!minSeconds) minSeconds = 0 - - let overflow = 0 - let now = Date.now() + (minSeconds * 1000) - - now -= 5000 // compensate delay for the seller to receive block - - for (let i = 0; i < activePayments.length; i++) { - const { amount, time } = activePayments[i] - const nextTime = i + 1 < activePayments.length ? activePayments[i + 1].time : now - - const consumed = Math.max(0, perSecond * ((nextTime - time) / 1000)) - const currentAmount = overflow + amount - - overflow = currentAmount - consumed - if (overflow < 0) { // we spent all the moneys - activePayments.splice(i, 1) // i is always 0 here i think, but better safe than sorry - i-- - overflow = 0 - } - } - - return overflow - } + sub.active = payments.active + sub.remainingTime = payments.remainingTime + sub.remainingFunds = payments.remainingFunds sub.destroy = function () { + payments = null stream.destroy() } diff --git a/package.json b/package.json index 7c99f35..7b697a5 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "dependencies": { "eosjs": "^20.0.0", "from2": "^2.3.0", - "node-fetch": "^2.6.0" + "node-fetch": "^2.6.0", + "payment-tracker": "^0.1.0" }, "devDependencies": { "tape": "^4.13.2" diff --git a/test.js b/test.js index e4c5d15..6049140 100644 --- a/test.js +++ b/test.js @@ -2,15 +2,15 @@ const test = require('tape') const deos = require('./') var buyerOpts = { - privateKey: '5KDiuujiPNpTEZ1zJ3NNCHDMq8C3SeAmHMbhxv5MGkphTYAHy7s', - account: 'alice', + privateKey: '5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3', + account: 'bob', rpc: 'http://localhost:8888', chainId: 'cf057bbfb72640471fd910bcb67639c22df9f92470936cddc1ade0e2f2e7dc4f' } var sellerOpts = { - account: 'bob', - privateKey: '5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3', + account: 'alice', + privateKey: '5KDiuujiPNpTEZ1zJ3NNCHDMq8C3SeAmHMbhxv5MGkphTYAHy7s', chainId: 'cf057bbfb72640471fd910bcb67639c22df9f92470936cddc1ade0e2f2e7dc4f', rpc: 'http://localhost:8888' } @@ -46,7 +46,9 @@ test('create transaction stream & pay', t => { str.on('synced', function () { synced = true - buyer.pay(sellerOpts.account, '0.1000 EOS', label, () => {}) + buyer.pay(sellerOpts.account, '0.1000 EOS', label, (err) => { + if (err) console.log(err) + }) }) str.on('data', function (data) { @@ -96,7 +98,9 @@ test('subscription & pay', t => { }) sub.on('synced', () => { - buyer.pay(sellerOpts.account, `${amount}.0000 EOS`, label, () => {}) + buyer.pay(sellerOpts.account, `${amount}.0000 EOS`, label, (err) => { + if (err) console.log(err) + }) }) }) @@ -171,11 +175,11 @@ test('subscription runs out', t => { var synced = false var rate = amount / 2 - var sub = seller.subscription(label, `${rate} EOS/s`) + var sub = seller.subscription(label, `${rate} EOS/s`, 0, 5000) sub.once('synced', function () { synced = true - t.notOk(sub.active()) + t.notOk(sub.active(), 'sync') buyer.pay(sellerOpts.account, `${amount.toFixed(4)} EOS`, label, function (err) { if (err) console.error(err) @@ -184,10 +188,10 @@ test('subscription runs out', t => { sub.on('update', function () { if (!synced) return - t.ok(sub.active()) + t.ok(sub.active(), 'update') setTimeout(() => { - t.notOk(sub.active()) + t.notOk(sub.active(), 'timeout') sub.destroy() t.end()