mirror of
https://github.com/Permissionless-Software-Foundation/bch-dex.git
synced 2026-09-22 09:11:59 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a904cbb473 | ||
|
|
84dfd56918 | ||
|
|
d62fc3b575 | ||
|
|
a7f8a520a2 | ||
|
|
87b9046af8 | ||
|
|
a6b4677571 | ||
|
|
bf71810974 | ||
|
|
e68514f542 | ||
|
|
5fc26dcc7c | ||
|
|
fb00c1bc86 | ||
|
|
7964bfbae7 | ||
|
|
a13a1c91e8 | ||
|
|
b19d4da345 | ||
|
|
443f405f56 | ||
|
|
61bb5169b3 | ||
|
|
f812440555 | ||
|
|
693c98fb15 | ||
|
|
fdf73610eb | ||
|
|
b09e77c079 | ||
|
|
1a4c602234 | ||
|
|
dedc6e2d30 | ||
|
|
68ad2561b1 | ||
|
|
791d11e597 | ||
|
|
5489e76f55 | ||
|
|
496a1dd07c | ||
|
|
a7b8ca9ad5 | ||
|
|
6eb16eb11a | ||
|
|
d1fb676a8f |
Vendored
+5
-3
@@ -15,7 +15,7 @@ const ipfsCoordName = process.env.COORD_NAME
|
||||
|
||||
module.exports = {
|
||||
// Configure TCP port.
|
||||
port: process.env.PORT || 5002,
|
||||
port: process.env.PORT || 5700,
|
||||
|
||||
// Password for HTML UI that displays logs.
|
||||
logPass: 'test',
|
||||
@@ -37,6 +37,9 @@ module.exports = {
|
||||
? process.env.CONSUMER_URL
|
||||
: 'https://free-bch.fullstack.cash',
|
||||
|
||||
// P2WDB URL that will accept API calls from the p2wdb npm library.
|
||||
p2wdbUrl: process.env.P2WDB_URL ? process.env.P2WDB_URL : 'https://p2wdb.fullstack.cash',
|
||||
|
||||
// FullStack.cash account information, used for automatic JWT handling.
|
||||
getJwtAtStartup: process.env.GET_JWT_AT_STARTUP ? true : false,
|
||||
authServer: process.env.AUTHSERVER
|
||||
@@ -67,8 +70,7 @@ module.exports = {
|
||||
name: ipfsCoordName,
|
||||
version,
|
||||
protocol: 'generic-service',
|
||||
description:
|
||||
'This is a generic IPFS Serivice Provider that uses JSON RPC over IPFS to communicate with it. This instance has not been customized. Source code: https://github.com/Permissionless-Software-Foundation/ipfs-service-provider',
|
||||
description: 'This is a generic IPFS Serivice Provider that uses JSON RPC over IPFS to communicate with it. This instance has not been customized. Source code: https://github.com/Permissionless-Software-Foundation/ipfs-service-provider',
|
||||
documentation: 'https://ipfs-service-provider.fullstack.cash/',
|
||||
provider: {
|
||||
'@type': 'Organization',
|
||||
|
||||
Vendored
+1
-1
@@ -7,6 +7,6 @@
|
||||
module.exports = {
|
||||
session: 'secret-boilerplate-token',
|
||||
token: 'secret-jwt-token',
|
||||
database: 'mongodb://localhost:27017/swap-service-dev',
|
||||
database: 'mongodb://localhost:27017/bch-swap-service-dev',
|
||||
env: 'dev'
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -13,6 +13,6 @@ module.exports = {
|
||||
// database: 'mongodb://172.17.0.1:5555/ipfs-service-prod',
|
||||
database: process.env.DBURL
|
||||
? process.env.DBURL
|
||||
: 'mongodb://172.17.0.1:5555/swap-service-prod',
|
||||
: 'mongodb://172.17.0.1:5555/bch-swap-service-prod',
|
||||
env: 'prod'
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -7,6 +7,6 @@
|
||||
module.exports = {
|
||||
session: 'secret-boilerplate-token',
|
||||
token: 'secret-jwt-token',
|
||||
database: 'mongodb://localhost:27017/swap-service-test',
|
||||
database: 'mongodb://localhost:27017/bch-swap-service-test',
|
||||
env: 'test'
|
||||
}
|
||||
|
||||
+7
-7
@@ -10,16 +10,16 @@ There are three major pieces of software behind the bch-dex concept. They work t
|
||||
|
||||

|
||||
|
||||
- _Client_ could be a web browser, or a command-line client like [psf-bch-wallet](https://github.com/Permissionless-Software-Foundation/psf-bch-wallet) or [psf-avax-wallet](https://github.com/Permissionless-Software-Foundation/psf-avax-wallet).
|
||||
- _Client_ could be a web browser like [wallet.fullstack.cash](https://bchn-wallet.fullstack.cash), or a command-line client like [psf-bch-wallet](https://github.com/Permissionless-Software-Foundation/psf-bch-wallet) or [psf-avax-wallet](https://github.com/Permissionless-Software-Foundation/psf-avax-wallet).
|
||||
- [bch-dex](https://github.com/Permissionless-Software-Foundation/bch-dex) is the back end REST API that maintains a local database of information that the client reads from.
|
||||
- [P2WDB](https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service) is the pay-to-write global database with a REST API for interfacing with the other two pieces of software.
|
||||
|
||||
The arrows in the image represent the information flow between the three pieces of software:
|
||||
|
||||
- The _Client_ is essentially a 'dummy terminal' with a bidirectional interface to bch-dex. bch-dex does the heavy lifting, and the _Client_ is a 'thin' UI wrapper.
|
||||
- `bch-dex` imports data from the global P2WDB database into its local database, using a [webhook](https://en.wikipedia.org/wiki/Webhook) (dashed line). It can also custody funds, pay transaction fees, and create an _Offer_ by submitting the data to the P2WDB to generate an _Order_ (solid line).
|
||||
- `bch-dex` imports data from the global P2WDB database into its local database, using a [webhook](https://en.wikipedia.org/wiki/Webhook) (dashed line). It can also custody funds, pay transaction fees, and create an _Order_ by submitting the data to the P2WDB to generate an _Offer_ (solid line).
|
||||
|
||||
This architecture keeps the global database highly censorship resistant, while allowing local installations to maintain tight control over the user experience. The goal is to have many redundant copies of `bch-dex` on the network, and to empower individual traders to run their own, private copy.
|
||||
This architecture keeps the global database highly censorship resistant, while allowing local installations to maintain tight control over the user experience. The goal is to have many redundant copies of `bch-dex` on the network, and to empower individual traders to run their own, private copy, while maintaining a single source of truth via the P2WDB.
|
||||
|
||||
# Back End
|
||||
|
||||
@@ -47,15 +47,15 @@ These are just a brief, high-level overview. Review the [Specifications](./speci
|
||||
|
||||
## Writing to the Global Database
|
||||
|
||||
Adding data to the global P2WDB is triggered by the _Client_ calling a REST API endpoint on `bch-dex`. `bch-dex` [p2wdb npm library](https://www.npmjs.com/package/p2wdb) can be leveraged for easy reading and writing to the P2WDB.
|
||||
Adding data to the global P2WDB is triggered by the _Client_ calling a REST API endpoint on `bch-dex`. The [p2wdb npm library](https://www.npmjs.com/package/p2wdb) can be leveraged for easy reading and writing to the P2WDB.
|
||||
|
||||
Writing data follows these steps:
|
||||
|
||||
- Tokens and BCH are held by a wallet which is under the controlled of `bch-dex`.
|
||||
- The _Client_ submits data to the POST `/offer` REST API endpoint to create a new Offer.
|
||||
- `bch-dex` will move the funds into a segregated UTXO, and will use that UTXO to create an Offer. The Offer data is written to the P2WDB.
|
||||
- `bch-dex` will receive a webhook call to its POST `/order` endpoint by the P2WDB. This event will trigger the import of the new data into the apps local Mongo database, and generate a new Order model.
|
||||
- This webhook event is mirrored by every instance of `bch-dex` on the network. Each P2WDB peer on the network will independently validate the new database entry.
|
||||
- `bch-dex` will move the funds into a segregated UTXO, and will use that UTXO to create an Offer. The Offer data is written to the P2WDB. The Offer data is also saved to the local MongoDB.
|
||||
- After submitting the data to the P2WDB, `bch-dex` will receive a webhook call to its POST `/order` endpoint by the P2WDB. This event will trigger the import of the new data into the apps local Mongo database, and generate a new Order model.
|
||||
- This webhook event is mirrored by every instance of `bch-dex` on the network. Each P2WDB peer on the network will independently validate the new database entry and create a new Order model.
|
||||
|
||||
|
||||
## Reading from the Local Database
|
||||
|
||||
Generated
+4249
-12778
File diff suppressed because it is too large
Load Diff
+6
-4
@@ -9,7 +9,7 @@
|
||||
"test:all": "export BCH_DEX=test && nyc --reporter=text mocha --exit --timeout 15000 --recursive test/unit test/e2e/automated/",
|
||||
"test:unit": "export BCH_DEX=test && mocha --exit --timeout 15000 --recursive test/unit/",
|
||||
"test:e2e:auto": "export BCH_DEX=test && mocha --exit --timeout 15000 test/e2e/automated/",
|
||||
"test:integration": "export BCH_DEX=test && mocha --exit --timeout 30000 --recursive test/integration",
|
||||
"test:integration": "export BCH_DEX=test && mocha --exit --timeout 45000 --recursive test/integration",
|
||||
"test:temp": "export BCH_DEX=test && mocha --exit --timeout 15000 -g '#rate-limit' test/unit/json-rpc/",
|
||||
"lint": "standard --env mocha --fix",
|
||||
"docs": "./node_modules/.bin/apidoc -i src/ -o docs",
|
||||
@@ -27,13 +27,13 @@
|
||||
},
|
||||
"repository": "Permissionless-Software-Foundation/bch-dex",
|
||||
"dependencies": {
|
||||
"@psf/bch-js": "5.3.2",
|
||||
"@psf/bch-js": "5.4.0",
|
||||
"axios": "0.21.1",
|
||||
"bch-message-lib": "2.1.4",
|
||||
"bcryptjs": "2.4.3",
|
||||
"glob": "7.1.6",
|
||||
"ipfs": "0.60.0",
|
||||
"ipfs-coord": "6.8.12",
|
||||
"ipfs-coord": "7.1.6",
|
||||
"ipfs-http-client": "55.0.0",
|
||||
"jsonrpc-lite": "2.2.0",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
@@ -51,10 +51,11 @@
|
||||
"koa2-ratelimit": "0.9.1",
|
||||
"libp2p": "^0.36.2",
|
||||
"line-reader": "0.4.0",
|
||||
"minimal-slp-wallet": "../minimal-slp-wallet",
|
||||
"minimal-slp-wallet": "4.4.3",
|
||||
"mongoose": "5.13.13",
|
||||
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
|
||||
"nodemailer": "6.4.17",
|
||||
"p2wdb": "1.3.1",
|
||||
"passport-local": "1.0.0",
|
||||
"public-ip": "^4.0.4",
|
||||
"winston": "3.3.3",
|
||||
@@ -62,6 +63,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"apidoc": "0.26.0",
|
||||
"bch-token-sweep": "1.5.16",
|
||||
"chai": "4.3.0",
|
||||
"coveralls": "2.11.4",
|
||||
"eslint": "7.19.0",
|
||||
|
||||
@@ -18,7 +18,7 @@ const JSONFiles = require('./json-files')
|
||||
const FullStackJWT = require('./fullstack-jwt')
|
||||
const BCHAdapter = require('./bch')
|
||||
const WalletAdapter = require('./wallet')
|
||||
const P2wdbAdapter = require('./p2wdb')
|
||||
const P2wdbAdapter = require('./p2wdb-adapter')
|
||||
|
||||
//
|
||||
// // Instantiate adapter libraries.
|
||||
@@ -52,10 +52,11 @@ class Adapters {
|
||||
this.nodemailer = new Nodemailer()
|
||||
this.jsonFiles = new JSONFiles()
|
||||
this.bchjs = new BCHJS()
|
||||
localConfig.bchjs = this.bchjs
|
||||
this.bch = new BCHAdapter()
|
||||
this.config = config
|
||||
this.wallet = new WalletAdapter()
|
||||
this.p2wdb = new P2wdbAdapter()
|
||||
this.p2wdb = new P2wdbAdapter(localConfig)
|
||||
|
||||
// Get a valid JWT API key and instance bch-js.
|
||||
this.fullStackJwt = new FullStackJWT(config)
|
||||
|
||||
@@ -91,7 +91,7 @@ class IpfsCoordAdapter {
|
||||
attachRPCRouter (router) {
|
||||
try {
|
||||
_this.ipfsCoord.privateLog = router
|
||||
_this.ipfsCoord.adapters.orbit.privateLog = router
|
||||
_this.ipfsCoord.adapters.pubsub.privateLog = router
|
||||
} catch (err) {
|
||||
console.error('Error in attachRPCRouter()')
|
||||
throw err
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
const Users = require('./models/users')
|
||||
const Entries = require('./models/entry')
|
||||
const Order = require('./models/order')
|
||||
const Offer = require('./models/offer')
|
||||
|
||||
class LocalDB {
|
||||
constructor () {
|
||||
@@ -13,6 +14,7 @@ class LocalDB {
|
||||
this.Users = Users
|
||||
this.Entry = Entries
|
||||
this.Order = Order
|
||||
this.Offer = Offer
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ const Offer = new mongoose.Schema({
|
||||
utxoTxid: { type: String },
|
||||
utxoVout: { type: Number },
|
||||
numTokens: { type: Number },
|
||||
hdIndex: { type: Number }, // HD index address holding the UTXO for this offer.
|
||||
|
||||
//
|
||||
offerIpfsId: { type: String },
|
||||
|
||||
@@ -17,7 +17,8 @@ const Order = new mongoose.Schema({
|
||||
timestamp: { type: String },
|
||||
localTimestamp: { type: String },
|
||||
p2wdbTxid: { type: String },
|
||||
p2wdbHash: { type: String }
|
||||
p2wdbHash: { type: String },
|
||||
orderStatus: { type: String }
|
||||
})
|
||||
|
||||
module.exports = mongoose.model('order', Order)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
Adapter library for interacting with the P2WDB
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
const axios = require('axios')
|
||||
const { Write, Read } = require('p2wdb/index')
|
||||
|
||||
// Local libraries
|
||||
const config = require('../../config')
|
||||
|
||||
class P2wdbAdapter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency injection
|
||||
this.bchjs = localConfig.bchjs
|
||||
if (!this.bchjs) {
|
||||
throw new Error('Must pass an instance of bch-js when instantiating p2wdb.js adapter.')
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.axios = axios
|
||||
this.config = config
|
||||
this.Write = Write
|
||||
this.Read = Read
|
||||
|
||||
// Allow the localConfig to overwrite the config file values.
|
||||
this.p2wdbURL = localConfig.p2wdbURL || config.p2wdbUrl
|
||||
}
|
||||
|
||||
// Write some data to the P2WDB
|
||||
async write (inputObj = {}) {
|
||||
try {
|
||||
const { appId = 'test', data = {}, wif } = inputObj
|
||||
|
||||
if (typeof wif !== 'string' || !wif) {
|
||||
throw new Error('wif input must be a private key starting with the letter L or K')
|
||||
}
|
||||
|
||||
const p2write = this.instantiateWriteLib(wif)
|
||||
|
||||
const result = await p2write.postEntry(data, appId)
|
||||
|
||||
return result.hash
|
||||
} catch (err) {
|
||||
console.error('Error in p2wdb.js/write()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Instantiate the Write library with a WIF, handling the various config
|
||||
// settings.
|
||||
instantiateWriteLib (wif) {
|
||||
try {
|
||||
// Object used to configure the Write library.
|
||||
let configObj = {}
|
||||
|
||||
if (this.config.useFullStackCash) {
|
||||
// Use web 2 infrastructure.
|
||||
// bch-js and FullStack.cash
|
||||
|
||||
configObj = {
|
||||
wif,
|
||||
serverURL: this.p2wdbURL,
|
||||
restURL: this.bchjs.restURL,
|
||||
apiToken: this.bchjs.apiToken
|
||||
}
|
||||
} else {
|
||||
// Use web 3 infrastructure.
|
||||
// ipfs-bch-wallet-consumer and bch-consumer library
|
||||
|
||||
configObj = {
|
||||
wif,
|
||||
serverURL: this.p2wdbURL,
|
||||
interface: 'consumer-api',
|
||||
restURL: this.config.consumerUrl
|
||||
}
|
||||
}
|
||||
|
||||
const write = new this.Write(configObj)
|
||||
|
||||
return write
|
||||
} catch (err) {
|
||||
console.error('Error in p2wdb-adapter.js/instantitateWriteLib()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a promise that resolves to true or false, to indicate if the
|
||||
// WIF controls the required funds to write to the P2WDB.
|
||||
async checkForSufficientFunds (wif) {
|
||||
try {
|
||||
if (typeof wif !== 'string' || !wif) {
|
||||
throw new Error('invalid wif to check for funds')
|
||||
}
|
||||
|
||||
// Instatiate the Write library
|
||||
const p2write = this.instantiateWriteLib(wif)
|
||||
|
||||
return await p2write.checkForSufficientFunds()
|
||||
} catch (err) {
|
||||
console.error('Error in p2wdb.js/checkForSufficientFunds()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = P2wdbAdapter
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
Adapter library for interacting with the P2WDB
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
const axios = require('axios')
|
||||
|
||||
// Local libraries
|
||||
const config = require('../../config')
|
||||
|
||||
// Global constants
|
||||
let P2WDB_SERVER = 'http://localhost:5001/entry/write'
|
||||
// const P2WDB_SERVER = 'https://p2wdb.fullstack.cash/entry/write'
|
||||
|
||||
class P2wdbAdapter {
|
||||
constructor (localConfig = {}) {
|
||||
// Encapsulate dependencies
|
||||
this.axios = axios
|
||||
this.config = config
|
||||
|
||||
P2WDB_SERVER = `http://localhost:${config.p2wdbPort}/entry/write`
|
||||
}
|
||||
|
||||
async write (inputObj) {
|
||||
try {
|
||||
const { txid, signature, message, appId, data } = inputObj
|
||||
|
||||
// TODO: Input validation
|
||||
|
||||
const now = new Date()
|
||||
|
||||
const dataObj = {
|
||||
appId,
|
||||
data,
|
||||
timestamp: now.toISOString(),
|
||||
localTimeStamp: now.toLocaleString()
|
||||
}
|
||||
|
||||
const bodyData = {
|
||||
txid,
|
||||
message,
|
||||
signature,
|
||||
data: JSON.stringify(dataObj)
|
||||
}
|
||||
|
||||
const result = await this.axios.post(P2WDB_SERVER, bodyData)
|
||||
// console.log(`Response from API: ${JSON.stringify(result.data, null, 2)}`)
|
||||
|
||||
return result.data.hash
|
||||
} catch (err) {
|
||||
console.error('Error in p2wdb.js/write()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = P2wdbAdapter
|
||||
+114
-46
@@ -10,9 +10,9 @@ const JsonFiles = require('./json-files')
|
||||
const config = require('../../config')
|
||||
|
||||
const WALLET_FILE = `${__dirname.toString()}/../../wallet.json`
|
||||
const PROOF_OF_BURN_QTY = 0.01
|
||||
const P2WDB_TOKEN_ID =
|
||||
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0'
|
||||
// const PROOF_OF_BURN_QTY = 0.01
|
||||
// const P2WDB_TOKEN_ID =
|
||||
// '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0'
|
||||
|
||||
class WalletAdapter {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -178,62 +178,130 @@ class WalletAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
// Burn enough PSF to generate a valide proof-of-burn for writing to the P2WDB.
|
||||
async burnPsf () {
|
||||
// Generate a partial transcation to *take* a 'sell' order.
|
||||
async generatePartialTx (orderInfo) {
|
||||
try {
|
||||
// TODO: Throw error if this.bchWallet has not been instantiated.
|
||||
console.log(`orderInfo: ${JSON.stringify(orderInfo, null, 2)}`)
|
||||
|
||||
// console.log('walletData: ', walletData)
|
||||
// console.log(
|
||||
// `walletData.utxos.utxoStore.slpUtxos: ${JSON.stringify(
|
||||
// walletData.utxos.utxoStore.slpUtxos,
|
||||
// null,
|
||||
// 2,
|
||||
// )}`,
|
||||
// )
|
||||
const bchjs = this.bchWallet.bchjs
|
||||
|
||||
// Get token UTXOs held by the wallet.
|
||||
const tokenUtxos = this.bchWallet.utxos.utxoStore.slpUtxos.type1.tokens
|
||||
// console.log(`tokenUtxos: ${JSON.stringify(tokenUtxos, null, 2)}`)
|
||||
// instance of transaction builder
|
||||
const transactionBuilder = new bchjs.TransactionBuilder()
|
||||
|
||||
// Find a token UTXO that contains PSF with a quantity higher than needed
|
||||
// to generate a proof-of-burn.
|
||||
let tokenUtxo = {}
|
||||
for (let i = 0; i < tokenUtxos.length; i++) {
|
||||
const thisUtxo = tokenUtxos[i]
|
||||
// Get a payment UTXO.
|
||||
// TODO: Create a segregated UTXO.
|
||||
const bchUtxos = this.bchWallet.utxos.utxoStore.bchUtxos
|
||||
const paymentUtxo = await bchjs.Utxo.findBiggestUtxo(bchUtxos)
|
||||
console.log('paymentUtxo: ', paymentUtxo)
|
||||
|
||||
// If token ID matches.
|
||||
if (thisUtxo.tokenId === P2WDB_TOKEN_ID) {
|
||||
if (parseFloat(thisUtxo.qtyStr) >= PROOF_OF_BURN_QTY) {
|
||||
tokenUtxo = thisUtxo
|
||||
break
|
||||
}
|
||||
}
|
||||
// Get token info on the offered UTXO
|
||||
const txData = await this.bchWallet.getTxData([orderInfo.utxoTxid])
|
||||
console.log(`txData: ${JSON.stringify(txData, null, 2)}`)
|
||||
|
||||
// Construct the UTXO being offered for sale.
|
||||
const offeredUtxo = {
|
||||
txid: orderInfo.utxoTxid,
|
||||
vout: orderInfo.utxoVout,
|
||||
tokenId: orderInfo.tokenId,
|
||||
decimals: txData[0].tokenDecimals,
|
||||
tokenQty: orderInfo.numTokens.toString()
|
||||
}
|
||||
console.log(`offeredUtxo: ${JSON.stringify(offeredUtxo, null, 2)}`)
|
||||
|
||||
if (tokenUtxo.tokenId !== P2WDB_TOKEN_ID) {
|
||||
throw new Error(
|
||||
`Token UTXO of with ID of ${P2WDB_TOKEN_ID} and quantity greater than ${PROOF_OF_BURN_QTY} could not be found in wallet.`
|
||||
)
|
||||
}
|
||||
// console.log(`tokenUtxo: ${JSON.stringify(tokenUtxo, null, 2)}`)
|
||||
|
||||
const result = await this.bchWallet.burnTokens(
|
||||
PROOF_OF_BURN_QTY,
|
||||
P2WDB_TOKEN_ID
|
||||
// Build First part of the collaborative Tx a.k.a. Alice
|
||||
// Generate the OP_RETURN code.
|
||||
const slpSendObj = bchjs.SLP.TokenType1.generateSendOpReturn(
|
||||
[offeredUtxo],
|
||||
orderInfo.numTokens.toString()
|
||||
)
|
||||
// console.log('walletData.burnTokens() result: ', result)
|
||||
const slpData = slpSendObj.script
|
||||
console.log(`slpOutputs: ${slpSendObj.outputs}`)
|
||||
|
||||
return result
|
||||
// Currently this app only supports a single SLP token UTXO for exact
|
||||
// token quantities (no token change). e.g. 1 UTXO representing the
|
||||
// exact number of 'numTokens'
|
||||
if (slpSendObj.outputs > 1) {
|
||||
console.log('WARNING: choose one UTXO with all tokens to exchange')
|
||||
return
|
||||
}
|
||||
|
||||
// return {
|
||||
// success: true,
|
||||
// txid: 'fakeTxid',
|
||||
// }
|
||||
// Calculate sats needed to pay the offer.
|
||||
const satsNeeded = orderInfo.numTokens * parseInt(orderInfo.rateInSats)
|
||||
|
||||
// Calculate miner fees.
|
||||
// Get byte count (minimum 2 inputs, 3 outputs)
|
||||
const opReturnBufLength = slpData.byteLength + 32 // add padding
|
||||
const byteCount =
|
||||
bchjs.BitcoinCash.getByteCount({ P2PKH: 2 }, { P2PKH: 4 }) +
|
||||
opReturnBufLength
|
||||
const totalSatsNeeded = byteCount + satsNeeded
|
||||
// console.log(`satoshis needed: ${satsNeeded}`)
|
||||
|
||||
// One last check to ensure the app wallet has enough BCH to complete
|
||||
// the trade.
|
||||
if (totalSatsNeeded > paymentUtxo.value) {
|
||||
console.log(`Selected payment UTXO is not big enough. Sats needed: ${totalSatsNeeded}, UTXO value: ${paymentUtxo.value}`)
|
||||
}
|
||||
|
||||
// add UTXO for sell(STILL CANNOT SPEND - not signed yet)
|
||||
transactionBuilder.addInput(orderInfo.utxoTxid, orderInfo.utxoVout)
|
||||
|
||||
// add payment UTXO
|
||||
transactionBuilder.addInput(paymentUtxo.tx_hash, paymentUtxo.tx_pos)
|
||||
|
||||
const originalAmount = paymentUtxo.value
|
||||
const dust = 546
|
||||
const remainder = originalAmount - satsNeeded - dust // exchange fee + token UTXO dust
|
||||
|
||||
// Add the SLP OP_RETURN data as the first output.
|
||||
transactionBuilder.addOutput(slpData, 0)
|
||||
|
||||
const buyerAddr = this.bchWallet.walletInfo.legacyAddress
|
||||
// console.log(`buyAddr: ${JSON.stringify(buyAddr, null, 2)}`)
|
||||
|
||||
// Send dust transaction representing tokens being sent.
|
||||
transactionBuilder.addOutput(
|
||||
buyerAddr,
|
||||
dust
|
||||
)
|
||||
|
||||
// Get seller address
|
||||
// TODO: Seller should explicitly define the address to send to in the
|
||||
// orderInfo object. Right now it retrieves the address from the UTXO
|
||||
// for sale, which is not ideal.
|
||||
const sellerAddr = txData[0].vout[1].scriptPubKey.addresses[0]
|
||||
|
||||
// Send payment to the offer side
|
||||
transactionBuilder.addOutput(sellerAddr, satsNeeded)
|
||||
|
||||
// Send the BCH change back to the buyer
|
||||
if (remainder > 550) {
|
||||
transactionBuilder.addOutput(buyerAddr, remainder)
|
||||
}
|
||||
|
||||
const buyerECPair = bchjs.ECPair.fromWIF(this.bchWallet.walletInfo.privateKey)
|
||||
|
||||
// Sign the buyers input UTXO for spending.
|
||||
transactionBuilder.sign(
|
||||
1,
|
||||
buyerECPair,
|
||||
null,
|
||||
transactionBuilder.hashTypes.SIGHASH_ALL,
|
||||
originalAmount
|
||||
)
|
||||
|
||||
const tx = transactionBuilder.transaction.buildIncomplete()
|
||||
|
||||
const hex = tx.toHex()
|
||||
console.log('hex: ', hex)
|
||||
|
||||
return hex
|
||||
} catch (err) {
|
||||
console.error('Error in burnPsf(): ', err)
|
||||
console.error('Error in wallet.js/generatePartialTx(): ', err)
|
||||
throw err
|
||||
}
|
||||
|
||||
// return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
/*
|
||||
This is the JSON RPC router for the users API
|
||||
|
||||
CT 3/5/22: This library can probably be deleted. Handling of the /about endpoint
|
||||
is now directly controlled by the ipfs-coord library.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
@@ -33,6 +36,8 @@ class AboutRPC {
|
||||
// This is a bit different than other router libraries, because there is
|
||||
// only one response, which is a string about this node.
|
||||
async aboutRouter (rpcData) {
|
||||
console.log('debugging: aboutRouter from ipfs-service-provider triggered')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status: 200,
|
||||
|
||||
@@ -49,6 +49,39 @@ class OrderRESTControllerLib {
|
||||
}
|
||||
}
|
||||
|
||||
// curl -X GET http://localhost:5700/order/list
|
||||
async listOrders (ctx) {
|
||||
try {
|
||||
const orders = await _this.useCases.order.listOrders()
|
||||
|
||||
ctx.body = orders
|
||||
} catch (err) {
|
||||
console.log('Error in listOrders REST API handler.')
|
||||
_this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Currently only supports 'sell' orders, and will only buy the 'numTokens'
|
||||
// listed in the order.
|
||||
async takeOrder (ctx) {
|
||||
try {
|
||||
console.log('body: ', ctx.request.body)
|
||||
|
||||
const orderCid = ctx.request.body.orderCid
|
||||
|
||||
// Find the Order.
|
||||
// const orderEntity = await _this.useCases.order.findOrder(orderId)
|
||||
|
||||
// 'Take' the Order.
|
||||
const hash = await _this.useCases.order.takeOrder(orderCid)
|
||||
|
||||
ctx.body = { hash }
|
||||
} catch (err) {
|
||||
console.log('Error in takeOrder REST API handler.')
|
||||
_this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, err) {
|
||||
console.log('err', err.message)
|
||||
|
||||
@@ -50,6 +50,8 @@ class OrderRouter {
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.post('/', _this.orderRESTController.createOrder)
|
||||
this.router.get('/list', _this.orderRESTController.listOrders)
|
||||
this.router.post('/take', _this.orderRESTController.takeOrder)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(_this.router.routes())
|
||||
|
||||
+10
-12
@@ -4,6 +4,10 @@
|
||||
It's destroyed when the UTXO described in the Signal has been detected as spent.
|
||||
*/
|
||||
class OrderEntity {
|
||||
constructor () {
|
||||
this.orderStatus = ['posted', 'taken', 'completed']
|
||||
}
|
||||
|
||||
validate (orderData = {}) {
|
||||
// Throw an error if input object does not have a data property
|
||||
if (!orderData.data) {
|
||||
@@ -12,17 +16,7 @@ class OrderEntity {
|
||||
)
|
||||
}
|
||||
|
||||
const {
|
||||
messageType,
|
||||
messageClass,
|
||||
tokenId,
|
||||
buyOrSell,
|
||||
rateInSats,
|
||||
minSatsToExchange,
|
||||
numTokens,
|
||||
utxoTxid,
|
||||
utxoVout
|
||||
} = orderData.data
|
||||
const { messageType, messageClass, tokenId, buyOrSell, rateInSats, minSatsToExchange, numTokens, utxoTxid, utxoVout, orderStatus } = orderData.data
|
||||
|
||||
// Input Validation
|
||||
if (!messageType || typeof messageType !== 'number') {
|
||||
@@ -52,6 +46,9 @@ class OrderEntity {
|
||||
if (typeof utxoVout !== 'number') {
|
||||
throw new Error("Property 'utxoVout' must be an integer number.")
|
||||
}
|
||||
if (orderStatus && !this.orderStatus.includes(orderStatus)) {
|
||||
throw new Error("Property 'orderStatus' must be a valid string")
|
||||
}
|
||||
|
||||
const validatedOrderData = {
|
||||
messageType,
|
||||
@@ -66,7 +63,8 @@ class OrderEntity {
|
||||
timestamp: orderData.timestamp,
|
||||
localTimestamp: orderData.localTimeStamp,
|
||||
txid: orderData.txid,
|
||||
p2wdbHash: orderData.hash
|
||||
p2wdbHash: orderData.hash,
|
||||
orderStatus: orderStatus || this.orderStatus[0]
|
||||
}
|
||||
|
||||
return validatedOrderData
|
||||
|
||||
+25
-23
@@ -47,30 +47,23 @@ class OfferLib {
|
||||
// Update the offer with the new UTXO information.
|
||||
offerEntity.utxoTxid = utxoInfo.txid
|
||||
offerEntity.utxoVout = utxoInfo.vout
|
||||
|
||||
// Burn PSF token to pay for P2WDB write.
|
||||
const txid = await this.adapters.wallet.burnPsf()
|
||||
console.log('burn txid: ', txid)
|
||||
console.log(`https://simpleledger.info/tx/${txid}`)
|
||||
|
||||
// generate signature.
|
||||
const now = new Date()
|
||||
const message = now.toISOString()
|
||||
const signature = await this.adapters.wallet.generateSignature(message)
|
||||
// console.log('signature: ', signature)
|
||||
|
||||
const p2wdbObj = {
|
||||
txid,
|
||||
signature,
|
||||
message,
|
||||
appId: this.config.p2wdbAppId,
|
||||
data: offerEntity
|
||||
}
|
||||
offerEntity.hdIndex = utxoInfo.hdIndex
|
||||
|
||||
// Add offer to P2WDB.
|
||||
const p2wdbObj = {
|
||||
wif: this.adapters.wallet.bchWallet.walletInfo.privateKey,
|
||||
data: offerEntity,
|
||||
appId: this.config.p2wdbAppId
|
||||
}
|
||||
const hash = await this.adapters.p2wdb.write(p2wdbObj)
|
||||
// console.log('hash: ', hash)
|
||||
|
||||
// Create a MongoDB model to hold the Offer
|
||||
offerEntity.p2wdbHash = hash
|
||||
console.log(`creating new offer model: ${JSON.stringify(offerEntity, null, 2)}`)
|
||||
const offer = new this.OfferModel(offerEntity)
|
||||
await offer.save()
|
||||
|
||||
return hash
|
||||
} catch (err) {
|
||||
// console.log("Error in use-cases/entry.js/createEntry()", err.message)
|
||||
@@ -97,7 +90,8 @@ class OfferLib {
|
||||
|
||||
const utxoInfo = {
|
||||
txid,
|
||||
vout: 0
|
||||
vout: 1,
|
||||
hdIndex: keyPair.hdIndex
|
||||
}
|
||||
|
||||
return utxoInfo
|
||||
@@ -112,6 +106,12 @@ class OfferLib {
|
||||
async ensureFunds (offerEntity) {
|
||||
try {
|
||||
// console.log('this.adapters.wallet: ', this.adapters.wallet.bchWallet)
|
||||
// console.log(`walletInfo: ${JSON.stringify(this.adapters.wallet.bchWallet.walletInfo, null, 2)}`)
|
||||
|
||||
// Ensure the app wallet has enough funds to write to the P2WDB.
|
||||
const wif = this.adapters.wallet.bchWallet.walletInfo.privateKey
|
||||
const canWriteToP2WDB = await this.adapters.p2wdb.checkForSufficientFunds(wif)
|
||||
if (!canWriteToP2WDB) throw new Error('App wallet does not have funds for writing to the P2WDB.')
|
||||
|
||||
// Get UTXOs.
|
||||
const utxos = this.adapters.wallet.bchWallet.utxos.utxoStore
|
||||
@@ -129,16 +129,18 @@ class OfferLib {
|
||||
// Get the total amount of tokens in the wallet that match the token
|
||||
// in the offer.
|
||||
let totalTokenBalance = 0
|
||||
tokenUtxos.map(x => (totalTokenBalance += parseFloat(x.tokenQty)))
|
||||
// console.log('totalTokenBalance: ', totalTokenBalance)
|
||||
tokenUtxos.map(x => (totalTokenBalance += parseFloat(x.qtyStr)))
|
||||
console.log('totalTokenBalance: ', totalTokenBalance)
|
||||
|
||||
// If there are fewer tokens in the wallet than what's in the offer,
|
||||
// throw an error.
|
||||
if (totalTokenBalance <= offerEntity.numTokens) {
|
||||
if (totalTokenBalance <= offerEntity.numTokens || isNaN(totalTokenBalance)) {
|
||||
throw new Error(
|
||||
'App wallet does not have enough tokens to satisfy the SELL offer.'
|
||||
)
|
||||
}
|
||||
|
||||
//
|
||||
} else {
|
||||
// Buy Offer
|
||||
throw new Error('Buy orders are not supported yet.')
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
local Offers are no different than Orders generated by other peers.
|
||||
*/
|
||||
|
||||
// Local libraries
|
||||
const OrderEntity = require('../../entities/order')
|
||||
const config = require('../../../config')
|
||||
|
||||
class OrderUseCases {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -20,6 +22,9 @@ class OrderUseCases {
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.config = config
|
||||
|
||||
this.orderEntity = new OrderEntity()
|
||||
this.OrderModel = this.adapters.localdb.Order
|
||||
}
|
||||
@@ -43,6 +48,9 @@ class OrderUseCases {
|
||||
console.log('utxoStatus: ', utxoStatus)
|
||||
if (utxoStatus === null) return false
|
||||
|
||||
// A new order gets a status of 'posted'
|
||||
orderObj.data.orderStatus = 'posted'
|
||||
|
||||
const orderEntity = this.orderEntity.validate(orderObj)
|
||||
console.log('orderEntity: ', orderEntity)
|
||||
|
||||
@@ -56,6 +64,142 @@ class OrderUseCases {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async listOrders () {
|
||||
try {
|
||||
return this.OrderModel.find({})
|
||||
} catch (error) {
|
||||
console.error('Error in use-cases/order/listOrders()')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Generate phase 2 of 3 - take the other side of an Order.
|
||||
// Based on this example:
|
||||
// https://github.com/Permissionless-Software-Foundation/bch-js-examples/blob/master/bch/applications/collaborate/sell-slp/e2e-exchange/step2-purchase-tx.js
|
||||
async takeOrder (orderCid) {
|
||||
try {
|
||||
console.log('orderCid: ', orderCid)
|
||||
|
||||
// Get the Order information
|
||||
const orderInfo = await this.findOrderByHash(orderCid)
|
||||
console.log(`orderInfo: ${JSON.stringify(orderInfo, null, 2)}`)
|
||||
|
||||
// Ensure the order is in a 'posted' state and not already 'taken'
|
||||
if (orderInfo.orderStatus && orderInfo.orderStatus !== 'posted') {
|
||||
throw new Error('order already taken')
|
||||
}
|
||||
|
||||
// Verify that UTXO for sale is unspent. Abort if it's been spent.
|
||||
const txid = orderInfo.utxoTxid
|
||||
const vout = orderInfo.utxoVout
|
||||
const utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut(
|
||||
txid,
|
||||
vout
|
||||
)
|
||||
console.log('utxoStatus: ', utxoStatus)
|
||||
if (utxoStatus === null) {
|
||||
console.log(`utxo txid: ${txid}, vout: ${vout}`)
|
||||
throw new Error('UTXO does not exist. Aborting.')
|
||||
}
|
||||
|
||||
// Ensure the app has enough funds to complete the trade.
|
||||
await this.ensureFunds(orderInfo)
|
||||
|
||||
// Get UTXOs.
|
||||
const utxos = this.adapters.wallet.bchWallet.utxos.utxoStore
|
||||
console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
||||
|
||||
// TODO: Move funds to create a segrated UTXO for taking the order
|
||||
|
||||
// Create a partially signed transaction.
|
||||
// https://github.com/Permissionless-Software-Foundation/bch-js-examples/blob/master/bch/applications/collaborate/sell-slp/e2e-exchange/step2-purchase-tx.js#L59
|
||||
const partialTxHex = await this.adapters.wallet.generatePartialTx(orderInfo)
|
||||
// return partialTxHex
|
||||
|
||||
// Create valid Order object
|
||||
const takenOrderInfo = Object.assign({}, orderInfo)
|
||||
takenOrderInfo.patialTxHex = partialTxHex
|
||||
delete takenOrderInfo.p2wdbHash
|
||||
delete takenOrderInfo._id
|
||||
takenOrderInfo.offerHash = orderInfo.p2wdbHash
|
||||
|
||||
// Write order info to the P2WDB
|
||||
// TODO: This will trigger the webhook. Find some way of triggering the
|
||||
// webhook on new orders, but not on counteroffers
|
||||
const p2wdbObj = {
|
||||
wif: this.adapters.wallet.bchWallet.walletInfo.privateKey,
|
||||
data: takenOrderInfo,
|
||||
appId: this.config.p2wdbAppId
|
||||
}
|
||||
const hash = await this.adapters.p2wdb.write(p2wdbObj)
|
||||
|
||||
// Return the P2WDB CID
|
||||
return hash
|
||||
} catch (err) {
|
||||
console.error('Error in use-cases/order/takeOrder(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that the wallet has enough BCH and tokens to complete the requested
|
||||
// trade. Will return true if it does. Will throw an error if it doesn't.
|
||||
async ensureFunds (orderEntity) {
|
||||
try {
|
||||
// console.log('this.adapters.wallet: ', this.adapters.wallet.bchWallet)
|
||||
// console.log(`walletInfo: ${JSON.stringify(this.adapters.wallet.bchWallet.walletInfo, null, 2)}`)
|
||||
|
||||
await this.adapters.wallet.bchWallet.walletInfoPromise
|
||||
|
||||
// Ensure the app wallet has enough funds to write to the P2WDB.
|
||||
const wif = this.adapters.wallet.bchWallet.walletInfo.privateKey
|
||||
const canWriteToP2WDB = await this.adapters.p2wdb.checkForSufficientFunds(wif)
|
||||
if (!canWriteToP2WDB) throw new Error('App wallet does not have funds for writing to the P2WDB.')
|
||||
|
||||
if (orderEntity.buyOrSell.includes('sell')) {
|
||||
// Sell Offer
|
||||
|
||||
// Ensure the app wallet controlls enough BCH to pay for the tokens.
|
||||
const satsNeeded = orderEntity.numTokens * parseInt(orderEntity.rateInSats)
|
||||
const balance = await this.adapters.wallet.bchWallet.getBalance()
|
||||
console.log(`wallet balance: ${balance}, sats needed: ${satsNeeded}`)
|
||||
const SATS_MARGIN = 5000
|
||||
if (satsNeeded + SATS_MARGIN > balance) { throw new Error('App wallet does not control enough BCH to purchase the tokens.') }
|
||||
|
||||
//
|
||||
} else {
|
||||
// Buy Offer
|
||||
throw new Error('Buy orders are not supported yet.')
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Error in ensureFunds()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async findOrderByHash (p2wdbHash) {
|
||||
try {
|
||||
if (typeof p2wdbHash !== 'string' || !p2wdbHash) {
|
||||
throw new Error('p2wdbHash must be a string')
|
||||
}
|
||||
|
||||
const order = await this.OrderModel.findOne({ p2wdbHash })
|
||||
|
||||
if (!order) {
|
||||
throw new Error('order not found')
|
||||
}
|
||||
|
||||
const orderObject = order.toObject()
|
||||
// return this.orderEntity.validateFromModel(orderObject)
|
||||
|
||||
return orderObject
|
||||
} catch (err) {
|
||||
console.error('Error in findOrder(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OrderUseCases
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
const axios = require('axios')
|
||||
|
||||
const LOCALHOST = 'http://localhost:5002'
|
||||
const LOCALHOST = 'http://localhost:5700'
|
||||
|
||||
async function start () {
|
||||
try {
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
Part 2 of 3: Take an order
|
||||
*/
|
||||
|
||||
const axios = require('axios')
|
||||
|
||||
const LOCALHOST = 'http://localhost:5700'
|
||||
|
||||
async function start () {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/order/take`,
|
||||
data: {
|
||||
orderCid: 'zdpuAkp98gTuivaNzGP31jTQi3ADXrFA6uANrceQcrQkTXy2j'
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
console.log('result.data: ', result.data)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
start()
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Integration tests for hte p2wdb-adapter.js library
|
||||
|
||||
These integration tests may not necessarily pass. They require a WIF set
|
||||
as an environment variable, and loaded with enough BCH and PSF to make
|
||||
a write to the P2WDB.
|
||||
|
||||
If these tests fail, it does not necessarily mean there is a problem with the
|
||||
code. Check that the WIF envrionment variable meets the requirements.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
|
||||
// Local libraries
|
||||
const P2wdbAdapter = require('../../../src/adapters/p2wdb-adapter')
|
||||
|
||||
const wif = process.env.WIF
|
||||
if (!wif) {
|
||||
throw new Error('You must provide a BCH WIF private key as an environment variable to run this test.')
|
||||
}
|
||||
|
||||
describe('#p2wdb-adapter.js', () => {
|
||||
let uut
|
||||
|
||||
beforeEach(() => {
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
uut = new P2wdbAdapter({ bchjs })
|
||||
})
|
||||
|
||||
describe('#checkForSufficientFunds', () => {
|
||||
it('should check for sufficient funds', async () => {
|
||||
const result = await uut.checkForSufficientFunds(wif)
|
||||
|
||||
console.log('Provided WIF has funds for making a write: ', result)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#write', () => {
|
||||
it('should write data to the P2WDB', async () => {
|
||||
const inObj = {
|
||||
appId: 'test234',
|
||||
data: {
|
||||
key: 'value'
|
||||
},
|
||||
wif
|
||||
}
|
||||
|
||||
const result = await uut.write(inObj)
|
||||
console.log('result: ', result)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
Integration tests for the offer-use-case.js library
|
||||
|
||||
For these tests to pass, the app wallet must have Trout test tokens with
|
||||
a token ID of a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const assert = require('chai').assert
|
||||
|
||||
// Local npm libraries
|
||||
const Adapters = require('../../../src/adapters')
|
||||
const Offer = require('../../../src/use-cases/offer')
|
||||
|
||||
describe('#offer-use-case.js', () => {
|
||||
let uut
|
||||
|
||||
before(async () => {
|
||||
const adapters = new Adapters()
|
||||
adapters.config.getJwtAtStartup = false
|
||||
await adapters.start()
|
||||
|
||||
uut = new Offer({ adapters })
|
||||
})
|
||||
|
||||
describe('#ensureFunds', () => {
|
||||
it('should throw error if wallet does not have enough funds', async () => {
|
||||
try {
|
||||
const offerEntity = {
|
||||
lokadId: 'SWP',
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
tokenId: 'token-id',
|
||||
buyOrSell: 'sell',
|
||||
rateInSats: 1000,
|
||||
minSatsToExchange: 1250,
|
||||
numTokens: 1
|
||||
}
|
||||
|
||||
await uut.ensureFunds(offerEntity)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'App wallet does not have enough tokens to satisfy the SELL offer.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return true if wallet has enough funds', async () => {
|
||||
const offerEntity = {
|
||||
lokadId: 'SWP',
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
|
||||
buyOrSell: 'sell',
|
||||
rateInSats: 1000,
|
||||
minSatsToExchange: 1250,
|
||||
numTokens: 1
|
||||
}
|
||||
|
||||
const result = await uut.ensureFunds(offerEntity)
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -74,8 +74,9 @@ describe('#IPFS', () => {
|
||||
}
|
||||
},
|
||||
adapters: {
|
||||
orbit: {
|
||||
privateLog: () => {}
|
||||
pubsub: {
|
||||
privateLog: () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,7 +97,7 @@ describe('#IPFS', () => {
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Cannot read property')
|
||||
assert.include(err.message, 'Cannot read')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -52,7 +52,8 @@ describe('#IPFS-adapter', () => {
|
||||
it('should stop the IPFS node', async () => {
|
||||
// Mock dependencies
|
||||
uut.ipfs = {
|
||||
stop: () => {}
|
||||
stop: () => {
|
||||
}
|
||||
}
|
||||
|
||||
const result = await uut.stop()
|
||||
@@ -61,24 +62,24 @@ describe('#IPFS-adapter', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('#rmBlocksDir', () => {
|
||||
it('should delete the /blocks directory', () => {
|
||||
const result = uut.rmBlocksDir()
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('should catch and throw an error', () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.fs, 'rmdirSync').throws(new Error('test error'))
|
||||
|
||||
uut.rmBlocksDir()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.equal(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
// describe('#rmBlocksDir', () => {
|
||||
// it('should delete the /blocks directory', () => {
|
||||
// const result = uut.rmBlocksDir()
|
||||
//
|
||||
// assert.equal(result, true)
|
||||
// })
|
||||
//
|
||||
// it('should catch and throw an error', () => {
|
||||
// try {
|
||||
// // Force an error
|
||||
// sandbox.stub(uut.fs, 'rmdirSync').throws(new Error('test error'))
|
||||
//
|
||||
// uut.rmBlocksDir()
|
||||
//
|
||||
// assert.fail('Unexpected code path')
|
||||
// } catch (err) {
|
||||
// assert.equal(err.message, 'test error')
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
})
|
||||
|
||||
@@ -5,48 +5,141 @@
|
||||
// Public npm libraries.
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
|
||||
// Local libraries.
|
||||
const P2wdbAdapter = require('../../../src/adapters/p2wdb')
|
||||
const P2wdbAdapter = require('../../../src/adapters/p2wdb-adapter')
|
||||
|
||||
describe('#P2wdbAdapter', () => {
|
||||
let uut, sandbox
|
||||
|
||||
beforeEach(() => {
|
||||
uut = new P2wdbAdapter()
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
uut = new P2wdbAdapter({ bchjs })
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#write', () => {
|
||||
it('should write an offer to the P2WDB', async () => {
|
||||
const inputObj = {
|
||||
txid: 'testTxid',
|
||||
signature: 'testSignature',
|
||||
message: 'testMessage',
|
||||
appId: 'testAppId',
|
||||
data: { key: 'value' }
|
||||
describe('#constructor', () => {
|
||||
it('should throw error if bch-js is not passed in', () => {
|
||||
try {
|
||||
uut = new P2wdbAdapter()
|
||||
|
||||
assert.fail('Unexpected coded path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Must pass an instance of bch-js when instantiating p2wdb.js adapter.')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Mock axios to prevent live network call
|
||||
sandbox.stub(uut.axios, 'post').resolves({ data: { hash: 'testhash' } })
|
||||
describe('#instantiateWriteLib', () => {
|
||||
it('should instantiate Write library using web 3 interface', async () => {
|
||||
// Force code path
|
||||
uut.config.useFullStackCash = false
|
||||
|
||||
const result = await uut.write(inputObj)
|
||||
// Mock dependencies
|
||||
uut.Write =
|
||||
class Write {
|
||||
constructor () {
|
||||
this.interface = 'web3'
|
||||
}
|
||||
}
|
||||
|
||||
const wif = 'L1tcvcqa5PztqqDH4ZEcUmHA9aSHhTau5E2Zwp1xEK5CrKBrjP3m'
|
||||
|
||||
const result = uut.instantiateWriteLib(wif)
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.equal(result, 'testhash')
|
||||
assert.equal(result.interface, 'web3')
|
||||
})
|
||||
|
||||
it('should instantiate Write library using web 2 interface', async () => {
|
||||
// Force code path
|
||||
uut.config.useFullStackCash = true
|
||||
|
||||
// Mock dependencies
|
||||
uut.Write =
|
||||
class Write {
|
||||
constructor () {
|
||||
this.interface = 'fullstack'
|
||||
}
|
||||
}
|
||||
|
||||
const wif = 'L1tcvcqa5PztqqDH4ZEcUmHA9aSHhTau5E2Zwp1xEK5CrKBrjP3m'
|
||||
|
||||
const result = uut.instantiateWriteLib(wif)
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.equal(result.interface, 'fullstack')
|
||||
})
|
||||
|
||||
it('should catch and throw errors', async () => {
|
||||
try {
|
||||
await uut.instantiateWriteLib()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, 'WIF private key required')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#checkForSufficientFunds', () => {
|
||||
it('should throw error if WIF is not provided', async () => {
|
||||
try {
|
||||
await uut.checkForSufficientFunds()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, 'invalid wif to check for funds')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return status of funds', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, 'instantiateWriteLib').returns({
|
||||
checkForSufficientFunds: () => true
|
||||
})
|
||||
|
||||
const wif = 'L1tcvcqa5PztqqDH4ZEcUmHA9aSHhTau5E2Zwp1xEK5CrKBrjP3m'
|
||||
|
||||
const result = await uut.checkForSufficientFunds(wif)
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#write', async () => {
|
||||
it('should throw error if WIF is not provided', async () => {
|
||||
try {
|
||||
await uut.write()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, 'Cannot destructure property')
|
||||
assert.include(err.message, 'wif input must be a private key starting with the letter L or K')
|
||||
}
|
||||
})
|
||||
|
||||
it('should write data to the P2WDB', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, 'instantiateWriteLib').returns({
|
||||
postEntry: async () => {
|
||||
return { hash: 'fake-hash' }
|
||||
}
|
||||
})
|
||||
|
||||
const wif = 'L1tcvcqa5PztqqDH4ZEcUmHA9aSHhTau5E2Zwp1xEK5CrKBrjP3m'
|
||||
const inObj = { wif }
|
||||
|
||||
const result = await uut.write(inObj)
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.equal(result, 'fake-hash')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -84,7 +84,8 @@ describe('#wallet', () => {
|
||||
try {
|
||||
// Force an error
|
||||
uut.WALLET_FILE = ''
|
||||
uut.BchWallet = () => {}
|
||||
uut.BchWallet = () => {
|
||||
}
|
||||
|
||||
await uut.openWallet()
|
||||
// console.log('result: ', result)
|
||||
@@ -120,7 +121,7 @@ describe('#wallet', () => {
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.include(err.message, 'Cannot read property')
|
||||
assert.include(err.message, 'Cannot read')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -155,44 +156,44 @@ describe('#wallet', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('#burnPsf', () => {
|
||||
it('should burn PSF tokens and return the txid', async () => {
|
||||
// mock instance of minimal-slp-wallet
|
||||
uut.bchWallet = new MockBchWallet()
|
||||
|
||||
const result = await uut.burnPsf()
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.equal(result.success, true)
|
||||
assert.equal(result.txid, 'txid')
|
||||
})
|
||||
|
||||
it('should throw error if no PSF tokens are found', async () => {
|
||||
try {
|
||||
// mock instance of minimal-slp-wallet
|
||||
uut.bchWallet = new MockBchWallet()
|
||||
|
||||
// Remove the PSF token from the mock data.
|
||||
uut.bchWallet.utxos.utxoStore.slpUtxos.type1.tokens.pop()
|
||||
|
||||
await uut.burnPsf()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Token UTXO of with ID of')
|
||||
}
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
await uut.burnPsf()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Cannot read property')
|
||||
}
|
||||
})
|
||||
})
|
||||
// describe('#burnPsf', () => {
|
||||
// it('should burn PSF tokens and return the txid', async () => {
|
||||
// // mock instance of minimal-slp-wallet
|
||||
// uut.bchWallet = new MockBchWallet()
|
||||
//
|
||||
// const result = await uut.burnPsf()
|
||||
// // console.log('result: ', result)
|
||||
//
|
||||
// assert.equal(result.success, true)
|
||||
// assert.equal(result.txid, 'txid')
|
||||
// })
|
||||
//
|
||||
// it('should throw error if no PSF tokens are found', async () => {
|
||||
// try {
|
||||
// // mock instance of minimal-slp-wallet
|
||||
// uut.bchWallet = new MockBchWallet()
|
||||
//
|
||||
// // Remove the PSF token from the mock data.
|
||||
// uut.bchWallet.utxos.utxoStore.slpUtxos.type1.tokens.pop()
|
||||
//
|
||||
// await uut.burnPsf()
|
||||
//
|
||||
// assert.fail('Unexpected code path')
|
||||
// } catch (err) {
|
||||
// assert.include(err.message, 'Token UTXO of with ID of')
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// it('should catch and throw an error', async () => {
|
||||
// try {
|
||||
// await uut.burnPsf()
|
||||
//
|
||||
// assert.fail('Unexpected code path')
|
||||
// } catch (err) {
|
||||
// assert.include(err.message, 'Cannot read')
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
|
||||
describe('#incrementNextAddress', () => {
|
||||
it('should increment the nextAddress property', async () => {
|
||||
|
||||
@@ -333,7 +333,7 @@ describe('#UserRPC', () => {
|
||||
assert.equal(result.endpoint, 'updateUser')
|
||||
assert.equal(result.success, false)
|
||||
assert.equal(result.status, 422)
|
||||
assert.include(result.message, 'Cannot read property')
|
||||
assert.include(result.message, 'Cannot read')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -375,7 +375,7 @@ describe('#UserRPC', () => {
|
||||
assert.equal(result.endpoint, 'getUser')
|
||||
assert.equal(result.success, false)
|
||||
assert.equal(result.status, 422)
|
||||
assert.include(result.message, 'Cannot read property')
|
||||
assert.include(result.message, 'Cannot read')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ let ctx
|
||||
const mockContext = require('../../../../unit/mocks/ctx-mock').context
|
||||
|
||||
describe('Contact', () => {
|
||||
before(async () => {})
|
||||
before(async () => {
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
uut = new ContactController()
|
||||
@@ -36,7 +37,7 @@ describe('Contact', () => {
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'Cannot read property')
|
||||
assert.include(err.message, 'Cannot read')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ describe('#Entry-REST-Controller', () => {
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'Cannot read property')
|
||||
assert.include(err.message, 'Cannot read')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -79,8 +79,7 @@ describe('#Entry-REST-Controller', () => {
|
||||
entry: 'entry',
|
||||
description: 'test',
|
||||
slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg',
|
||||
signature:
|
||||
'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
|
||||
signature: 'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
|
||||
category: 'test'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ let ctx
|
||||
const mockContext = require('../../../../unit/mocks/ctx-mock').context
|
||||
|
||||
describe('Logapi', () => {
|
||||
before(async () => {})
|
||||
before(async () => {
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
uut = new LogsApiController()
|
||||
@@ -35,7 +36,7 @@ describe('Logapi', () => {
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'Cannot read property')
|
||||
assert.include(err.message, 'Cannot read')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ describe('#Users-REST-Controller', () => {
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'Cannot read property')
|
||||
assert.include(err.message, 'Cannot read')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -91,9 +91,9 @@ describe('#Users-REST-Controller', () => {
|
||||
assert.property(ctx.response.body, 'user')
|
||||
assert.property(ctx.response.body, 'token')
|
||||
|
||||
// Used by downstream tests.
|
||||
// testUser = ctx.response.body.user
|
||||
// console.log('testUser: ', testUser)
|
||||
// Used by downstream tests.
|
||||
// testUser = ctx.response.body.user
|
||||
// console.log('testUser: ', testUser)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -181,7 +181,7 @@ describe('#Users-REST-Controller', () => {
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'Cannot read property')
|
||||
assert.include(err.message, 'Cannot read')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -223,7 +223,7 @@ describe('#Users-REST-Controller', () => {
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'Cannot read property')
|
||||
assert.include(err.message, 'Cannot read')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ const ipfs = {
|
||||
ipfsCoord: {
|
||||
useCases: {
|
||||
peer: {
|
||||
sendPrivateMessage: () => {}
|
||||
sendPrivateMessage: () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,6 +83,18 @@ const localdb = {
|
||||
static find () {}
|
||||
static findOne () {}
|
||||
|
||||
async save () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
|
||||
Offer: class Offer {
|
||||
constructor (obj) {}
|
||||
|
||||
static findById () {}
|
||||
static find () {}
|
||||
static findOne () {}
|
||||
|
||||
async save () {
|
||||
return {}
|
||||
}
|
||||
@@ -106,8 +119,10 @@ const bch = {
|
||||
// }
|
||||
const { MockBchWallet } = require('./wallet')
|
||||
const wallet = {
|
||||
burnPsf: async () => {},
|
||||
generateSignature: async () => {},
|
||||
burnPsf: async () => {
|
||||
},
|
||||
generateSignature: async () => {
|
||||
},
|
||||
getKeyPair: async () => {
|
||||
return { cashAddress: 'fakeAddr', wif: 'fakeWif', hdIndex: 1 }
|
||||
},
|
||||
@@ -115,7 +130,9 @@ const wallet = {
|
||||
}
|
||||
|
||||
const p2wdb = {
|
||||
write: async () => {}
|
||||
write: async () => {
|
||||
},
|
||||
checkForSufficientFunds: async () => true
|
||||
}
|
||||
|
||||
module.exports = { ipfs, localdb, bch, wallet, p2wdb, bchjs }
|
||||
module.exports = { ipfs, localdb, bch, wallet, p2wdb, bchjs}
|
||||
|
||||
@@ -5,11 +5,9 @@
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
|
||||
const mockWallet = {
|
||||
mnemonic:
|
||||
'course abstract aerobic deer try switch turtle diet fence affair butter top',
|
||||
mnemonic: 'course abstract aerobic deer try switch turtle diet fence affair butter top',
|
||||
privateKey: 'L5D2UAam8tvo3uii5kpgaGyjvVMimdrXu8nWGQSQjuuAix6ji1YQ',
|
||||
publicKey:
|
||||
'0379433ffc401483ade310469953c1cba77c71af904f07c15bde330d7198b4d6dc',
|
||||
publicKey: '0379433ffc401483ade310469953c1cba77c71af904f07c15bde330d7198b4d6dc',
|
||||
cashAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
|
||||
address: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
|
||||
slpAddress: 'simpleledger:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acq9zm7d33',
|
||||
@@ -29,7 +27,8 @@ class MockBchWallet {
|
||||
this.sendTokens = async () => {
|
||||
return 'fakeTxid'
|
||||
}
|
||||
this.getUtxos = async () => {}
|
||||
this.getUtxos = async () => {
|
||||
}
|
||||
|
||||
// Environment variable is used by wallet-balance.unit.js to force an error.
|
||||
if (process.env.NO_UTXO) {
|
||||
@@ -41,8 +40,7 @@ class MockBchWallet {
|
||||
bchUtxos: [
|
||||
{
|
||||
height: 700685,
|
||||
tx_hash:
|
||||
'1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
|
||||
tx_hash: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
|
||||
tx_pos: 0,
|
||||
value: 1000,
|
||||
txid: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
|
||||
@@ -51,8 +49,7 @@ class MockBchWallet {
|
||||
},
|
||||
{
|
||||
height: 700685,
|
||||
tx_hash:
|
||||
'1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
|
||||
tx_hash: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
|
||||
tx_pos: 2,
|
||||
value: 19406,
|
||||
txid: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
|
||||
@@ -66,47 +63,61 @@ class MockBchWallet {
|
||||
mintBatons: [],
|
||||
tokens: [
|
||||
{
|
||||
height: 700522,
|
||||
tx_hash:
|
||||
'bb5691b50930816be78dad76d203a1c97ac94c03f6051b2fa0159c71c43aa3d0',
|
||||
tx_pos: 1,
|
||||
value: 546,
|
||||
txid: 'bb5691b50930816be78dad76d203a1c97ac94c03f6051b2fa0159c71c43aa3d0',
|
||||
vout: 1,
|
||||
utxoType: 'token',
|
||||
transactionType: 'send',
|
||||
tokenId:
|
||||
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
|
||||
tokenTicker: 'TROUT',
|
||||
tokenName: "Trout's test token",
|
||||
tokenDocumentUrl: 'troutsblog.com',
|
||||
tokenDocumentHash: '',
|
||||
decimals: 2,
|
||||
tokenType: 1,
|
||||
isValid: true,
|
||||
tokenQty: '4.25'
|
||||
'height': 717331,
|
||||
'tx_hash': '74889580bb1a5f8c026aa2f55118ac9917df3332f7abae72a70343daa1c29621',
|
||||
'tx_pos': 1,
|
||||
'value': 546,
|
||||
'txid': '74889580bb1a5f8c026aa2f55118ac9917df3332f7abae72a70343daa1c29621',
|
||||
'vout': 1,
|
||||
'isSlp': true,
|
||||
'type': 'token',
|
||||
'qty': '10',
|
||||
'tokenId': '600ee24d0f208aebc2bdd2c4ee1b9acb6d57343561442e8676b5bbea311d5a0f',
|
||||
'address': 'bitcoincash:qqraj35x6l2qyqhjm5l7qlt7z2245ez8l5z3dwkeq5',
|
||||
'ticker': 'FLIPS',
|
||||
'name': 'FLIPS',
|
||||
'documentUri': '',
|
||||
'documentHash': '',
|
||||
'decimals': 1,
|
||||
'qtyStr': '1'
|
||||
},
|
||||
{
|
||||
height: 0,
|
||||
tx_hash:
|
||||
'c0ac066ce6efa1fa4763bf85a91c738e57c12b8765731bd07f0d8f5a55ce582f',
|
||||
tx_pos: 1,
|
||||
value: 546,
|
||||
txid: 'c0ac066ce6efa1fa4763bf85a91c738e57c12b8765731bd07f0d8f5a55ce582f',
|
||||
vout: 1,
|
||||
utxoType: 'token',
|
||||
transactionType: 'send',
|
||||
tokenId:
|
||||
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
|
||||
tokenTicker: 'PSF',
|
||||
tokenName: 'Permissionless Software Foundation',
|
||||
tokenDocumentUrl: 'psfoundation.cash',
|
||||
tokenDocumentHash: '',
|
||||
decimals: 8,
|
||||
tokenType: 1,
|
||||
isValid: true,
|
||||
tokenQty: '1',
|
||||
qtyStr: '1'
|
||||
'height': 730597,
|
||||
'tx_hash': '52520faddfafc46b8f8c9548b097f3a3b82a5bf363b5095047b9c5f83247fe36',
|
||||
'tx_pos': 1,
|
||||
'value': 546,
|
||||
'txid': '52520faddfafc46b8f8c9548b097f3a3b82a5bf363b5095047b9c5f83247fe36',
|
||||
'vout': 1,
|
||||
'isSlp': true,
|
||||
'type': 'token',
|
||||
'qty': '34999991',
|
||||
'tokenId': '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
|
||||
'address': 'bitcoincash:qqraj35x6l2qyqhjm5l7qlt7z2245ez8l5z3dwkeq5',
|
||||
'ticker': 'PSF',
|
||||
'name': 'Permissionless Software Foundation',
|
||||
'documentUri': 'psfoundation.cash',
|
||||
'documentHash': '',
|
||||
'decimals': 8,
|
||||
'qtyStr': '0.34999991'
|
||||
},
|
||||
{
|
||||
'height': 730597,
|
||||
'tx_hash': '5dc7e7c91382aed1666a51212dfb74050261e12c3c4f62b6b1e57f42d6c51ee1',
|
||||
'tx_pos': 2,
|
||||
'value': 546,
|
||||
'txid': '5dc7e7c91382aed1666a51212dfb74050261e12c3c4f62b6b1e57f42d6c51ee1',
|
||||
'vout': 2,
|
||||
'isSlp': true,
|
||||
'type': 'token',
|
||||
'qty': '18898',
|
||||
'tokenId': 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
|
||||
'address': 'bitcoincash:qqraj35x6l2qyqhjm5l7qlt7z2245ez8l5z3dwkeq5',
|
||||
'ticker': 'TROUT',
|
||||
'name': "Trout's test token",
|
||||
'documentUri': 'troutsblog.com',
|
||||
'documentHash': '',
|
||||
'decimals': 2,
|
||||
'qtyStr': '188.98'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -122,4 +133,4 @@ class MockBchWallet {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { MockBchWallet, mockWallet }
|
||||
module.exports = { MockBchWallet, mockWallet}
|
||||
|
||||
@@ -46,60 +46,13 @@ describe('#offer-use-case', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('#createOffer', () => {
|
||||
it('should create an offer and return the hash', async () => {
|
||||
const entryObj = {
|
||||
lokadId: 'SWP',
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
tokenId: 'token-id',
|
||||
buyOrSell: 'sell',
|
||||
rateInSats: 1000,
|
||||
minSatsToExchange: 1250,
|
||||
numTokens: 1
|
||||
}
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.adapters.wallet, 'burnPsf').resolves('fakeTxid')
|
||||
sandbox.stub(uut.offerEntity, 'validate').returns(entryObj)
|
||||
sandbox.stub(uut, 'ensureFunds').resolves()
|
||||
sandbox.stub(uut, 'moveTokens').resolves({ txid: 'fakeTxid', vout: 0 })
|
||||
sandbox.stub(uut.adapters.wallet.bchWallet, 'getUtxos').resolves()
|
||||
sandbox
|
||||
.stub(uut.adapters.wallet, 'generateSignature')
|
||||
.resolves('fakeSignature')
|
||||
sandbox.stub(uut.adapters.p2wdb, 'write').resolves('fakeHash')
|
||||
|
||||
const result = await uut.createOffer(entryObj)
|
||||
console.log('result: ', result)
|
||||
|
||||
assert.isString(result)
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(uut.offerEntity, 'validate')
|
||||
.throws(new Error('test error'))
|
||||
|
||||
await uut.createOffer()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#ensureFunds', () => {
|
||||
it('should return true if wallet has enough funds for a sell order', async () => {
|
||||
const offerEntity = {
|
||||
lokadId: 'SWP',
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
tokenId:
|
||||
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
|
||||
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
|
||||
buyOrSell: 'sell',
|
||||
rateInSats: 1000,
|
||||
minSatsToExchange: 0,
|
||||
@@ -123,8 +76,7 @@ describe('#offer-use-case', () => {
|
||||
lokadId: 'SWP',
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
tokenId:
|
||||
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
|
||||
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
|
||||
buyOrSell: 'sell',
|
||||
rateInSats: 1000,
|
||||
minSatsToExchange: 0,
|
||||
@@ -138,7 +90,53 @@ describe('#offer-use-case', () => {
|
||||
assert.property(result, 'vout')
|
||||
|
||||
assert.equal(result.txid, 'fakeTxid')
|
||||
assert.equal(result.vout, 0)
|
||||
assert.equal(result.vout, 1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#createOffer', () => {
|
||||
it('should create an offer and return the hash', async () => {
|
||||
const entryObj = {
|
||||
lokadId: 'SWP',
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
tokenId: 'token-id',
|
||||
buyOrSell: 'sell',
|
||||
rateInSats: 1000,
|
||||
minSatsToExchange: 1250,
|
||||
numTokens: 1
|
||||
}
|
||||
|
||||
// Mock dependencies
|
||||
// sandbox.stub(uut.adapters.wallet, 'burnPsf').resolves('fakeTxid')
|
||||
sandbox.stub(uut.offerEntity, 'validate').returns(entryObj)
|
||||
sandbox.stub(uut, 'ensureFunds').resolves()
|
||||
sandbox.stub(uut, 'moveTokens').resolves({ txid: 'fakeTxid', vout: 0, hdIndex: 1 })
|
||||
sandbox.stub(uut.adapters.wallet.bchWallet, 'getUtxos').resolves()
|
||||
// sandbox
|
||||
// .stub(uut.adapters.wallet, 'generateSignature')
|
||||
// .resolves('fakeSignature')
|
||||
sandbox.stub(uut.adapters.p2wdb, 'write').resolves('fakeHash')
|
||||
|
||||
const result = await uut.createOffer(entryObj)
|
||||
console.log('result: ', result)
|
||||
|
||||
assert.isString(result)
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(uut.offerEntity, 'validate')
|
||||
.throws(new Error('test error'))
|
||||
|
||||
await uut.createOffer()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -153,7 +153,7 @@ describe('#users-use-case', () => {
|
||||
await uut.getAllUsers()
|
||||
// console.log(`users: ${JSON.stringify(users, null, 2)}`)
|
||||
|
||||
// assert.isArray(users)
|
||||
// assert.isArray(users)
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
@@ -228,8 +228,8 @@ describe('#users-use-case', () => {
|
||||
// Assert that the expected properties for the user model exist.
|
||||
// assert.property(result, 'type')
|
||||
assert.property(result, '_id')
|
||||
// assert.property(result, 'email')
|
||||
// assert.property(result, 'name')
|
||||
// assert.property(result, 'email')
|
||||
// assert.property(result, 'name')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -241,7 +241,7 @@ describe('#users-use-case', () => {
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, 'Cannot read property')
|
||||
assert.include(err.message, 'Cannot read')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -346,7 +346,7 @@ describe('#users-use-case', () => {
|
||||
// assert.equal(result.name, 'testy tester')
|
||||
// })
|
||||
|
||||
// TODO: verify that an admin can change the type of a user
|
||||
// TODO: verify that an admin can change the type of a user
|
||||
})
|
||||
|
||||
describe('#authUser', () => {
|
||||
@@ -356,9 +356,9 @@ describe('#users-use-case', () => {
|
||||
await uut.authUser('test@test.com', 'password')
|
||||
// console.log('user: ', user)
|
||||
|
||||
// assert.property(user, '_id')
|
||||
// assert.property(user, 'email')
|
||||
// assert.property(user, 'name')
|
||||
// assert.property(user, '_id')
|
||||
// assert.property(user, 'email')
|
||||
// assert.property(user, 'name')
|
||||
})
|
||||
|
||||
it('should throw an error if no user matches the login', async () => {
|
||||
@@ -399,7 +399,7 @@ describe('#users-use-case', () => {
|
||||
assert.fail('Unexpected code path.')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, 'Cannot read property')
|
||||
assert.include(err.message, 'Cannot read')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
This script will travers the HD wallet and sweep funds and tokens back
|
||||
into the root address (index 0). That root address needs to have funds
|
||||
to pay for the transactions.
|
||||
|
||||
The root address will make a final transaction to consolidate all tokens.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const BchTokenSweep = require('bch-token-sweep/index')
|
||||
|
||||
// Local libraries
|
||||
const WalletAdapter = require('../../src/adapters/wallet')
|
||||
|
||||
async function sweepFunds () {
|
||||
try {
|
||||
// Open the wallet files.
|
||||
const wallet = new WalletAdapter()
|
||||
const walletInfo = await wallet.openWallet()
|
||||
console.log('walletInfo: ', walletInfo)
|
||||
|
||||
const rootAddr = walletInfo.cashAddress
|
||||
const rootWif = walletInfo.privateKey
|
||||
console.log(`Sweeping all funds into root address ${rootAddr}...`)
|
||||
|
||||
// Generate an HD tree
|
||||
const bchjs = new BCHJS()
|
||||
const rootSeed = await bchjs.Mnemonic.toSeed(walletInfo.mnemonic)
|
||||
const masterHDNode = bchjs.HDNode.fromSeed(rootSeed)
|
||||
|
||||
let emptyAddrCnt = 0
|
||||
let hdIndex = 1
|
||||
|
||||
do {
|
||||
// Generate a keypair from the HD wallet.
|
||||
const childNode = masterHDNode.derivePath(`m/44'/245'/0'/0/${hdIndex}`)
|
||||
// const cashAddress = bchjs.HDNode.toCashAddress(childNode)
|
||||
const wifToSweep = bchjs.HDNode.toWIF(childNode)
|
||||
|
||||
try {
|
||||
// Sweep tokens from address
|
||||
const sweeper = new BchTokenSweep(
|
||||
wifToSweep,
|
||||
rootWif,
|
||||
bchjs,
|
||||
550,
|
||||
rootAddr
|
||||
)
|
||||
await sweeper.populateObjectFromNetwork()
|
||||
|
||||
const hex = await sweeper.sweepTo(rootAddr)
|
||||
// console.log(`hex: ${hex}`)
|
||||
|
||||
const txid = await sweeper.blockchain.broadcast(hex)
|
||||
|
||||
// console.log('Transaction ID', txid)
|
||||
console.log(`Swept HD index ${hdIndex}. TXID: ${txid}`)
|
||||
|
||||
emptyAddrCnt = 0
|
||||
|
||||
// Wait between loop iterations.
|
||||
await bchjs.Util.sleep(3000)
|
||||
} catch (err) {
|
||||
console.log(`error message: ${err.message}`)
|
||||
emptyAddrCnt++
|
||||
}
|
||||
|
||||
hdIndex++
|
||||
} while (emptyAddrCnt < 5)
|
||||
|
||||
console.log('5 empty addresses detected. Exiting.')
|
||||
} catch (err) {
|
||||
console.error('Error in sweepFunds(): ', err)
|
||||
}
|
||||
}
|
||||
sweepFunds()
|
||||
Reference in New Issue
Block a user