Checking-in pre-crash progress

This commit is contained in:
Chris Troutner
2021-09-10 11:29:38 -07:00
parent a43747c656
commit 806a1437f6
10 changed files with 521 additions and 32 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
This is a prototype web service that monitors the [P2WDB]() for trading signals, to trade BCH and SLP tokens. It's inspired by the [SWaP Prototype]().
This is a prototype web service that monitors the [P2WDB](https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service) for trading signals, to trade BCH and SLP tokens. It's inspired by the [SWaP Prototype](https://github.com/vinarmani/swap-protocol/blob/master/swap-protocol-spec.md).
This repository was forked from [ipfs-torlist-service](https://github.com/Permissionless-Software-Foundation/ipfs-torlist-service).
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "ipfs-torlist-service",
"name": "ipfs-swap-service",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ipfs-torlist-service",
"name": "ipfs-swap-service",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
+9 -6
View File
@@ -10,7 +10,8 @@ class Bch {
constructor () {
// Encapsulate dependencies
this.bchjs = new BCHJS()
this.PSF_TOKEN_ID = '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0'
this.PSF_TOKEN_ID =
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0'
this.msgLib = new MsgLib({ bchjs: this.bchjs })
}
@@ -18,15 +19,15 @@ class Bch {
_verifySignature (verifyObj) {
try {
// Expand the input object.
const { slpAddress, signature, entry } = verifyObj
const { offerBchAddr, signature, sigMsg } = verifyObj
// Convert to BCH address.
const scrubbedAddr = this.bchjs.SLP.Address.toCashAddress(slpAddress)
// const scrubbedAddr = this.bchjs.SLP.Address.toCashAddress(slpAddress)
const isValid = this.bchjs.BitcoinCash.verifyMessage(
scrubbedAddr,
offerBchAddr,
signature,
entry
sigMsg
)
return isValid
@@ -43,7 +44,9 @@ class Bch {
throw new Error('slpAddress must be a string')
}
let psfBalance = 0
const balances = await this.bchjs.SLP.Utils.balancesForAddress(slpAddress)
const balances = await this.bchjs.SLP.Utils.balancesForAddress(
slpAddress
)
// Sums all the balances of all tokens
// that match the psf token ID
+23
View File
@@ -0,0 +1,23 @@
const mongoose = require('mongoose')
const Offer = new mongoose.Schema({
// SWaP Protocol Properties
lokadId: { type: String },
messageType: { type: Number },
messageClass: { type: Number },
tokenId: { type: String },
buyOrSell: { type: String },
rateInSats: { type: String },
minSatsToExchange: { type: String },
signature: { type: String },
sigMsg: { type: String },
utxoTxid: { type: String },
utxoVout: { type: Number },
//
offerIpfsId: { type: String },
offerBchAddr: { type: String },
offerPubKey: { type: String }
})
module.exports = mongoose.model('offer', Offer)
@@ -0,0 +1,67 @@
/*
REST API Controller library for the /offer route
*/
// const { wlogger } = require('../../../adapters/wlogger')
let _this
class OfferRESTControllerLib {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating /offer REST Controller.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating /offer REST Controller.'
)
}
// Encapsulate dependencies
this.OfferModel = this.adapters.localdb.Offer
// this.userUseCases = this.useCases.user
_this = this
}
// No api-doc documentation because this wont be a public endpoint
async createOffer (ctx) {
try {
console.log('body: ', ctx.request.body)
const offerObj = ctx.request.body.entry
const offer = await _this.useCases.offer.createoffer(offerObj)
ctx.body = { offer }
} catch (err) {
// console.log(`err.message: ${err.message}`)
// console.log('err: ', err)
// ctx.throw(422, err.message)
_this.handleError(ctx, err)
}
}
// DRY error handler
handleError (ctx, err) {
console.log('err', err.message)
// If an HTTP status is specified by the buisiness logic, use that.
if (err.status) {
if (err.message) {
ctx.throw(err.status, err.message)
} else {
ctx.throw(err.status)
}
} else {
// By default use a 422 error if the HTTP status is not specified.
ctx.throw(422, err.message)
}
}
}
module.exports = OfferRESTControllerLib
+60
View File
@@ -0,0 +1,60 @@
/*
REST API library for /offer route.
*/
// Public npm libraries.
const Router = require('koa-router')
// Local libraries.
const OfferRESTControllerLib = require('./controller')
let _this
class OfferRouter {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating /offer REST Controller.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating /offer REST Controller.'
)
}
const dependencies = {
adapters: this.adapters,
useCases: this.useCases
}
// Encapsulate dependencies.
this.offerRESTController = new OfferRESTControllerLib(dependencies)
// Instantiate the router and set the base route.
const baseUrl = '/offer'
this.router = new Router({ prefix: baseUrl })
_this = this
}
attach (app) {
if (!app) {
throw new Error(
'Must pass app object when attaching REST API controllers.'
)
}
// Define the routes and attach the controller.
this.router.post('/', _this.offerRESTController.createOffer)
// Attach the Controller routes to the Koa app.
app.use(_this.router.routes())
app.use(_this.router.allowedMethods())
}
}
module.exports = OfferRouter
+52
View File
@@ -0,0 +1,52 @@
/*
Offer Entity
An offer is created when a new Signal is detected. It's destroyed when the
UTXO described in the Signal has been detected as spent.
{
lokadId: 'SWP', // Placeholder for now, use for backwards compatibility
messageType: 1, // SLP Atomic Swap
messageClass: 1,
tokenId: '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
buyOrSell: 'sell',
rateInSats: 7972, // Price per token in sats
minSatsToExchange: 8100, // Minum size of UTXO to use, e.g. 1 token + miner fees
// Signature from the address holding the tokens, provides 'proof of reserves'
signature: 'H2Sq0UPh0jgs1Zt3JERHtbzfPGXJk9DgJ0FVxVa6iUqiIh6XcvEUFBbvYIuODQs3hYSCkkjcuzbvzNEiv69kFKg=',
sigMsg: 'test',
address: 'bitcoincash:qphjncqpnv444jq8acqk4dkm3296c50xhqggeatvn8',
// UTXO for sale
utxoTxid: 'b9457808be70c39a9cc6c5857cbef856b35fdc91a59debfe06acfc45b11955e3',
utxoVout: 2
}
*/
class Offer {
validate ({ data } = {}) {
// Input Validation
// if (!entry || typeof entry !== "string") {
// throw new Error("Property 'entry' must be a string!");
// }
// if (!description || typeof description !== "string") {
// throw new Error("Property 'description' must be a string!");
// }
// if (!slpAddress || typeof slpAddress !== "string") {
// throw new Error("Property 'slpAddress' must be a string!");
// }
// if (!signature || typeof signature !== "string") {
// throw new Error("Property 'signature' must be a string!");
// }
// if (!category || typeof category !== "string") {
// throw new Error("Property 'category' must be a string!");
// }
const offerData = {
data
}
return offerData
}
}
module.exports = Offer
+66
View File
@@ -0,0 +1,66 @@
const { wlogger } = require('../adapters/wlogger')
const OfferEntity = require('../entities/offer')
class OfferLib {
constructor (localConfig = {}) {
// console.log('User localConfig: ', localConfig)
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of adapters must be passed in when instantiating Offer Use Cases library.'
)
}
// Encapsulate dependencies
this.OfferEntity = new OfferEntity()
this.OfferModel = this.adapters.localdb.Offer
this.bchjs = this.adapters.bchjs
}
// Create a new offer model and add it to the Mongo database.
async createOffer (entryObj) {
try {
// Input Validation
const offerEntity = this.OfferEntity.validate(entryObj)
// Verify that the entry was signed by a specific BCH address.
const isValidSignature = this.bchjs._verifySignature(offerEntity)
if (!isValidSignature) {
throw new Error('Invalid signature')
}
// Verify psf tokens balance
// const psfBalance = await this.bchjs.getPSFTokenBalance(entryEntity.slpAddress)
//
// if (psfBalance < 10) {
// throw new Error('Insufficient psf balance')
// }
//
// const merit = await this.bchjs.getMerit(entryEntity.slpAddress)
//
// const updatedEntry = {
// entry: entryEntity.entry.trim(),
// slpAddress: entryEntity.slpAddress.trim(),
// description: entryEntity.description.trim(),
// signature: entryEntity.signature.trim(),
// category: entryEntity.category.trim(),
// balance: psfBalance,
// merit
// }
//
// const entryModel = new this.EntryModel(updatedEntry)
// await entryModel.save()
//
// return entryModel
return true
} catch (err) {
// console.log("Error in use-cases/entry.js/createEntry()", err.message)
wlogger.error('Error in use-cases/entry.js/createOffer())')
throw err
}
}
}
module.exports = OfferLib
+25 -23
View File
@@ -22,11 +22,12 @@ describe('bch', () => {
describe('#_verifySignature', () => {
it('should return true for valid signature', () => {
const slpAddress = 'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
const offerBchAddr =
'bitcoincash:qphjncqpnv444jq8acqk4dkm3296c50xhqggeatvn8'
const signature =
'H1Bv2xUBGZBTuNsUghix03Yp8n8YPPkfsPq6LktwDpC2e1estOfYx96NH3/eaHJpQpPSHSb6pQYaiR3KZ6Z9lRc='
const entry = 'example.com'
const verifyObj = { slpAddress, signature, entry }
'Hz8bi9CsHaYkk5SGtHLU0aaxFspEXz7IdBNn6xV8ejE6OCuIRoZuVE9QJsGSlJ3Rt0ez2LWD0e292NZ84rRwnfk='
const sigMsg = 'example.com'
const verifyObj = { offerBchAddr, signature, sigMsg }
const result = uut._verifySignature(verifyObj)
@@ -34,12 +35,12 @@ describe('bch', () => {
})
it('should return false for invalid signature', () => {
const slpAddress = 'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
const offerBchAddr =
'bitcoincash:qphjncqpnv444jq8acqk4dkm3296c50xhqggeatvn8'
const signature =
'ICcj+ShSRIllp0iTqQK49Ltnycg1upaT7dK5CPAwNIBqEtegn305dPBf5IMdx/ScuyOBWPEfOqab2V73TbuK6Us='
const entry = 'example.com'
const verifyObj = { slpAddress, signature, entry }
'Hz8bi9CsHaYkk5SGtHLU0aaxFspEXz7IdBNn6xV8ejE6OCuIRoZuVE9QJsGSlJ3Rt0ez2LWD0e292NZ84rRwnfd='
const sigMsg = 'example.com'
const verifyObj = { offerBchAddr, signature, sigMsg }
const result = uut._verifySignature(verifyObj)
@@ -53,11 +54,12 @@ describe('bch', () => {
.stub(uut.bchjs.BitcoinCash, 'verifyMessage')
.throws(new Error('test error'))
const slpAddress = 'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
const offerBchAddr =
'bitcoincash:qphjncqpnv444jq8acqk4dkm3296c50xhqggeatvn8'
const signature =
'ICcj+ShSRIllp0iTqQK49Ltnycg1upaT7dK5CPAwNIBqEtegn305dPBf5IMdx/ScuyOBWPEfOqab2V73TbuK6Us='
const entry = 'example.com'
const verifyObj = { slpAddress, signature, entry }
'Hz8bi9CsHaYkk5SGtHLU0aaxFspEXz7IdBNn6xV8ejE6OCuIRoZuVE9QJsGSlJ3Rt0ez2LWD0e292NZ84rRwnfk='
const sigMsg = 'example.com'
const verifyObj = { offerBchAddr, signature, sigMsg }
uut._verifySignature(verifyObj)
assert.fail('Unexpected result')
@@ -83,7 +85,8 @@ describe('bch', () => {
.stub(uut.bchjs.SLP.Utils, 'balancesForAddress')
.resolves(mockData.psfBalances)
const slpAddress = 'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
const slpAddress =
'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
const result = await uut.getPSFTokenBalance(slpAddress)
assert.isNumber(result)
@@ -96,7 +99,8 @@ describe('bch', () => {
.stub(uut.bchjs.SLP.Utils, 'balancesForAddress')
.resolves(mockData.noPsfBalances)
const slpAddress = 'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
const slpAddress =
'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
const result = await uut.getPSFTokenBalance(slpAddress)
assert.isNumber(result)
@@ -105,11 +109,10 @@ describe('bch', () => {
it('should return 0 for empty balances', async () => {
// Mock live network calls.
sandbox
.stub(uut.bchjs.SLP.Utils, 'balancesForAddress')
.resolves([])
sandbox.stub(uut.bchjs.SLP.Utils, 'balancesForAddress').resolves([])
const slpAddress = 'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
const slpAddress =
'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
const result = await uut.getPSFTokenBalance(slpAddress)
assert.isNumber(result)
@@ -136,11 +139,10 @@ describe('bch', () => {
it('should return the merit ', async () => {
try {
// Mock live network calls.
sandbox
.stub(uut.msgLib.merit, 'agMerit')
.resolves(100)
sandbox.stub(uut.msgLib.merit, 'agMerit').resolves(100)
const slpAddr = 'simpleledger:qqgnksc6zr4nzxrye69fq625wu2myxey6uh9kzjy96'
const slpAddr =
'simpleledger:qqgnksc6zr4nzxrye69fq625wu2myxey6uh9kzjy96'
const merit = await uut.getMerit(slpAddr)
assert.isNumber(merit)
} catch (err) {
+216
View File
@@ -0,0 +1,216 @@
/*
Unit tests for the Offer Use Case library.
*/
// Public npm libraries
const assert = require('chai').assert
const sinon = require('sinon')
// Local support libraries
// const testUtils = require('../../utils/test-utils')
// Unit under test (uut)
const EntryLib = require('../../../src/use-cases/entry')
const adapters = require('../mocks/adapters')
describe('#users-use-case', () => {
let uut
let sandbox
before(async () => {
// Delete all previous users in the database.
// await testUtils.deleteAllUsers()
})
beforeEach(() => {
sandbox = sinon.createSandbox()
uut = new EntryLib({ adapters })
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new EntryLib()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of adapters must be passed in when instantiating User Use Cases library.'
)
}
})
})
describe('#createEntry', () => {
it('should throw an error if entry is not provided', async () => {
try {
await uut.createEntry({})
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, "Property 'entry' must be a string!")
}
})
it('should throw an error if description is not provided', async () => {
try {
const inputData = {
entry: 'entry'
}
await uut.createEntry(inputData)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, "Property 'description' must be a string!")
}
})
it('should throw an error if slpAddress is not provided', async () => {
try {
const inputData = {
entry: 'entry',
description: 'test'
}
await uut.createEntry(inputData)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, "Property 'slpAddress' must be a string!")
}
})
it('should throw an error if signature is not provided', async () => {
try {
const inputData = {
entry: 'entry',
description: 'test',
slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg'
}
await uut.createEntry(inputData)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, "Property 'signature' must be a string!")
}
})
it('should throw an error if category is not provided', async () => {
try {
const inputData = {
entry: 'entry',
description: 'test',
slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg',
signature:
'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw='
}
await uut.createEntry(inputData)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, "Property 'category' must be a string!")
}
})
it('should catch and throw DB errors', async () => {
try {
// Force an error with the database.
sandbox.stub(uut, 'EntryModel').throws(new Error('test error'))
const inputData = {
entry: 'entry',
description: 'test',
slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg',
signature:
'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
category: 'test'
}
await uut.createEntry(inputData)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should throw error if signature is invalid', async () => {
try {
// Mocking bchjs functions
sandbox.stub(uut.bchjs, '_verifySignature').callsFake(() => {
return false
})
const inputData = {
entry: 'entry',
description: 'test',
slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg',
signature:
'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
category: 'test'
}
await uut.createEntry(inputData)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'Invalid signature')
}
})
it('should throw error for insufficient psf balance', async () => {
try {
// Mocking bchjs functions
sandbox.stub(uut.bchjs, 'getPSFTokenBalance').resolves(0)
const inputData = {
entry: 'entry',
description: 'test',
slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg',
signature:
'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
category: 'test'
}
await uut.createEntry(inputData)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'Insufficient psf balance')
}
})
it('should create a new entry in the DB', async () => {
// Mocking bchjs functions
// sandbox.stub(uut.bchjs, '_verifySignature').resolves(true)
// sandbox.stub(uut.bchjs, 'getPSFTokenBalance').resolves(100)
// sandbox.stub(uut.bchjs, 'getMerit').resolves(100)
const inputData = {
entry: 'entry',
description: 'test',
slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg',
signature:
'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
category: 'test'
}
const result = await uut.createEntry(inputData)
assert.property(result, 'entry')
assert.isString(result.entry)
assert.property(result, 'slpAddress')
assert.isString(result.slpAddress)
assert.property(result, 'description')
assert.isString(result.description)
assert.property(result, 'signature')
assert.isString(result.signature)
assert.property(result, 'category')
assert.isString(result.category)
assert.property(result, 'balance')
assert.isNumber(result.balance)
assert.property(result, 'merit')
assert.isNumber(result.merit)
})
})
})