mirror of
https://github.com/Permissionless-Software-Foundation/bch-dex.git
synced 2026-09-22 17:12:03 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b44aab429 | ||
|
|
8cd8086440 | ||
|
|
37e2377113 | ||
|
|
a6949f0e8d | ||
|
|
9e14f0a230 | ||
|
|
7e6b76703d | ||
|
|
5910620048 | ||
|
|
4201850389 | ||
|
|
449efd2a98 | ||
|
|
5af8a31242 | ||
|
|
65bffe1ef6 | ||
|
|
ceafb2820d | ||
|
|
90d1e68dae | ||
|
|
57dfad9215 | ||
|
|
212bc65c7e | ||
|
|
810d386dbc | ||
|
|
8ed1dbcffe | ||
|
|
1563a740aa |
+2
-2
@@ -94,13 +94,13 @@ class Server {
|
||||
try {
|
||||
try {
|
||||
// Delete an old webhook if it exists.
|
||||
await webhookLib.deleteWebhook(`http://localhost:${config.port}/offer`)
|
||||
await webhookLib.deleteWebhook(`http://localhost:${config.port}/p2wdb`)
|
||||
} catch (err) {
|
||||
/* exit quietly */
|
||||
// console.log('err deleting webhook: ', err)
|
||||
}
|
||||
|
||||
await webhookLib.createWebhook(`http://localhost:${config.port}/offer`)
|
||||
await webhookLib.createWebhook(`http://localhost:${config.port}/p2wdb`)
|
||||
console.log('Webhook created')
|
||||
} catch (error) {
|
||||
console.log('Webhook cant be created')
|
||||
|
||||
Vendored
+2
-1
@@ -35,7 +35,8 @@ module.exports = {
|
||||
useFullStackCash: process.env.USE_FULLSTACKCASH ? true : false,
|
||||
consumerUrl: process.env.CONSUMER_URL
|
||||
? process.env.CONSUMER_URL
|
||||
: 'https://free-bch.fullstack.cash',
|
||||
// : 'https://free-bch.fullstack.cash',
|
||||
: 'https://wa-usa-bch-consumer.fullstackcash.nl',
|
||||
|
||||
// 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',
|
||||
|
||||
+4
-3
@@ -74,7 +74,7 @@ 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 `/order` REST API endpoint to create a new Order.
|
||||
- `bch-dex` will move the funds into a segregated UTXO, and will use that UTXO to create an Order. The Order data is written to the P2WDB. The Order 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 `/offer` endpoint by the P2WDB. This event will trigger the import of the new data into the apps local Mongo database, and generate a new Offer model.
|
||||
- After submitting the data to the P2WDB, `bch-dex` will receive a webhook call to its POST `/p2wdb` endpoint by the P2WDB. This event will trigger the import of the new data into the apps local Mongo database, and generate a new Offer 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 Offer model.
|
||||
|
||||
## Taking an Offer
|
||||
@@ -82,8 +82,8 @@ Writing data follows these steps:
|
||||
Users can browse the Offers tracked by a local `bch-dex` by using a *Client*. When they find an Offer they want to to take, they'll use some UI element that will send data to the POST `/offer/take` REST API endpoint. These series of steps happen:
|
||||
|
||||
- The `bch-dex` checks to see if the wallet it controls has enough BCH to take the other side of the Offer. If it does, the funds for the offer are moved to a segregated UTXO.
|
||||
- The new UTXO is used to generate a *Counter Offer*, which contain a partially signed transaction saved as a hex string.
|
||||
- The *Counter Offer* is submitted to the P2WDB. This triggers a webhook event in every running instance of `bch-dex` on the network.
|
||||
- The new UTXO is used to generate a *Counter Offer*, which contains a partially signed transaction saved as a hex string.
|
||||
- The *Counter Offer* is uploaded to the P2WDB. This triggers a webhook event in every running instance of `bch-dex` on the network.
|
||||
- When the webhook is triggered, `bch-dex` will check to see if the *Counter Offer* matches an *Order* under its control. If a match is found, it will trigger an *Accept* event.
|
||||
|
||||
## Accepting a Counter Offer
|
||||
@@ -92,6 +92,7 @@ This part of the process is automated and does not require input from the user.
|
||||
|
||||
- When a *Counter Offer* is received that matches an *Order* tracked by the local copy of `bch-dex`, it will trigger the *Acceptance* phase.
|
||||
- In the *Acceptance* phase, the transaction will be checked to see if it matches the conditions in the original *Order*. If all checks pass, `bch-dex` will sign the transaction and broadcast it, completing the trade.
|
||||
- A 'garbage collection' function that runs periodically will delete Orders and Offers in bch-dex that have had their UTXO spent, automatically cleaning up stale trade data that is no longer valid.
|
||||
|
||||
## Maintenance
|
||||
|
||||
|
||||
+73
-35
@@ -26,28 +26,31 @@ Order entities have the following properties:
|
||||
- _utxoTxid_ - The TXID of the UTXO representing the token or BCH being offered for sale.
|
||||
- _utxoVout_ - The vout of the UTXO representing the token or BCH being offered for sale.
|
||||
|
||||
- Trade Data:
|
||||
- _buyOrSell_ - A string with a value `buy` or `sell` indicating which type of offer this is.
|
||||
- _numTokens_ - The maximum number of tokens offered for sale.
|
||||
- _rateInBaseUnit_ - The rate in terms of currency-unit-per-token. Ex: 1000 = 1000 sats per token
|
||||
- For Bitcoin, the min currency is sats.
|
||||
- For AVAX, the min currency is nano-Avax.
|
||||
- for eCash, the min currency is bits.
|
||||
- _minUnitsToExchange_ - The minimum order size accepted.
|
||||
- Trade Data:
|
||||
- _buyOrSell_ - A string with a value `buy` or `sell` indicating which type of offer this is.
|
||||
- _numTokens_ - The maximum number of tokens offered for sale.
|
||||
- _rateInBaseUnit_ - The rate in terms of currency-unit-per-token. Ex: 1000 = 1000 sats per token
|
||||
- For Bitcoin, the min currency is sats.
|
||||
- For AVAX, the min currency is nano-Avax.
|
||||
- for eCash, the min currency is bits.
|
||||
- _minUnitsToExchange_ - The minimum order size accepted.
|
||||
- _makerAddr_ - The address for the taker to send money to.
|
||||
- _p2wdbTxid_ - The TXID proof-of-burn used to add the order to the P2WDB.
|
||||
- _p2wdbHash_ - The CID used to identify the order entry in the P2WDB.
|
||||
|
||||
- Authentication Data:
|
||||
- _signature_ - A message signed by the address which created the order.
|
||||
- _sigMsg_ - The clear-text message used to generate the signature.
|
||||
- _offerBchAddr_ - The BCH address controlling the offer.
|
||||
- _offerPubKey_ - The public key used to generate the BCH address, used for encryption.
|
||||
- Authentication Data:
|
||||
- _signature_ - A message signed by the address which created the order.
|
||||
- _sigMsg_ - The clear-text message used to generate the signature.
|
||||
- _offerBchAddr_ - The BCH address controlling the offer.
|
||||
- _offerPubKey_ - The public key used to generate the BCH address, used for encryption.
|
||||
|
||||
- Wallet Data:
|
||||
- _hdIndex_ - The HD index of the wallet used to generate the keypair to store the UTXO being offered for sale.
|
||||
- Wallet Data:
|
||||
- _hdIndex_ - The HD index of the wallet used to generate the keypair to store the UTXO being offered for sale.
|
||||
|
||||
- SWaP Protocol properties:
|
||||
- _lokadId_ - Not used. Provided for future functionality.
|
||||
- _messageType_ - Not used. Provided for future functionality.
|
||||
- _messageClass_ - Not used. Provided for future functionality.
|
||||
- SWaP Protocol properties:
|
||||
- _lokadId_ - Not used. Provided for future functionality.
|
||||
- _messageType_ - Not used. Provided for future functionality.
|
||||
- _messageClass_ - Not used. Provided for future functionality.
|
||||
|
||||
|
||||
### Offer
|
||||
@@ -70,8 +73,13 @@ Offer entities have the following properties:
|
||||
- For AVAX, the min currency is nano-Avax.
|
||||
- for eCash, the min currency is bits.
|
||||
- _minUnitsToExchange_ - The minimum order size accepted.
|
||||
- _makerAddr_ - The address for the taker to send money to.
|
||||
- _p2wdbTxid_ - The TXID proof-of-burn used to add the order to the P2WDB.
|
||||
- _p2wdbHash_ - The CID used to identify the order entry in the P2WDB.
|
||||
- _offerStatus_ - The state of the offer. When the data is added to the P2WDB, it gets a value of 'posted', but the database model internal to bch-dex can have the following properties:
|
||||
- *posted* - The offer is posted and can be countered by a taker.
|
||||
- *taken* - The offer was countered and accepted.
|
||||
- *dead* - The UTXO was spent outside the trade, which automatically makes the offer dead.
|
||||
|
||||
- Authentication Data:
|
||||
- _signature_ - A message signed by the address which created the order.
|
||||
@@ -88,46 +96,76 @@ Offer entities have the following properties:
|
||||
- _messageType_ - Not used. Provided for future functionality.
|
||||
- _messageClass_ - Not used. Provided for future functionality.
|
||||
|
||||
When an Offer is uploaded to the P2WDB, the following properties are added to the data:
|
||||
|
||||
- Added properties to P2WDB data:
|
||||
- *dataType* - Has a value of 'offer'. This is used to route the data correctly when the P2WDB webhook passes the data to `bch-dex`.
|
||||
|
||||
### Counter Offer
|
||||
|
||||
A Counter Offer is the other side of an Offer. It contains a partially signed transaction, created by the Taker. The Maker will review the Counter Offer before accepting and finalizing the trade.
|
||||
A Counter Offer is the other side of the trade, a mirror image to an Offer. It contains a partially signed transaction, created by the Taker. The Maker will review the Counter Offer before accepting and finalizing the trade.
|
||||
|
||||
Counter Offers are not tracked via database models like Offers and Orders are. They are processed by `bch-dex` as soon as they are received. A garbage collection function will be called periodically, to sweep the segregated UTXOs used to generate a Counter Offer back into the root address of the wallet, if it is not accepted after a period of time.
|
||||
|
||||
To generate a Counter Offer, a segregated UTXO is created that matches the requirements in the Offer. A partially signed transaction is created that includes the Offer UTXO and the Counter Offer UTXO. The Taker signs the input consuming the Counter Offer UTXO. The partially signed transaction and other trade data is uploaded to P2WDB.
|
||||
|
||||
When the Counter Offer data is uploaded to the P2WDB, the data must have the following properties:
|
||||
|
||||
- *partialTxHex* - A hexidecimal representation of the partially-signed transaction, which includes the UTXO in the Offer and a second UTXO for that matches the conditions in the Offer.
|
||||
- *dataType* - Must have a value of `counter-offer`
|
||||
|
||||
Details TBD.
|
||||
|
||||
## Use Cases
|
||||
|
||||
Use cases are verbs or actions that is done _to_ an Entity or _between_ Entities.
|
||||
|
||||
### Offer
|
||||
|
||||
- **`createOffer()`** - This method is triggered by a webhook from the P2WDB. It will take the data provided by the P2WDB and create a new Order entity in the local database.
|
||||
|
||||
### Order
|
||||
|
||||
- **`createOrder()`** - A macro command that leverages the other functions in this library to create a new Order and submit it to the P2WDB.
|
||||
- **`ensureFunds()`** - Ensure that the wallet has enough BCH and tokens to complete the requested trade.
|
||||
- **`moveTokens()`** - Move the tokens indicated in the order to a temporary holding address. This will generate the UTXO used in the webhook message. This function moves the funds and returns the UTXO information.
|
||||
- **`createOrder()`** - A macro command that leverages `ensureFunds()` and `moveTokens()`, to create a new Order and submit it to the P2WDB.
|
||||
- **`findOrderByHash()`** - Given a P2WDB CID 'hash', this function will return the corresponding Order model associated with that CID.
|
||||
|
||||
### Counter Offer
|
||||
### Offer
|
||||
|
||||
TBD
|
||||
- **`createOffer()`** - This method is triggered by a webhook from the P2WDB. It will take the data provided by the P2WDB and create a new Offer entity in the local database.
|
||||
- **`listOffers()`** - Returns a list of all the active Offers tracked by `bch-dex`.
|
||||
- **`takeOffer()`** - Generate a segregated UTXO and partially signed transaction, then upload the Counter Offer data to the P2WDB.
|
||||
- **`ensureFunds()`** - Ensure the wallet has enough BCH and tokens to make a Counter Offer.
|
||||
- **`findOfferByHash()`** - Given a P2WDB CID 'hash', this function will return the corresponding Offer model associated with that CID.
|
||||
- **`acceptCounterOffer()`** - This function is triggered by the P2WDB webhook REST API handler. When a Counter Offer is passed to `bch-dex` by the P2WDB, the data is then passed to this function. It does due diligence on the Counter Offer, then signs and broadcasts the transaction to accept the Counter Offer.
|
||||
|
||||
## Controllers
|
||||
|
||||
Controllers are inputs to the system. When a controller is activated, it causes the system to react in some way.
|
||||
|
||||
### Offers
|
||||
|
||||
- **POST /offer** - This POST REST API endpoint will be triggered by a webhook generated from P2WDB. This will notify the `bch-dex` that a new entry has been added to the P2WDB that matches the `appId` of `swap-<chain>`, where `<chain>` has a value of `avax`, `bch`, or `ecash`. It's a new entry that should be evaluated for inclusion in the `ipfs-swap-service` local database.
|
||||
|
||||
### Orders
|
||||
|
||||
- **POST /order** - This POST REST API endpoint can be triggered by the Client or a simple curl call. It passes in the data needed for `bch-dex` to generate and track a new Order, then submit the data to the P2WDB to generate an Offer that is tracked by all other instances of `bch-dex`.
|
||||
- **POST /order** - This POST REST API endpoint can be triggered by the Client or a simple curl call. It passes in the data needed for `bch-dex` to generate and track a new Order, then submits the data to the P2WDB to generate an Offer that is tracked by all other instances of `bch-dex` on the network.
|
||||
|
||||
### Offers
|
||||
|
||||
- **GET /offer/list** - Returns an array of objects, where each object represents an active Offer.
|
||||
|
||||
- **POST /offer/take** - Given the P2WDB CID of an Offer, calling this endpoint will attempt to generate a Counter Offer and upload it to the P2WDB.
|
||||
|
||||
### P2WDB
|
||||
|
||||
- **POST /p2wdb** - This POST REST API endpoint will be called by a webhook generated from the P2WDB. This will notify the `bch-dex` that a new entry has been added to the P2WDB that matches the `appId` of `swap-<chain>`, where `<chain>` has a value of `avax`, `bch`, or `ecash`. It's a new entry that should be evaluated for inclusion in the `bch-dex` local database. Based on the `dataType` property, the data is routed to either create a new Offer or to process a new Counter Offer.
|
||||
|
||||
## Adapters
|
||||
|
||||
Adapters are output libraries so that the business logic doesn't need to know any specific information about the I/O. They are essentially the output of the application.
|
||||
Adapters are outputs. These libraries exist so that the business logic doesn't need to know any specific information about the I/O. They handle the low-level mechanics of the various subsystems.
|
||||
|
||||
- **localdb** - An adapter for the local database (MongoDB).
|
||||
- **ipfs** - An adapter for IPFS and the ipfs-coord library. It allows the app to use the JSON-RPC over IPFS.
|
||||
- **wallet** - Handles low-level wallet tasks.
|
||||
- **`moveTokens()`** - Move the tokens indicated in the order to a temporary holding address. This will generate the segregated UTXOs used in the trade. This function moves the funds and returns the UTXO information.
|
||||
- **`moveBCH()`** - Same idea as `moveTokens()`, but creates a non-SLP segregated UTXO, used when generating a Counter Offer.
|
||||
- **`openWallet()`** - Opens the JSON file containing the wallet data for the app.
|
||||
- **`instanceWallet()`** - Given the wallet data returned by `openWallet()`, it generates an instance of the [minimal-slp-wallet](https://www.npmjs.com/package/minimal-slp-wallet) for generating UTXOs and interacting with the blockchain.
|
||||
- **`incrementNextAddress()`** - Increments the index of the [HD wallet](https://github.com/bitcoinbook/bitcoinbook/blob/develop/ch05.asciidoc#hd_wallets) used to generate keypairs for storing segregated UTXOs.
|
||||
- **`getKeyPair()`** - Generates a keypair from the HD wallet.
|
||||
- **`generateSignature()`** - Generate a cryptographic signature.
|
||||
- **`generatePartialTx()`** - Create a partial transaction as part of a Counter Offer.
|
||||
- **`deseralizeTx()`** - Used only for debugging purposes. Expands a hex transaction into a JSON object.
|
||||
- **`completeTx()`** - Complete the partially signed transaction by signing the first input, then broadcasting the transaction to the network.
|
||||
|
||||
Generated
+104
-26
@@ -9,10 +9,11 @@
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@psf/bch-js": "5.4.0",
|
||||
"@psf/bch-js": "6.2.1",
|
||||
"axios": "0.21.1",
|
||||
"bch-message-lib": "2.1.4",
|
||||
"bcryptjs": "2.4.3",
|
||||
"bitcoincashjs-lib": "3.3.3",
|
||||
"glob": "7.1.6",
|
||||
"ipfs": "0.60.0",
|
||||
"ipfs-coord": "7.1.6",
|
||||
@@ -33,7 +34,7 @@
|
||||
"koa2-ratelimit": "0.9.1",
|
||||
"libp2p": "^0.36.2",
|
||||
"line-reader": "0.4.0",
|
||||
"minimal-slp-wallet": "4.4.3",
|
||||
"minimal-slp-wallet": "4.4.5",
|
||||
"mongoose": "5.13.13",
|
||||
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
|
||||
"nodemailer": "6.4.17",
|
||||
@@ -1603,9 +1604,9 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@psf/bch-js": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "http://94.130.170.209:4873/@psf%2fbch-js/-/bch-js-5.4.0.tgz",
|
||||
"integrity": "sha512-HyBCpobC9moxSbQl4k/tb/GW9p8L12Fu+8yR1x8eflxBe7c2bwBlG218LmuoNcvjJMAdDxk0VEHvtO64vpltsw==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "http://94.130.170.209:4873/@psf%2fbch-js/-/bch-js-6.2.1.tgz",
|
||||
"integrity": "sha512-sTqjbPkpfxrxu0WARZBf/t2K+Sz3Y4EpuzHlSZG2n4k7jKM+J8tQuh/stRsEXOl/ewwdVmjNIAcBx+Z+MkCtHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@chris.troutner/bip32-utils": "1.0.5",
|
||||
@@ -1613,7 +1614,7 @@
|
||||
"@psf/bitcoincash-ops": "2.0.0",
|
||||
"@psf/bitcoincashjs-lib": "4.0.2",
|
||||
"@psf/coininfo": "4.0.0",
|
||||
"axios": "^0.21.4",
|
||||
"axios": "0.26.1",
|
||||
"bc-bip68": "1.0.5",
|
||||
"bchaddrjs-slp": "0.2.5",
|
||||
"bigi": "1.4.2",
|
||||
@@ -1635,12 +1636,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@psf/bch-js/node_modules/axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "http://94.130.170.209:4873/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"version": "0.26.1",
|
||||
"resolved": "http://94.130.170.209:4873/axios/-/axios-0.26.1.tgz",
|
||||
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
"follow-redirects": "^1.14.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@psf/bch-js/node_modules/randombytes": {
|
||||
@@ -3113,6 +3114,39 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bitcoin-ops": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "http://94.130.170.209:4873/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz",
|
||||
"integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bitcoincashjs-lib": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "http://94.130.170.209:4873/bitcoincashjs-lib/-/bitcoincashjs-lib-3.3.3.tgz",
|
||||
"integrity": "sha512-dk6GCVpCE+7oELy8l2Y/9vGR1ymlnsQffU7YmYNeBoUSTH9gckbKSk6xrg3O5b2P0C7+1SifQ8de7luCpq6k8g==",
|
||||
"deprecated": "ooops",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bech32": "^1.1.2",
|
||||
"bigi": "^1.4.0",
|
||||
"bip66": "^1.1.0",
|
||||
"bitcoin-ops": "^1.3.0",
|
||||
"bs58check": "^2.0.0",
|
||||
"create-hash": "^1.1.0",
|
||||
"create-hmac": "^1.1.3",
|
||||
"ecurve": "^1.0.0",
|
||||
"merkle-lib": "^2.0.10",
|
||||
"pushdata-bitcoin": "^1.0.1",
|
||||
"randombytes": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1",
|
||||
"typeforce": "^1.11.3",
|
||||
"varuint-bitcoin": "^1.0.4",
|
||||
"wif": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bitcoinjs-message": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "http://94.130.170.209:4873/bitcoinjs-message/-/bitcoinjs-message-2.0.0.tgz",
|
||||
@@ -13021,12 +13055,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimal-slp-wallet": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "http://94.130.170.209:4873/minimal-slp-wallet/-/minimal-slp-wallet-4.4.3.tgz",
|
||||
"integrity": "sha512-wpELTh4WYems96SxyRW/KdglJ6T3ieR7VVDCLcqq7bwZ+tJXFznOxDRggS5sREI9tTxiAHIyVSg+nbXZZk8buQ==",
|
||||
"version": "4.4.5",
|
||||
"resolved": "http://94.130.170.209:4873/minimal-slp-wallet/-/minimal-slp-wallet-4.4.5.tgz",
|
||||
"integrity": "sha512-f2Se66WAdVzPFwmsIRU2ltVLcjWYLDaAC5amlkEyk6lfkj5VEXrYq3480iz/BNRYWwtrTq63oKqMyXpJ7w9HWA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@psf/bch-js": "5.4.0",
|
||||
"@psf/bch-js": "6.2.1",
|
||||
"apidoc": "0.25.0",
|
||||
"bch-consumer": "1.2.0",
|
||||
"bch-donation": "1.1.1",
|
||||
@@ -18841,6 +18875,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pushdata-bitcoin": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "http://94.130.170.209:4873/pushdata-bitcoin/-/pushdata-bitcoin-1.0.1.tgz",
|
||||
"integrity": "sha1-FZMdPNlnreUiBvUjqnMxrvfUOvc=",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bitcoin-ops": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/q": {
|
||||
"version": "1.5.1",
|
||||
"dev": true,
|
||||
@@ -23401,16 +23444,16 @@
|
||||
"version": "1.1.0"
|
||||
},
|
||||
"@psf/bch-js": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "http://94.130.170.209:4873/@psf%2fbch-js/-/bch-js-5.4.0.tgz",
|
||||
"integrity": "sha512-HyBCpobC9moxSbQl4k/tb/GW9p8L12Fu+8yR1x8eflxBe7c2bwBlG218LmuoNcvjJMAdDxk0VEHvtO64vpltsw==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "http://94.130.170.209:4873/@psf%2fbch-js/-/bch-js-6.2.1.tgz",
|
||||
"integrity": "sha512-sTqjbPkpfxrxu0WARZBf/t2K+Sz3Y4EpuzHlSZG2n4k7jKM+J8tQuh/stRsEXOl/ewwdVmjNIAcBx+Z+MkCtHQ==",
|
||||
"requires": {
|
||||
"@chris.troutner/bip32-utils": "1.0.5",
|
||||
"@psf/bip21": "2.0.1",
|
||||
"@psf/bitcoincash-ops": "2.0.0",
|
||||
"@psf/bitcoincashjs-lib": "4.0.2",
|
||||
"@psf/coininfo": "4.0.0",
|
||||
"axios": "^0.21.4",
|
||||
"axios": "0.26.1",
|
||||
"bc-bip68": "1.0.5",
|
||||
"bchaddrjs-slp": "0.2.5",
|
||||
"bigi": "1.4.2",
|
||||
@@ -23432,11 +23475,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "http://94.130.170.209:4873/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"version": "0.26.1",
|
||||
"resolved": "http://94.130.170.209:4873/axios/-/axios-0.26.1.tgz",
|
||||
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
"follow-redirects": "^1.14.8"
|
||||
}
|
||||
},
|
||||
"randombytes": {
|
||||
@@ -24510,6 +24553,33 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"bitcoin-ops": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "http://94.130.170.209:4873/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz",
|
||||
"integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow=="
|
||||
},
|
||||
"bitcoincashjs-lib": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "http://94.130.170.209:4873/bitcoincashjs-lib/-/bitcoincashjs-lib-3.3.3.tgz",
|
||||
"integrity": "sha512-dk6GCVpCE+7oELy8l2Y/9vGR1ymlnsQffU7YmYNeBoUSTH9gckbKSk6xrg3O5b2P0C7+1SifQ8de7luCpq6k8g==",
|
||||
"requires": {
|
||||
"bech32": "^1.1.2",
|
||||
"bigi": "^1.4.0",
|
||||
"bip66": "^1.1.0",
|
||||
"bitcoin-ops": "^1.3.0",
|
||||
"bs58check": "^2.0.0",
|
||||
"create-hash": "^1.1.0",
|
||||
"create-hmac": "^1.1.3",
|
||||
"ecurve": "^1.0.0",
|
||||
"merkle-lib": "^2.0.10",
|
||||
"pushdata-bitcoin": "^1.0.1",
|
||||
"randombytes": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1",
|
||||
"typeforce": "^1.11.3",
|
||||
"varuint-bitcoin": "^1.0.4",
|
||||
"wif": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"bitcoinjs-message": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "http://94.130.170.209:4873/bitcoinjs-message/-/bitcoinjs-message-2.0.0.tgz",
|
||||
@@ -31683,11 +31753,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"minimal-slp-wallet": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "http://94.130.170.209:4873/minimal-slp-wallet/-/minimal-slp-wallet-4.4.3.tgz",
|
||||
"integrity": "sha512-wpELTh4WYems96SxyRW/KdglJ6T3ieR7VVDCLcqq7bwZ+tJXFznOxDRggS5sREI9tTxiAHIyVSg+nbXZZk8buQ==",
|
||||
"version": "4.4.5",
|
||||
"resolved": "http://94.130.170.209:4873/minimal-slp-wallet/-/minimal-slp-wallet-4.4.5.tgz",
|
||||
"integrity": "sha512-f2Se66WAdVzPFwmsIRU2ltVLcjWYLDaAC5amlkEyk6lfkj5VEXrYq3480iz/BNRYWwtrTq63oKqMyXpJ7w9HWA==",
|
||||
"requires": {
|
||||
"@psf/bch-js": "5.4.0",
|
||||
"@psf/bch-js": "6.2.1",
|
||||
"apidoc": "0.25.0",
|
||||
"bch-consumer": "1.2.0",
|
||||
"bch-donation": "1.1.1",
|
||||
@@ -35681,6 +35751,14 @@
|
||||
"escape-goat": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"pushdata-bitcoin": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "http://94.130.170.209:4873/pushdata-bitcoin/-/pushdata-bitcoin-1.0.1.tgz",
|
||||
"integrity": "sha1-FZMdPNlnreUiBvUjqnMxrvfUOvc=",
|
||||
"requires": {
|
||||
"bitcoin-ops": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"q": {
|
||||
"version": "1.5.1",
|
||||
"dev": true
|
||||
|
||||
+5
-4
@@ -6,9 +6,9 @@
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"test": "npm run test:all",
|
||||
"test:all": "export BCH_DEX=test && nyc --reporter=text mocha --exit --timeout 15000 --recursive test/unit test/e2e/automated/",
|
||||
"test:all": "export BCH_DEX=test && nyc --reporter=text mocha --exit --timeout 30000 --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:e2e:auto": "export BCH_DEX=test && mocha --exit --timeout 30000 test/e2e/automated/",
|
||||
"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",
|
||||
@@ -27,10 +27,11 @@
|
||||
},
|
||||
"repository": "Permissionless-Software-Foundation/bch-dex",
|
||||
"dependencies": {
|
||||
"@psf/bch-js": "5.4.0",
|
||||
"@psf/bch-js": "6.2.1",
|
||||
"axios": "0.21.1",
|
||||
"bch-message-lib": "2.1.4",
|
||||
"bcryptjs": "2.4.3",
|
||||
"bitcoincashjs-lib": "3.3.3",
|
||||
"glob": "7.1.6",
|
||||
"ipfs": "0.60.0",
|
||||
"ipfs-coord": "7.1.6",
|
||||
@@ -51,7 +52,7 @@
|
||||
"koa2-ratelimit": "0.9.1",
|
||||
"libp2p": "^0.36.2",
|
||||
"line-reader": "0.4.0",
|
||||
"minimal-slp-wallet": "4.4.3",
|
||||
"minimal-slp-wallet": "4.4.5",
|
||||
"mongoose": "5.13.13",
|
||||
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
|
||||
"nodemailer": "6.4.17",
|
||||
|
||||
+24
-24
@@ -40,30 +40,30 @@ class Bch {
|
||||
}
|
||||
|
||||
// Gets the total psf token balance
|
||||
async getPSFTokenBalance (slpAddress) {
|
||||
try {
|
||||
if (!slpAddress || typeof slpAddress !== 'string') {
|
||||
throw new Error('slpAddress must be a string')
|
||||
}
|
||||
let psfBalance = 0
|
||||
const balances = await this.bchjs.SLP.Utils.balancesForAddress(
|
||||
slpAddress
|
||||
)
|
||||
|
||||
// Sums all the balances of all tokens
|
||||
// that match the psf token ID
|
||||
for (let i = 0; i < balances.length; i++) {
|
||||
if (balances[i].tokenId === this.PSF_TOKEN_ID) {
|
||||
psfBalance += balances[i].balance
|
||||
}
|
||||
}
|
||||
|
||||
return psfBalance
|
||||
} catch (err) {
|
||||
console.error('Error in bch.js/getPSFTokenBalance()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
// async getPSFTokenBalance (slpAddress) {
|
||||
// try {
|
||||
// if (!slpAddress || typeof slpAddress !== 'string') {
|
||||
// throw new Error('slpAddress must be a string')
|
||||
// }
|
||||
// let psfBalance = 0
|
||||
// const balances = await this.bchjs.SLP.Utils.balancesForAddress(
|
||||
// slpAddress
|
||||
// )
|
||||
//
|
||||
// // Sums all the balances of all tokens
|
||||
// // that match the psf token ID
|
||||
// for (let i = 0; i < balances.length; i++) {
|
||||
// if (balances[i].tokenId === this.PSF_TOKEN_ID) {
|
||||
// psfBalance += balances[i].balance
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return psfBalance
|
||||
// } catch (err) {
|
||||
// console.error('Error in bch.js/getPSFTokenBalance()')
|
||||
// throw err
|
||||
// }
|
||||
// }
|
||||
|
||||
async getMerit (slpAddr) {
|
||||
try {
|
||||
|
||||
@@ -14,6 +14,8 @@ const Offer = new mongoose.Schema({
|
||||
minUnitsToExchange: { type: String },
|
||||
p2wdbTxid: { type: String },
|
||||
p2wdbHash: { type: String },
|
||||
offerStatus: { type: String },
|
||||
makerAddr: { type: String },
|
||||
|
||||
// Authentication data
|
||||
signature: { type: String },
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
Order Model. Orders are 'internal' to the system and track the HD wallet index
|
||||
that contains funds for that Order. This is in contrast to Offers, which
|
||||
are 'external' to the system and mirrored by every instance of bch-dex.
|
||||
|
||||
See the dev-docs/specification.md for details on each property.
|
||||
*/
|
||||
|
||||
const mongoose = require('mongoose')
|
||||
@@ -19,6 +21,7 @@ const Order = new mongoose.Schema({
|
||||
minUnitsToExchange: { type: String },
|
||||
p2wdbTxid: { type: String },
|
||||
p2wdbHash: { type: String },
|
||||
makerAddr: { type: String },
|
||||
|
||||
// Authentication data
|
||||
signature: { type: String },
|
||||
|
||||
+204
-39
@@ -4,6 +4,7 @@
|
||||
|
||||
// Public npm libraries
|
||||
const BchWallet = require('minimal-slp-wallet/index')
|
||||
const bitcoinJs = require('bitcoincashjs-lib')
|
||||
|
||||
// Local libraries
|
||||
const JsonFiles = require('./json-files')
|
||||
@@ -21,6 +22,7 @@ class WalletAdapter {
|
||||
this.WALLET_FILE = WALLET_FILE
|
||||
this.BchWallet = BchWallet
|
||||
this.config = config
|
||||
this.bitcoinJs = bitcoinJs
|
||||
}
|
||||
|
||||
// Open the wallet file, or create one if the file doesn't exist.
|
||||
@@ -84,6 +86,7 @@ class WalletAdapter {
|
||||
|
||||
// Wait for wallet to initialize.
|
||||
await this.bchWallet.walletInfoPromise
|
||||
console.log('BCH wallet initialized.')
|
||||
|
||||
return this.bchWallet
|
||||
} catch (err) {
|
||||
@@ -122,9 +125,12 @@ class WalletAdapter {
|
||||
// and the index of the HD wallet that the key pair was generated from.
|
||||
// TODO: Allow input integer. If input is used, use that as the index. If no
|
||||
// input is provided, then call incrementNextAddress().
|
||||
async getKeyPair () {
|
||||
async getKeyPair (hdIndex = 0) {
|
||||
try {
|
||||
const hdIndex = await this.incrementNextAddress()
|
||||
if (!hdIndex) {
|
||||
// Increment the HD index and generate a new key pair.
|
||||
hdIndex = await this.incrementNextAddress()
|
||||
}
|
||||
|
||||
const mnemonic = this.bchWallet.walletInfo.mnemonic
|
||||
|
||||
@@ -178,10 +184,10 @@ class WalletAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a partial transcation to *take* a 'sell' order.
|
||||
async generatePartialTx (orderInfo) {
|
||||
// Generate a partial transcation to *take* a 'sell' offer.
|
||||
async generatePartialTx (offerInfo, utxoInfo) {
|
||||
try {
|
||||
console.log(`orderInfo: ${JSON.stringify(orderInfo, null, 2)}`)
|
||||
console.log(`offerInfo: ${JSON.stringify(offerInfo, null, 2)}`)
|
||||
|
||||
const bchjs = this.bchWallet.bchjs
|
||||
|
||||
@@ -190,21 +196,21 @@ class WalletAdapter {
|
||||
|
||||
// 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)
|
||||
// const bchUtxos = this.bchWallet.utxos.utxoStore.bchUtxos
|
||||
// const paymentUtxo = await bchjs.Utxo.findBiggestUtxo(bchUtxos)
|
||||
// console.log('paymentUtxo: ', paymentUtxo)
|
||||
|
||||
// Get token info on the offered UTXO
|
||||
const txData = await this.bchWallet.getTxData([orderInfo.utxoTxid])
|
||||
console.log(`txData: ${JSON.stringify(txData, null, 2)}`)
|
||||
const txData = await this.bchWallet.getTxData([offerInfo.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,
|
||||
txid: offerInfo.utxoTxid,
|
||||
vout: offerInfo.utxoVout,
|
||||
tokenId: offerInfo.tokenId,
|
||||
decimals: txData[0].tokenDecimals,
|
||||
tokenQty: orderInfo.numTokens.toString()
|
||||
tokenQty: offerInfo.numTokens.toString()
|
||||
}
|
||||
console.log(`offeredUtxo: ${JSON.stringify(offeredUtxo, null, 2)}`)
|
||||
|
||||
@@ -212,7 +218,7 @@ class WalletAdapter {
|
||||
// Generate the OP_RETURN code.
|
||||
const slpSendObj = bchjs.SLP.TokenType1.generateSendOpReturn(
|
||||
[offeredUtxo],
|
||||
orderInfo.numTokens.toString()
|
||||
offerInfo.numTokens.toString()
|
||||
)
|
||||
const slpData = slpSendObj.script
|
||||
console.log(`slpOutputs: ${slpSendObj.outputs}`)
|
||||
@@ -226,32 +232,34 @@ class WalletAdapter {
|
||||
}
|
||||
|
||||
// Calculate sats needed to pay the offer.
|
||||
const satsNeeded = orderInfo.numTokens * parseInt(orderInfo.rateInSats)
|
||||
const satsNeeded = offerInfo.numTokens * parseInt(offerInfo.rateInBaseUnit)
|
||||
if (isNaN(satsNeeded)) throw new Error('Can not calculate needed sats')
|
||||
|
||||
// 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
|
||||
// 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}`)
|
||||
}
|
||||
// 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 UTXO for sale (STILL CANNOT SPEND - not signed yet)
|
||||
transactionBuilder.addInput(offerInfo.utxoTxid, offerInfo.utxoVout)
|
||||
|
||||
// add payment UTXO
|
||||
transactionBuilder.addInput(paymentUtxo.tx_hash, paymentUtxo.tx_pos)
|
||||
// transactionBuilder.addInput(paymentUtxo.tx_hash, paymentUtxo.tx_pos)
|
||||
transactionBuilder.addInput(utxoInfo.txid, utxoInfo.vout)
|
||||
|
||||
const originalAmount = paymentUtxo.value
|
||||
// const originalAmount = paymentUtxo.value
|
||||
const dust = 546
|
||||
const remainder = originalAmount - satsNeeded - dust // exchange fee + token UTXO dust
|
||||
// const remainder = originalAmount - satsNeeded - dust // exchange fee + token UTXO dust
|
||||
|
||||
// Add the SLP OP_RETURN data as the first output.
|
||||
transactionBuilder.addOutput(slpData, 0)
|
||||
@@ -266,20 +274,18 @@ class WalletAdapter {
|
||||
)
|
||||
|
||||
// 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]
|
||||
const sellerAddr = offerInfo.makerAddr
|
||||
|
||||
// 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)
|
||||
}
|
||||
// if (remainder > 550) {
|
||||
// transactionBuilder.addOutput(buyerAddr, remainder)
|
||||
// }
|
||||
|
||||
const buyerECPair = bchjs.ECPair.fromWIF(this.bchWallet.walletInfo.privateKey)
|
||||
// const buyerECPair = bchjs.ECPair.fromWIF(this.bchWallet.walletInfo.privateKey)
|
||||
const buyerECPair = bchjs.ECPair.fromWIF(utxoInfo.wif)
|
||||
|
||||
// Sign the buyers input UTXO for spending.
|
||||
transactionBuilder.sign(
|
||||
@@ -287,13 +293,13 @@ class WalletAdapter {
|
||||
buyerECPair,
|
||||
null,
|
||||
transactionBuilder.hashTypes.SIGHASH_ALL,
|
||||
originalAmount
|
||||
utxoInfo.sats
|
||||
)
|
||||
|
||||
const tx = transactionBuilder.transaction.buildIncomplete()
|
||||
|
||||
const hex = tx.toHex()
|
||||
console.log('hex: ', hex)
|
||||
// console.log('hex: ', hex)
|
||||
|
||||
return hex
|
||||
} catch (err) {
|
||||
@@ -303,6 +309,165 @@ class WalletAdapter {
|
||||
|
||||
// return true
|
||||
}
|
||||
|
||||
// Move tokens to an address controlled by the HD wallet, to generate a
|
||||
// segregated UTXO.
|
||||
async moveTokens (inObj = {}) {
|
||||
try {
|
||||
const { tokenId, qty } = inObj
|
||||
|
||||
const keyPair = await this.getKeyPair()
|
||||
console.log('keyPair: ', keyPair)
|
||||
|
||||
const receiver = {
|
||||
address: keyPair.cashAddress,
|
||||
tokenId,
|
||||
qty
|
||||
}
|
||||
|
||||
// Update the UTXO store of the wallet.
|
||||
await this.bchWallet.getUtxos()
|
||||
|
||||
const txid = await this.bchWallet.sendTokens(receiver, 3)
|
||||
|
||||
const utxoInfo = {
|
||||
txid,
|
||||
vout: 1,
|
||||
hdIndex: keyPair.hdIndex
|
||||
}
|
||||
|
||||
return utxoInfo
|
||||
} catch (err) {
|
||||
console.error('Error in wallet.js/moveTokens()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Move a quanity of BCH to an address controlled by the HD wallet, to
|
||||
// generate a segregated UTXO.
|
||||
async moveBch (amountSat) {
|
||||
try {
|
||||
const keyPair = await this.getKeyPair()
|
||||
console.log('keyPair: ', keyPair)
|
||||
|
||||
const receivers = [{
|
||||
address: keyPair.cashAddress,
|
||||
amountSat
|
||||
}]
|
||||
console.log(`receivers: ${JSON.stringify(receivers, null, 2)}`)
|
||||
|
||||
// Update the UTXO store of the wallet.
|
||||
await this.bchWallet.getUtxos()
|
||||
|
||||
const txid = await this.bchWallet.send(receivers)
|
||||
|
||||
const utxoInfo = {
|
||||
txid,
|
||||
vout: 0,
|
||||
hdIndex: keyPair.hdIndex,
|
||||
wif: keyPair.wif
|
||||
}
|
||||
|
||||
return utxoInfo
|
||||
} catch (err) {
|
||||
console.error('Error in wallet.js/moveBch()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Deserialize a hex string representing a partial TX. Returns an object
|
||||
// representing the transaction.
|
||||
async deseralizeTx (txHex) {
|
||||
try {
|
||||
// Ensure the URL points at FullStack.cash, since the web 3 infra does not
|
||||
// yet support this call.
|
||||
const oldUrl = this.bchWallet.bchjs.RawTransactions.restURL
|
||||
this.bchWallet.bchjs.RawTransactions.restURL = 'https://api.fullstack.cash/v5/'
|
||||
|
||||
// Use a full node to deserialize the transaction.
|
||||
const txObj2 = await this.bchWallet.bchjs.RawTransactions.decodeRawTransaction(txHex)
|
||||
// console.log(`txObj2: ${JSON.stringify(txObj2, null, 2)}`)
|
||||
|
||||
// Restore the old URL.
|
||||
this.bchWallet.bchjs.RawTransactions.restURL = oldUrl
|
||||
|
||||
return txObj2
|
||||
} catch (err) {
|
||||
console.error('Error in wallet.js/deserializePartialTx()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Complete the partially signed transaction by signing the first input,
|
||||
// then broadcasting the transaction to the network.
|
||||
async completeTx (hex, hdIndex) {
|
||||
try {
|
||||
// console.log('hex: ', hex)
|
||||
|
||||
const bchjs = this.bchWallet.bchjs
|
||||
|
||||
// instance of transaction builder
|
||||
// const transactionBuilder = new bchjs.TransactionBuilder()
|
||||
|
||||
// Convert the hex string version of the transaction into a Buffer.
|
||||
// const paymentBuffer = Buffer.from(hex, 'hex')
|
||||
|
||||
// Generate a Transaction object from the transaction binary data.
|
||||
// const csTransaction = this.bitcoinJs.Transaction.fromBuffer(paymentBuffer)
|
||||
const csTransaction = this.bitcoinJs.Transaction.fromHex(hex)
|
||||
// console.log(`payment tx: ${JSON.stringify(csTransaction, null, 2)}`)
|
||||
|
||||
// Instantiate the Transaction Builder.
|
||||
const csTransactionBuilder = this.bitcoinJs.TransactionBuilder.fromTransaction(
|
||||
csTransaction,
|
||||
'mainnet'
|
||||
)
|
||||
// const csTransactionBuilder = bchjs.TransactionBuilder.fromTransaction(
|
||||
// csTransaction,
|
||||
// 'mainnet'
|
||||
// )
|
||||
|
||||
// Get the keypair for the address used in the Order
|
||||
const keyPair = await this.getKeyPair(hdIndex)
|
||||
console.log(`maker keyPair: ${JSON.stringify(keyPair, null, 2)}`)
|
||||
const makerECPair = bchjs.ECPair.fromWIF(keyPair.wif)
|
||||
|
||||
// Assumption: segregated UTXO has a value of 546 sats. It might be better
|
||||
// to explicitly look this data up via the blockchain.
|
||||
const dust = 546
|
||||
|
||||
// Coutnersign the Maker's input, representing the tokens for sale.
|
||||
csTransactionBuilder.sign(
|
||||
0,
|
||||
makerECPair,
|
||||
null,
|
||||
this.bitcoinJs.Transaction.SIGHASH_ALL,
|
||||
// csTransactionBuilder.hashTypes.SIGHASH_ALL,
|
||||
dust
|
||||
)
|
||||
|
||||
// build tx
|
||||
const csTx = csTransactionBuilder.build()
|
||||
|
||||
// output rawhex
|
||||
const csTxHex = csTx.toHex()
|
||||
console.log(`Fully signed Tx hex: ${csTxHex}`)
|
||||
|
||||
// Debug: Display the fully signed TX
|
||||
const txObj = await this.deseralizeTx(csTxHex)
|
||||
console.log(`Final tx: ${JSON.stringify(txObj, null, 2)}`)
|
||||
|
||||
// return csTxHex
|
||||
|
||||
// Broadcast transaction to the network
|
||||
const txid = await this.bchWallet.ar.sendTx(csTxHex)
|
||||
|
||||
return txid
|
||||
} catch (err) {
|
||||
console.error('Error in wallet.js/completeTx()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WalletAdapter
|
||||
|
||||
@@ -14,6 +14,7 @@ const LogsRESTController = require('./logs')
|
||||
const EntryRouter = require('./entry')
|
||||
const OfferRouter = require('./offer')
|
||||
const OrderRouter = require('./order')
|
||||
const P2WDBRouter = require('./p2wdb')
|
||||
|
||||
class RESTControllers {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -65,6 +66,9 @@ class RESTControllers {
|
||||
|
||||
const orderRouter = new OrderRouter(dependencies)
|
||||
orderRouter.attach(app)
|
||||
|
||||
const p2wdbRouter = new P2WDBRouter(dependencies)
|
||||
p2wdbRouter.attach(app)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
REST API Controller library for the /offer route
|
||||
*/
|
||||
|
||||
// const { wlogger } = require('../../../adapters/wlogger')
|
||||
const { wlogger } = require('../../../adapters/wlogger')
|
||||
|
||||
let _this
|
||||
|
||||
@@ -65,19 +65,15 @@ class OfferRESTControllerLib {
|
||||
// listed in the offer.
|
||||
async takeOffer (ctx) {
|
||||
try {
|
||||
console.log('body: ', ctx.request.body)
|
||||
console.log('REST API controller, body: ', ctx.request.body)
|
||||
|
||||
const offerCid = ctx.request.body.offerCid
|
||||
|
||||
// Find the Offer.
|
||||
// const offerEntity = await _this.useCases.offer.findOffer(offerId)
|
||||
|
||||
// 'Take' the Offer.
|
||||
const hash = await _this.useCases.offer.takeOffer(offerCid)
|
||||
|
||||
ctx.body = { hash }
|
||||
} catch (err) {
|
||||
console.log('Error in takeOffer REST API handler.')
|
||||
wlogger.error('Error in takeOffer() REST API handler.')
|
||||
_this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
REST API Controller library for the /p2wdb route
|
||||
This route handles incoming data from the P2WDB webhook, and routes the data
|
||||
to the proper handler.
|
||||
*/
|
||||
|
||||
// const { wlogger } = require('../../../adapters/wlogger')
|
||||
|
||||
let _this
|
||||
|
||||
class P2WDBRESTControllerLib {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating /p2wdb REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating /p2wdb REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
// this.OrderModel = this.adapters.localdb.Order
|
||||
// this.userUseCases = this.useCases.user
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
// No api-doc documentation because this wont be a public endpoint
|
||||
async routeWebhook (ctx) {
|
||||
try {
|
||||
console.log('p2wdb REST API handler: body: ', ctx.request.body)
|
||||
|
||||
const dataType = ctx.request.body.data.dataType
|
||||
|
||||
if (dataType.includes('counter')) {
|
||||
// Detect and handle Counter Offers
|
||||
console.log('counter-offer data detected.')
|
||||
|
||||
const counterOffer = ctx.request.body
|
||||
|
||||
await _this.useCases.offer.acceptCounterOffer(counterOffer)
|
||||
} else if (dataType.includes('offer')) {
|
||||
// Detect and handle new Offers
|
||||
console.log('offer data detected')
|
||||
|
||||
const offerObj = ctx.request.body
|
||||
await _this.useCases.offer.createOffer(offerObj)
|
||||
} else {
|
||||
console.log('Could not route P2WDB webhook data.')
|
||||
}
|
||||
|
||||
// const orderObj = ctx.request.body.order
|
||||
//
|
||||
// const hash = await _this.useCases.order.createOrder(orderObj)
|
||||
//
|
||||
// ctx.body = { hash }
|
||||
|
||||
ctx.body = {
|
||||
success: true
|
||||
}
|
||||
} 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)
|
||||
|
||||
// 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 = P2WDBRESTControllerLib
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
REST API library for /p2wdb route.
|
||||
This route handles incoming data from the P2WDB webhook, and routes the data
|
||||
to the proper handler.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
const Router = require('koa-router')
|
||||
|
||||
// Local libraries.
|
||||
const P2WDBRESTControllerLib = require('./controller')
|
||||
|
||||
let _this
|
||||
|
||||
class P2WDBRouter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating /p2wdb REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating /p2wdb REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
const dependencies = {
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
// Encapsulate dependencies.
|
||||
this.p2wdbRESTController = new P2WDBRESTControllerLib(dependencies)
|
||||
|
||||
// Instantiate the router and set the base route.
|
||||
const baseUrl = '/p2wdb'
|
||||
this.router = new Router({ prefix: baseUrl })
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
attach (app) {
|
||||
if (!app) {
|
||||
throw new Error(
|
||||
'Must pass app object when attached REST API controllers.'
|
||||
)
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.post('/', _this.p2wdbRESTController.routeWebhook)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(_this.router.routes())
|
||||
app.use(_this.router.allowedMethods())
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = P2WDBRouter
|
||||
+34
-18
@@ -6,18 +6,30 @@
|
||||
|
||||
class OfferEntity {
|
||||
constructor () {
|
||||
this.orderStatus = ['posted', 'taken', 'completed']
|
||||
this.offerStatus = ['posted', 'taken', 'dead']
|
||||
}
|
||||
|
||||
validate (orderData = {}) {
|
||||
validate (offerData = {}) {
|
||||
// Throw an error if input object does not have a data property
|
||||
if (!orderData.data) {
|
||||
if (!offerData.data) {
|
||||
throw new Error(
|
||||
'Input to order.validate() must be an object with a data property.'
|
||||
'Input to offer.validate() must be an object with a data property.'
|
||||
)
|
||||
}
|
||||
|
||||
const { messageType, messageClass, tokenId, buyOrSell, rateInSats, minSatsToExchange, numTokens, utxoTxid, utxoVout, orderStatus } = orderData.data
|
||||
const {
|
||||
messageType,
|
||||
messageClass,
|
||||
tokenId,
|
||||
buyOrSell,
|
||||
rateInBaseUnit,
|
||||
minUnitsToExchange,
|
||||
numTokens,
|
||||
utxoTxid,
|
||||
utxoVout,
|
||||
offerStatus,
|
||||
makerAddr
|
||||
} = offerData.data
|
||||
|
||||
// Input Validation
|
||||
if (!messageType || typeof messageType !== 'number') {
|
||||
@@ -32,11 +44,11 @@ class OfferEntity {
|
||||
if (!buyOrSell || typeof buyOrSell !== 'string') {
|
||||
throw new Error("Property 'buyOrSell' must be a string.")
|
||||
}
|
||||
if (!rateInSats || typeof rateInSats !== 'number') {
|
||||
throw new Error("Property 'rateInSats' must be an integer number.")
|
||||
if (!rateInBaseUnit || typeof rateInBaseUnit !== 'number') {
|
||||
throw new Error("Property 'rateInBaseUnit' must be an integer number.")
|
||||
}
|
||||
if (!minSatsToExchange || typeof minSatsToExchange !== 'number') {
|
||||
throw new Error("Property 'minSatsToExchange' must be an integer number.")
|
||||
if (!minUnitsToExchange || typeof minUnitsToExchange !== 'number') {
|
||||
throw new Error("Property 'minUnitsToExchange' must be an integer number.")
|
||||
}
|
||||
if (!numTokens || typeof numTokens !== 'number') {
|
||||
throw new Error("Property 'numTokens' must be a number.")
|
||||
@@ -47,8 +59,11 @@ class OfferEntity {
|
||||
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")
|
||||
if (offerStatus && !this.offerStatus.includes(offerStatus)) {
|
||||
throw new Error("Property 'offerStatus' must be posted, taken, or dead")
|
||||
}
|
||||
if (!makerAddr || typeof makerAddr !== 'string') {
|
||||
throw new Error("Property 'makerAddr' must be a string.")
|
||||
}
|
||||
|
||||
const validatedOfferData = {
|
||||
@@ -56,16 +71,17 @@ class OfferEntity {
|
||||
messageClass,
|
||||
tokenId,
|
||||
buyOrSell,
|
||||
rateInSats,
|
||||
minSatsToExchange,
|
||||
rateInBaseUnit,
|
||||
minUnitsToExchange,
|
||||
numTokens,
|
||||
utxoTxid,
|
||||
utxoVout,
|
||||
timestamp: orderData.timestamp,
|
||||
localTimestamp: orderData.localTimeStamp,
|
||||
txid: orderData.txid,
|
||||
p2wdbHash: orderData.hash,
|
||||
orderStatus: orderStatus || this.orderStatus[0]
|
||||
timestamp: offerData.timestamp,
|
||||
localTimestamp: offerData.localTimeStamp,
|
||||
txid: offerData.txid,
|
||||
p2wdbHash: offerData.hash,
|
||||
offerStatus: offerStatus || this.offerStatus[0],
|
||||
makerAddr
|
||||
}
|
||||
|
||||
return validatedOfferData
|
||||
|
||||
+15
-10
@@ -13,9 +13,10 @@ class Order {
|
||||
messageClass,
|
||||
tokenId,
|
||||
buyOrSell,
|
||||
rateInSats,
|
||||
minSatsToExchange,
|
||||
numTokens
|
||||
rateInBaseUnit,
|
||||
minUnitsToExchange,
|
||||
numTokens,
|
||||
makerAddr
|
||||
} = data
|
||||
|
||||
// Input Validation
|
||||
@@ -31,24 +32,28 @@ class Order {
|
||||
if (!buyOrSell || typeof buyOrSell !== 'string') {
|
||||
throw new Error("Property 'buyOrSell' must be a string.")
|
||||
}
|
||||
if (!rateInSats || typeof rateInSats !== 'number') {
|
||||
throw new Error("Property 'rateInSats' must be an integer number.")
|
||||
if (!rateInBaseUnit || typeof rateInBaseUnit !== 'number') {
|
||||
throw new Error("Property 'rateInBaseUnit' must be an integer number.")
|
||||
}
|
||||
if (!minSatsToExchange || typeof minSatsToExchange !== 'number') {
|
||||
throw new Error("Property 'minSatsToExchange' must be an integer number.")
|
||||
if (!minUnitsToExchange || typeof minUnitsToExchange !== 'number') {
|
||||
throw new Error("Property 'minUnitsToExchange' must be an integer number.")
|
||||
}
|
||||
if (!numTokens || typeof numTokens !== 'number') {
|
||||
throw new Error("Property 'numTokens' must be a number.")
|
||||
}
|
||||
if (!makerAddr || typeof makerAddr !== 'string') {
|
||||
throw new Error("Property 'makerAddr' must be a string.")
|
||||
}
|
||||
|
||||
const offerData = {
|
||||
messageType,
|
||||
messageClass,
|
||||
tokenId,
|
||||
buyOrSell,
|
||||
rateInSats,
|
||||
minSatsToExchange,
|
||||
numTokens
|
||||
rateInBaseUnit,
|
||||
minUnitsToExchange,
|
||||
numTokens,
|
||||
makerAddr
|
||||
}
|
||||
|
||||
return offerData
|
||||
|
||||
@@ -30,10 +30,10 @@ class EntryLib {
|
||||
}
|
||||
|
||||
// Verify psf tokens balance
|
||||
|
||||
const psfBalance = await this.bch.getPSFTokenBalance(
|
||||
entryEntity.slpAddress
|
||||
)
|
||||
// const psfBalance = await this.bch.getPSFTokenBalance(
|
||||
// entryEntity.slpAddress
|
||||
// )
|
||||
const psfBalance = 1
|
||||
|
||||
if (psfBalance < 10) {
|
||||
throw new Error('Insufficient psf balance')
|
||||
|
||||
@@ -21,8 +21,9 @@ class UseCases {
|
||||
// console.log('use-cases/index.js localConfig: ', localConfig)
|
||||
this.user = new UserUseCases(localConfig)
|
||||
this.entry = new EntryUseCases(localConfig)
|
||||
this.offer = new OfferUseCases(localConfig)
|
||||
this.order = new OrderUseCases(localConfig)
|
||||
localConfig.order = this.order
|
||||
this.offer = new OfferUseCases(localConfig)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
/*
|
||||
Use Case library for Offers.
|
||||
|
||||
Offers are created by a webhook trigger from the P2WDB. Offers are a result of
|
||||
new data in P2WDB. They differ from Offers, which are generated by a local
|
||||
user.
|
||||
|
||||
An Offer is created to match a local Offer, but it's created indirectly, as
|
||||
a response to the webhook from the P2WDB. In this way, Offers generated from
|
||||
local Offers are no different than Offers generated by other peers.
|
||||
*/
|
||||
|
||||
A Counter Offer is created by calling the /take/:cid endpoint. It creates
|
||||
a partially signed transaction.
|
||||
*/
|
||||
|
||||
// Local libraries
|
||||
const OfferEntity = require('../../entities/offer')
|
||||
@@ -21,6 +26,12 @@ class OfferUseCases {
|
||||
'Instance of adapters must be passed in when instantiating Offer Use Cases library.'
|
||||
)
|
||||
}
|
||||
this.orderUseCase = localConfig.order
|
||||
if (!this.orderUseCase) {
|
||||
throw new Error(
|
||||
'Instance of Order Use Cases must be passed in when instantiating Offer Use Cases library.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.config = config
|
||||
@@ -76,7 +87,12 @@ class OfferUseCases {
|
||||
|
||||
// Generate phase 2 of 3 - take the other side of an Offer.
|
||||
// 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
|
||||
// https://github.com/Permission=less-Software-Foundation/bch-js-examples/blob/master/bch/applications/collaborate/sell-slp/e2e-exchange/step2-purchase-tx.js
|
||||
//
|
||||
// Dev Note: Right now this use cases takes all the tokens offered. It does not
|
||||
// provide functionality to take less than the total amount of tokens offered
|
||||
// (offerInfo.numTokens). Taking less than the offered amount will be added
|
||||
// in the future.
|
||||
async takeOffer (offerCid) {
|
||||
try {
|
||||
console.log('offerCid: ', offerCid)
|
||||
@@ -86,8 +102,8 @@ class OfferUseCases {
|
||||
console.log(`offerInfo: ${JSON.stringify(offerInfo, null, 2)}`)
|
||||
|
||||
// Ensure the offer is in a 'posted' state and not already 'taken'
|
||||
if (offerInfo.offerStatus && offerInfo.offerStatus !== 'posted') {
|
||||
throw new Error('offer already taken')
|
||||
if (!offerInfo.offerStatus || offerInfo.offerStatus !== 'posted') {
|
||||
throw new Error('offer status is not "posted", so offer is dead and can not be countered.')
|
||||
}
|
||||
|
||||
// Verify that UTXO for sale is unspent. Abort if it's been spent.
|
||||
@@ -100,6 +116,9 @@ class OfferUseCases {
|
||||
console.log('utxoStatus: ', utxoStatus)
|
||||
if (utxoStatus === null) {
|
||||
console.log(`utxo txid: ${txid}, vout: ${vout}`)
|
||||
|
||||
// TODO: Mark this Offer as 'dead'
|
||||
|
||||
throw new Error('UTXO does not exist. Aborting.')
|
||||
}
|
||||
|
||||
@@ -110,20 +129,40 @@ class OfferUseCases {
|
||||
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 offer
|
||||
// Calculate amount of sats to generate a counter offer.
|
||||
let satsToMove = offerInfo.numTokens * parseInt(offerInfo.rateInBaseUnit)
|
||||
if (isNaN(satsToMove)) {
|
||||
throw new Error('Could not calculate the amount of BCH to generate counter offer')
|
||||
}
|
||||
|
||||
// Add sats to cover mining fees and dust for token UTXO
|
||||
satsToMove += 1000
|
||||
|
||||
// Move funds to create a segrated UTXO for taking the offer
|
||||
const utxoInfo = await this.adapters.wallet.moveBch(satsToMove)
|
||||
utxoInfo.sats = satsToMove
|
||||
console.log('utxoInfo: ', utxoInfo)
|
||||
|
||||
// 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(offerInfo)
|
||||
const partialTxHex = await this.adapters.wallet.generatePartialTx(offerInfo, utxoInfo)
|
||||
console.log('partialTxHex: ', partialTxHex)
|
||||
// return partialTxHex
|
||||
|
||||
// Debug: Decode the transaction for manual QA
|
||||
const txObj = await this.adapters.wallet.deseralizeTx(partialTxHex)
|
||||
console.log(`partially-signed transaction: ${JSON.stringify(txObj, null, 2)}`)
|
||||
|
||||
// Create valid Offer object
|
||||
const takenOfferInfo = Object.assign({}, offerInfo)
|
||||
takenOfferInfo.patialTxHex = partialTxHex
|
||||
takenOfferInfo.partialTxHex = partialTxHex
|
||||
delete takenOfferInfo.p2wdbHash
|
||||
delete takenOfferInfo._id
|
||||
takenOfferInfo.offerHash = offerInfo.p2wdbHash
|
||||
|
||||
// Add P2WDB specific flag for signaling that this is a new offer.
|
||||
takenOfferInfo.dataType = 'counter-offer'
|
||||
|
||||
// Write offer info to the P2WDB
|
||||
// TODO: This will trigger the webhook. Find some way of triggering the
|
||||
// webhook on new offers, but not on counteroffers
|
||||
@@ -136,6 +175,8 @@ class OfferUseCases {
|
||||
|
||||
// Return the P2WDB CID
|
||||
return hash
|
||||
|
||||
// return 'fake-hash'
|
||||
} catch (err) {
|
||||
console.error('Error in use-cases/offer/takeOffer(): ', err)
|
||||
throw err
|
||||
@@ -150,6 +191,7 @@ class OfferUseCases {
|
||||
// console.log(`walletInfo: ${JSON.stringify(this.adapters.wallet.bchWallet.walletInfo, null, 2)}`)
|
||||
|
||||
await this.adapters.wallet.bchWallet.walletInfoPromise
|
||||
// console.log(`utxos: ${JSON.stringify(this.adapters.wallet.bchWallet.utxos.utxoStore, null, 2)}`)
|
||||
|
||||
// Ensure the app wallet has enough funds to write to the P2WDB.
|
||||
const wif = this.adapters.wallet.bchWallet.walletInfo.privateKey
|
||||
@@ -159,12 +201,19 @@ class OfferUseCases {
|
||||
if (offerEntity.buyOrSell.includes('sell')) {
|
||||
// Sell Offer
|
||||
|
||||
// Calculate the sats needed
|
||||
const satsNeeded = offerEntity.numTokens * parseInt(offerEntity.rateInBaseUnit)
|
||||
if (isNaN(satsNeeded)) {
|
||||
throw new Error('Could not calculate sats needed!')
|
||||
}
|
||||
|
||||
// Ensure the app wallet controlls enough BCH to pay for the tokens.
|
||||
const satsNeeded = offerEntity.numTokens * parseInt(offerEntity.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.') }
|
||||
if (satsNeeded + SATS_MARGIN > balance) {
|
||||
throw new Error('App wallet does not control enough BCH to purchase the tokens.')
|
||||
}
|
||||
|
||||
//
|
||||
} else {
|
||||
@@ -200,6 +249,56 @@ class OfferUseCases {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// This function is called by the P2WDB webhook REST API handler. When a
|
||||
// Counter Offer is passed to bch-dex by the P2WDB, the data is then passed
|
||||
// to this function. It does due dilligence on the Counter Offer, then signs
|
||||
// and broadcasts the transaction to accept the Counter Offer.
|
||||
async acceptCounterOffer (p2wdbData) {
|
||||
try {
|
||||
console.log(`acceptCounterOffer() p2wdbData: ${JSON.stringify(p2wdbData, null, 2)}`)
|
||||
|
||||
// See if this instance of bch-dex is managing the Order associated with
|
||||
// the incoming Counter Offer.
|
||||
const orderHash = p2wdbData.data.offerHash
|
||||
const orderData = await this.orderUseCase.findOrderByHash(orderHash)
|
||||
console.log(`orderData: ${JSON.stringify(orderData, null, 2)}`)
|
||||
|
||||
// Deserialize the partially signed transaction.
|
||||
const txHex = p2wdbData.data.partialTxHex
|
||||
const txObj = await this.adapters.wallet.deseralizeTx(txHex)
|
||||
console.log(`txObj: ${JSON.stringify(txObj, null, 2)}`)
|
||||
|
||||
// Ensure the 3rd output (vout=2) contains the required amount of BCH.
|
||||
const satsToReceive = orderData.numTokens * parseInt(orderData.rateInBaseUnit)
|
||||
if (isNaN(satsToReceive)) {
|
||||
throw new Error('Could not calculate the amount of BCH offered in the Counter Offer')
|
||||
}
|
||||
const satsOut = this.adapters.wallet.bchWallet.bchjs.BitcoinCash.toSatoshi(txObj.vout[2].value)
|
||||
const hasRequiredAmount = satsOut === satsToReceive
|
||||
if (!hasRequiredAmount) {
|
||||
throw new Error(`The Counter Offer has an output of ${satsOut}, which does not match the required ${satsToReceive} in the Offer.`)
|
||||
}
|
||||
|
||||
// Ensure the 3rd output (vout=2) is going to the maker address specified
|
||||
// in the Offer.
|
||||
const addrInCounterOffer = txObj.vout[2].scriptPubKey.addresses[0]
|
||||
const makerAddr = orderData.makerAddr
|
||||
const hasCorrectAddr = makerAddr === addrInCounterOffer
|
||||
if (!hasCorrectAddr) {
|
||||
throw new Error(`The Counter Offer has an output address of ${addrInCounterOffer}, which does not match the Maker address of ${makerAddr} in the Offer.`)
|
||||
}
|
||||
|
||||
// Sign and broadcast the transaction.
|
||||
const txid = await this.adapters.wallet.completeTx(txHex, orderData.hdIndex)
|
||||
console.log('txid: ', txid)
|
||||
|
||||
return txid
|
||||
} catch (err) {
|
||||
console.error('Error in acceptCounterOffer()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OfferUseCases
|
||||
|
||||
+39
-33
@@ -27,7 +27,11 @@ class OrderLib {
|
||||
// Create a new order model and add it to the Mongo database.
|
||||
async createOrder (entryObj) {
|
||||
try {
|
||||
// console.log('createOrder(entryObj): ', entryObj)
|
||||
console.log('createOrder(entryObj): ', entryObj)
|
||||
|
||||
// Specify the address to send payment.
|
||||
entryObj.makerAddr = this.adapters.wallet.bchWallet.walletInfo.cashAddress
|
||||
console.log('entryObj.makerAddr: ', entryObj.makerAddr)
|
||||
|
||||
// Input Validation
|
||||
const orderEntity = this.orderEntity.validate(entryObj)
|
||||
@@ -37,7 +41,11 @@ class OrderLib {
|
||||
await this.ensureFunds(orderEntity)
|
||||
|
||||
// Move the tokens to holding address.
|
||||
const utxoInfo = await this.moveTokens(orderEntity)
|
||||
const moveObj = {
|
||||
tokenId: orderEntity.tokenId,
|
||||
qty: orderEntity.numTokens
|
||||
}
|
||||
const utxoInfo = await this.adapters.wallet.moveTokens(moveObj)
|
||||
console.log('utxoInfo: ', utxoInfo)
|
||||
|
||||
// Update the UTXO store for the wallet.
|
||||
@@ -47,7 +55,9 @@ class OrderLib {
|
||||
// Update the order with the new UTXO information.
|
||||
orderEntity.utxoTxid = utxoInfo.txid
|
||||
orderEntity.utxoVout = utxoInfo.vout
|
||||
orderEntity.hdIndex = utxoInfo.hdIndex
|
||||
|
||||
// Add P2WDB specific flag for signaling that this is a new offer.
|
||||
orderEntity.dataType = 'offer'
|
||||
|
||||
// Add order to P2WDB.
|
||||
const p2wdbObj = {
|
||||
@@ -59,6 +69,7 @@ class OrderLib {
|
||||
// console.log('hash: ', hash)
|
||||
|
||||
// Create a MongoDB model to hold the Order
|
||||
orderEntity.hdIndex = utxoInfo.hdIndex
|
||||
orderEntity.p2wdbHash = hash
|
||||
console.log(`creating new order model: ${JSON.stringify(orderEntity, null, 2)}`)
|
||||
const order = new this.OrderModel(orderEntity)
|
||||
@@ -67,36 +78,8 @@ class OrderLib {
|
||||
return hash
|
||||
} catch (err) {
|
||||
// console.log("Error in use-cases/entry.js/createEntry()", err.message)
|
||||
wlogger.error('Error in use-cases/entry.js/createOrder())')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Move the tokens indicated in the order to a temporary holding address.
|
||||
// This will generate the UTXO used in the Signal message. This function
|
||||
// moves the funds and returns the UTXO information.
|
||||
async moveTokens (orderEntity) {
|
||||
try {
|
||||
const keyPair = await this.adapters.wallet.getKeyPair()
|
||||
console.log('keyPair: ', keyPair)
|
||||
|
||||
const receiver = {
|
||||
address: keyPair.cashAddress,
|
||||
tokenId: orderEntity.tokenId,
|
||||
qty: orderEntity.numTokens
|
||||
}
|
||||
|
||||
const txid = await this.adapters.wallet.bchWallet.sendTokens(receiver, 3)
|
||||
|
||||
const utxoInfo = {
|
||||
txid,
|
||||
vout: 1,
|
||||
hdIndex: keyPair.hdIndex
|
||||
}
|
||||
|
||||
return utxoInfo
|
||||
} catch (err) {
|
||||
console.error('Error in moveTokens(): ', err)
|
||||
wlogger.error('Error in use-cases/createOrder())')
|
||||
console.log('error entryObj: ', entryObj)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -152,6 +135,29 @@ class OrderLib {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve an Order model from the database. Find it by its P2WDB CID.
|
||||
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.offerEntity.validateFromModel(offerObject)
|
||||
|
||||
return orderObject
|
||||
} catch (err) {
|
||||
console.error('Error in findOrder(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OrderLib
|
||||
|
||||
@@ -18,8 +18,8 @@ async function start () {
|
||||
tokenId:
|
||||
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
|
||||
buyOrSell: 'sell',
|
||||
rateInSats: 1000,
|
||||
minSatsToExchange: 1000,
|
||||
rateInBaseUnit: 5000,
|
||||
minUnitsToExchange: 5000,
|
||||
numTokens: 1
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Part 2 of 3: Take an order
|
||||
Part 2 of 3: Take an offer by submiting a counter-offer.
|
||||
*/
|
||||
|
||||
const axios = require('axios')
|
||||
@@ -10,9 +10,9 @@ async function start () {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/order/take`,
|
||||
url: `${LOCALHOST}/offer/take`,
|
||||
data: {
|
||||
orderCid: 'zdpuAkp98gTuivaNzGP31jTQi3ADXrFA6uANrceQcrQkTXy2j'
|
||||
offerCid: 'zdpuAkv76xbgqFEdFvcz28yNvtVjoZS7wsMo5wvuxWstycLRg'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
Integration tests for the bch.js adapter.
|
||||
*/
|
||||
|
||||
// Local libraries.
|
||||
// const BchAdapter = require('../../../src/adapters/bch')
|
||||
//
|
||||
// describe('#wallet', () => {
|
||||
// let uut
|
||||
//
|
||||
// beforeEach(() => {
|
||||
// uut = new BchAdapter()
|
||||
// })
|
||||
//
|
||||
// // describe('#getPSFTokenBalance', () => {
|
||||
// // it('should get the PSF balance for the root address of the wallet', async () => {
|
||||
// // await uut.wallet.walletInfoPromise
|
||||
// // console.log('bch.wallet.walletInfo: ', uut.wallet.walletInfo)
|
||||
// // })
|
||||
// // })
|
||||
// })
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
const assert = require('chai').assert
|
||||
// const assert = require('chai').assert
|
||||
|
||||
// Local libraries.
|
||||
const WalletAdapter = require('../../../src/adapters/wallet')
|
||||
@@ -15,27 +15,82 @@ describe('#wallet', () => {
|
||||
uut = new WalletAdapter()
|
||||
})
|
||||
|
||||
describe('#instanceWallet', () => {
|
||||
it('should instance the wallet using the web 3 infra by default', async () => {
|
||||
const walletData = await uut.openWallet()
|
||||
// console.log('walletData: ', walletData)
|
||||
// describe('#instanceWallet', () => {
|
||||
// it('should instance the wallet using the web 3 infra by default', async () => {
|
||||
// const walletData = await uut.openWallet()
|
||||
// // console.log('walletData: ', walletData)
|
||||
//
|
||||
// const walletInstance = await uut.instanceWallet(walletData)
|
||||
// // console.log('walletInstance: ', walletInstance)
|
||||
//
|
||||
// assert.equal(walletInstance.ar.interface, 'consumer-api')
|
||||
// })
|
||||
//
|
||||
// it('should instance using web 2 FullStack.cash infra', async () => {
|
||||
// const walletData = await uut.openWallet()
|
||||
//
|
||||
// // Force usage of FullStack.cash
|
||||
// uut.config.useFullStackCash = true
|
||||
//
|
||||
// const walletInstance = await uut.instanceWallet(walletData)
|
||||
// // console.log('walletInstance: ', walletInstance)
|
||||
//
|
||||
// assert.equal(walletInstance.ar.interface, 'rest-api')
|
||||
// })
|
||||
// })
|
||||
//
|
||||
// describe('#moveTokens', () => {
|
||||
// it('should move tokens to a new address in the HD wallet', async () => {
|
||||
// const walletData = await uut.openWallet()
|
||||
//
|
||||
// // Force usage of FullStack.cash
|
||||
// uut.config.useFullStackCash = true
|
||||
//
|
||||
// await uut.instanceWallet(walletData)
|
||||
//
|
||||
// const inObj = {
|
||||
// tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
|
||||
// qty: 1
|
||||
// }
|
||||
//
|
||||
// const result = await uut.moveTokens(inObj)
|
||||
// // console.log('result: ', result)
|
||||
//
|
||||
// assert.property(result, 'txid')
|
||||
// assert.property(result, 'vout')
|
||||
// assert.property(result, 'hdIndex')
|
||||
// })
|
||||
// })
|
||||
//
|
||||
// describe('#moveBch', () => {
|
||||
// it('should move BCH to a new address in the HD wallet', async () => {
|
||||
// const walletData = await uut.openWallet()
|
||||
//
|
||||
// // Force usage of FullStack.cash
|
||||
// uut.config.useFullStackCash = true
|
||||
//
|
||||
// await uut.instanceWallet(walletData)
|
||||
//
|
||||
// const amountSat = 1000
|
||||
//
|
||||
// const result = await uut.moveBch(amountSat)
|
||||
// console.log('result: ', result)
|
||||
// })
|
||||
// })
|
||||
|
||||
const walletInstance = await uut.instanceWallet(walletData)
|
||||
// console.log('walletInstance: ', walletInstance)
|
||||
|
||||
assert.equal(walletInstance.ar.interface, 'consumer-api')
|
||||
})
|
||||
|
||||
it('should instance using web 2 FullStack.cash infra', async () => {
|
||||
describe('#completeTx', () => {
|
||||
it('should complete the transaction with bchjs.TransactionBuilder', async () => {
|
||||
const walletData = await uut.openWallet()
|
||||
|
||||
// Force usage of FullStack.cash
|
||||
uut.config.useFullStackCash = true
|
||||
|
||||
const walletInstance = await uut.instanceWallet(walletData)
|
||||
// console.log('walletInstance: ', walletInstance)
|
||||
await uut.instanceWallet(walletData)
|
||||
|
||||
assert.equal(walletInstance.ar.interface, 'rest-api')
|
||||
const hex = '0200000002cf24c6f6e55fc84e7699223f7dac568aae991f1a49747758a797d06a516f718c0100000000ffffffff0c7a53c39a7f215c9fa0f409710cf5cb8926e40c104efcedb43d69d5e113fd9d000000006a47304402204d5d1ce837594a151ef8ccecf95b5c7ebb2e71560c6d2597fa559b92f9f032bc02206ef67cd2ef4b4df7207225c26c9982df184959ac389c3f49f1ff5dce641a72ee412103fdc2366a32184220e77fd50e7fff30e7c76b2b7246dea6d56b8177f9fca6fec7ffffffff030000000000000000376a04534c500001010453454e4420a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b208000000000000006422020000000000001976a914d46461bf6a5f3a8e0e0e92b85465ad79696ccb7688ac88130000000000001976a914d46461bf6a5f3a8e0e0e92b85465ad79696ccb7688ac00000000'
|
||||
|
||||
const result = await uut.completeTx(hex, 10)
|
||||
console.log('result: ', result)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -24,27 +24,6 @@ describe('#offer-use-case.js', () => {
|
||||
})
|
||||
|
||||
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',
|
||||
@@ -52,8 +31,8 @@ describe('#offer-use-case.js', () => {
|
||||
messageClass: 1,
|
||||
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
|
||||
buyOrSell: 'sell',
|
||||
rateInSats: 1000,
|
||||
minSatsToExchange: 1250,
|
||||
rateInBaseUnit: 1000,
|
||||
minUnitsToExchange: 1250,
|
||||
numTokens: 1
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const sinon = require('sinon')
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const mockData = require('../mocks/bchjs-mock')
|
||||
// const mockData = require('../mocks/bchjs-mock')
|
||||
|
||||
let sandbox
|
||||
let uut
|
||||
@@ -69,57 +69,58 @@ describe('bch', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getPSFTokenBalance', () => {
|
||||
it('should throw error if slpAddress is not provided', async () => {
|
||||
try {
|
||||
await uut.getPSFTokenBalance()
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'slpAddress must be a string')
|
||||
}
|
||||
})
|
||||
// describe('#getPSFTokenBalance', () => {
|
||||
// it('should throw error if slpAddress is not provided', async () => {
|
||||
// try {
|
||||
// await uut.getPSFTokenBalance()
|
||||
// assert.fail('Unexpected result')
|
||||
// } catch (err) {
|
||||
// assert.include(err.message, 'slpAddress must be a string')
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// it('should return psf tokens balance', async () => {
|
||||
// // Mock live network calls.
|
||||
// sandbox
|
||||
// .stub(uut.bchjs.SLP.Utils, 'balancesForAddress')
|
||||
// .resolves(mockData.psfBalances)
|
||||
//
|
||||
// const slpAddress =
|
||||
// 'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
|
||||
// const result = await uut.getPSFTokenBalance(slpAddress)
|
||||
//
|
||||
// assert.isNumber(result)
|
||||
// assert.equal(result, 2)
|
||||
// })
|
||||
//
|
||||
// it('should return 0 if the slp address does not has Psf tokens', async () => {
|
||||
// // Mock live network calls.
|
||||
// sandbox
|
||||
// .stub(uut.bchjs.SLP.Utils, 'balancesForAddress')
|
||||
// .resolves(mockData.noPsfBalances)
|
||||
//
|
||||
// const slpAddress =
|
||||
// 'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
|
||||
// const result = await uut.getPSFTokenBalance(slpAddress)
|
||||
//
|
||||
// assert.isNumber(result)
|
||||
// assert.equal(result, 0)
|
||||
// })
|
||||
//
|
||||
// it('should return 0 for empty balances', async () => {
|
||||
// // Mock live network calls.
|
||||
// sandbox.stub(uut.bchjs.SLP.Utils, 'balancesForAddress').resolves([])
|
||||
//
|
||||
// const slpAddress =
|
||||
// 'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
|
||||
// const result = await uut.getPSFTokenBalance(slpAddress)
|
||||
//
|
||||
// assert.isNumber(result)
|
||||
// assert.equal(result, 0)
|
||||
// })
|
||||
// })
|
||||
|
||||
it('should return psf tokens balance', async () => {
|
||||
// Mock live network calls.
|
||||
sandbox
|
||||
.stub(uut.bchjs.SLP.Utils, 'balancesForAddress')
|
||||
.resolves(mockData.psfBalances)
|
||||
|
||||
const slpAddress =
|
||||
'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
|
||||
const result = await uut.getPSFTokenBalance(slpAddress)
|
||||
|
||||
assert.isNumber(result)
|
||||
assert.equal(result, 2)
|
||||
})
|
||||
|
||||
it('should return 0 if the slp address does not has Psf tokens', async () => {
|
||||
// Mock live network calls.
|
||||
sandbox
|
||||
.stub(uut.bchjs.SLP.Utils, 'balancesForAddress')
|
||||
.resolves(mockData.noPsfBalances)
|
||||
|
||||
const slpAddress =
|
||||
'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
|
||||
const result = await uut.getPSFTokenBalance(slpAddress)
|
||||
|
||||
assert.isNumber(result)
|
||||
assert.equal(result, 0)
|
||||
})
|
||||
|
||||
it('should return 0 for empty balances', async () => {
|
||||
// Mock live network calls.
|
||||
sandbox.stub(uut.bchjs.SLP.Utils, 'balancesForAddress').resolves([])
|
||||
|
||||
const slpAddress =
|
||||
'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
|
||||
const result = await uut.getPSFTokenBalance(slpAddress)
|
||||
|
||||
assert.isNumber(result)
|
||||
assert.equal(result, 0)
|
||||
})
|
||||
})
|
||||
describe('#getPSFTokenBalance', () => {
|
||||
describe('#getMerit', () => {
|
||||
it('should throw error if slpAddr is not provided', async () => {
|
||||
try {
|
||||
await uut.getMerit()
|
||||
|
||||
@@ -253,6 +253,57 @@ describe('#wallet', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#moveTokens', () => {
|
||||
it('should move tokens to a new address in the HD wallet', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, 'getKeyPair').resolves({
|
||||
cashAddress: 'bitcoincash:qqsj63493jk4p05zzdgqzc29k5unqtet9vv8l4x0yt',
|
||||
wif: 'L4qKTMCwjH9jHnYtNh9Vsrxj7Hg6zmoN8E2v7N47UKvNVEjw7FU8',
|
||||
hdIndex: 6
|
||||
})
|
||||
uut.bchWallet = {
|
||||
sendTokens: async () => 'fake-txid',
|
||||
getUtxos: async () => {}
|
||||
}
|
||||
|
||||
const inObj = {
|
||||
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
|
||||
qty: 1
|
||||
}
|
||||
|
||||
const result = await uut.moveTokens(inObj)
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'txid')
|
||||
assert.property(result, 'vout')
|
||||
assert.property(result, 'hdIndex')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#moveBch', () => {
|
||||
it('should move BCH to a new address in the HD wallet', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, 'getKeyPair').resolves({
|
||||
cashAddress: 'bitcoincash:qqsj63493jk4p05zzdgqzc29k5unqtet9vv8l4x0yt',
|
||||
wif: 'L4qKTMCwjH9jHnYtNh9Vsrxj7Hg6zmoN8E2v7N47UKvNVEjw7FU8',
|
||||
hdIndex: 6
|
||||
})
|
||||
uut.bchWallet = {
|
||||
send: async () => 'fake-txid',
|
||||
getUtxos: async () => {}
|
||||
}
|
||||
|
||||
const amountSat = 1000
|
||||
|
||||
const result = await uut.moveBch(amountSat)
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'txid')
|
||||
assert.property(result, 'vout')
|
||||
assert.property(result, 'hdIndex')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const deleteFile = (filepath) => {
|
||||
|
||||
@@ -25,19 +25,23 @@ describe('#Offer-Entity', () => {
|
||||
it('should throw an error if data is not provided', () => {
|
||||
try {
|
||||
uut.validate()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(
|
||||
err.message,
|
||||
'Input to order.validate() must be an object with a data property.'
|
||||
'Input to offer.validate() must be an object with a data property.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if messageType is not included', () => {
|
||||
try {
|
||||
const orderData = { data: {} }
|
||||
uut.validate(orderData)
|
||||
const offerData = { data: {} }
|
||||
uut.validate(offerData)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(
|
||||
@@ -49,8 +53,10 @@ describe('#Offer-Entity', () => {
|
||||
|
||||
it('should throw an error if messageClass is not included', () => {
|
||||
try {
|
||||
const orderData = { data: { messageType: 1 } }
|
||||
uut.validate(orderData)
|
||||
const offerData = { data: { messageType: 1 } }
|
||||
uut.validate(offerData)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(
|
||||
@@ -62,8 +68,10 @@ describe('#Offer-Entity', () => {
|
||||
|
||||
it('should throw an error if tokenId is not included', () => {
|
||||
try {
|
||||
const orderData = { data: { messageType: 1, messageClass: 1 } }
|
||||
uut.validate(orderData)
|
||||
const offerData = { data: { messageType: 1, messageClass: 1 } }
|
||||
uut.validate(offerData)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, "Property 'tokenId' must be a string.")
|
||||
@@ -72,20 +80,22 @@ describe('#Offer-Entity', () => {
|
||||
|
||||
it('should throw an error if buyOrSell is not included', () => {
|
||||
try {
|
||||
const orderData = {
|
||||
const offerData = {
|
||||
data: { messageType: 1, messageClass: 1, tokenId: 'fakeId' }
|
||||
}
|
||||
|
||||
uut.validate(orderData)
|
||||
uut.validate(offerData)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, "Property 'buyOrSell' must be a string.")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if rateInSats is not included', () => {
|
||||
it('should throw an error if rateInBaseUnit is not included', () => {
|
||||
try {
|
||||
const orderData = {
|
||||
const offerData = {
|
||||
data: {
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
@@ -94,51 +104,57 @@ describe('#Offer-Entity', () => {
|
||||
}
|
||||
}
|
||||
|
||||
uut.validate(orderData)
|
||||
uut.validate(offerData)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(
|
||||
err.message,
|
||||
"Property 'rateInSats' must be an integer number."
|
||||
"Property 'rateInBaseUnit' must be an integer number."
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if minSatsToExchange is not included', () => {
|
||||
it('should throw an error if minUnitsToExchange is not included', () => {
|
||||
try {
|
||||
const orderData = {
|
||||
const offerData = {
|
||||
data: {
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
tokenId: 'fakeId',
|
||||
buyOrSell: 'buy',
|
||||
rateInSats: 1000
|
||||
rateInBaseUnit: 1000
|
||||
}
|
||||
}
|
||||
|
||||
uut.validate(orderData)
|
||||
uut.validate(offerData)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(
|
||||
err.message,
|
||||
"Property 'minSatsToExchange' must be an integer number."
|
||||
"Property 'minUnitsToExchange' must be an integer number."
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if numTokens is not included', () => {
|
||||
try {
|
||||
const orderData = {
|
||||
const offerData = {
|
||||
data: {
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
tokenId: 'fakeId',
|
||||
buyOrSell: 'buy',
|
||||
rateInSats: 1000,
|
||||
minSatsToExchange: 350
|
||||
rateInBaseUnit: 1000,
|
||||
minUnitsToExchange: 350
|
||||
}
|
||||
}
|
||||
uut.validate(orderData)
|
||||
uut.validate(offerData)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, "Property 'numTokens' must be a number.")
|
||||
@@ -147,18 +163,20 @@ describe('#Offer-Entity', () => {
|
||||
|
||||
it('should throw an error if utxoTxid is not included', () => {
|
||||
try {
|
||||
const orderData = {
|
||||
const offerData = {
|
||||
data: {
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
tokenId: 'fakeId',
|
||||
buyOrSell: 'buy',
|
||||
rateInSats: 1000,
|
||||
minSatsToExchange: 350,
|
||||
rateInBaseUnit: 1000,
|
||||
minUnitsToExchange: 350,
|
||||
numTokens: 1
|
||||
}
|
||||
}
|
||||
uut.validate(orderData)
|
||||
uut.validate(offerData)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, "Property 'utxoTxid' must be a string.")
|
||||
@@ -167,19 +185,21 @@ describe('#Offer-Entity', () => {
|
||||
|
||||
it('should throw an error if utxoVout is not included', () => {
|
||||
try {
|
||||
const orderData = {
|
||||
const offerData = {
|
||||
data: {
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
tokenId: 'fakeId',
|
||||
buyOrSell: 'buy',
|
||||
rateInSats: 1000,
|
||||
minSatsToExchange: 350,
|
||||
rateInBaseUnit: 1000,
|
||||
minUnitsToExchange: 350,
|
||||
numTokens: 1,
|
||||
utxoTxid: 'fakeTxid'
|
||||
}
|
||||
}
|
||||
uut.validate(orderData)
|
||||
uut.validate(offerData)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(
|
||||
@@ -189,8 +209,36 @@ describe('#Offer-Entity', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('should validate a new order', () => {
|
||||
const orderObj = {
|
||||
it('should throw an error if proper status is not applied', () => {
|
||||
try {
|
||||
const offerData = {
|
||||
data: {
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
tokenId: 'fakeId',
|
||||
buyOrSell: 'buy',
|
||||
rateInBaseUnit: 1000,
|
||||
minUnitsToExchange: 350,
|
||||
numTokens: 1,
|
||||
utxoTxid: 'fakeTxid',
|
||||
utxoVout: 0,
|
||||
offerStatus: 'badStatus'
|
||||
}
|
||||
}
|
||||
uut.validate(offerData)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(
|
||||
err.message,
|
||||
"Property 'offerStatus' must be posted, taken, or dead"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should validate a new offer', () => {
|
||||
const offerObj = {
|
||||
appId: 'swapTest555',
|
||||
data: {
|
||||
messageType: 1,
|
||||
@@ -198,12 +246,14 @@ describe('#Offer-Entity', () => {
|
||||
tokenId:
|
||||
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
|
||||
buyOrSell: 'sell',
|
||||
rateInSats: 1000,
|
||||
minSatsToExchange: 10,
|
||||
rateInBaseUnit: 1000,
|
||||
minUnitsToExchange: 10,
|
||||
numTokens: 0.02,
|
||||
utxoTxid:
|
||||
'241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
|
||||
utxoVout: 0
|
||||
utxoVout: 0,
|
||||
offerStatus: 'posted',
|
||||
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00'
|
||||
},
|
||||
timestamp: '2021-09-20T17:54:26.395Z',
|
||||
localTimeStamp: '9/20/2021, 10:54:26 AM',
|
||||
@@ -211,15 +261,15 @@ describe('#Offer-Entity', () => {
|
||||
hash: 'zdpuB2X25AZCKo3wpr4sSbw44vqPWJRqcxWQRHZccK5BdtoGD'
|
||||
}
|
||||
|
||||
const result = uut.validate(orderObj)
|
||||
const result = uut.validate(offerObj)
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'messageType')
|
||||
assert.property(result, 'messageClass')
|
||||
assert.property(result, 'tokenId')
|
||||
assert.property(result, 'buyOrSell')
|
||||
assert.property(result, 'rateInSats')
|
||||
assert.property(result, 'minSatsToExchange')
|
||||
assert.property(result, 'rateInBaseUnit')
|
||||
assert.property(result, 'minUnitsToExchange')
|
||||
assert.property(result, 'numTokens')
|
||||
assert.property(result, 'utxoTxid')
|
||||
assert.property(result, 'utxoVout')
|
||||
|
||||
@@ -77,7 +77,7 @@ describe('#Order-Entity', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if rateInSats is not included', () => {
|
||||
it('should throw an error if rateInBaseUnit is not included', () => {
|
||||
try {
|
||||
const data = {
|
||||
messageType: 1,
|
||||
@@ -90,26 +90,26 @@ describe('#Order-Entity', () => {
|
||||
// console.log(err)
|
||||
assert.include(
|
||||
err.message,
|
||||
"Property 'rateInSats' must be an integer number."
|
||||
"Property 'rateInBaseUnit' must be an integer number."
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if minSatsToExchange is not included', () => {
|
||||
it('should throw an error if minUnitsToExchange is not included', () => {
|
||||
try {
|
||||
const data = {
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
tokenId: 'fakeId',
|
||||
buyOrSell: 'buy',
|
||||
rateInSats: 1000
|
||||
rateInBaseUnit: 1000
|
||||
}
|
||||
uut.validate(data)
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(
|
||||
err.message,
|
||||
"Property 'minSatsToExchange' must be an integer number."
|
||||
"Property 'minUnitsToExchange' must be an integer number."
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -121,8 +121,8 @@ describe('#Order-Entity', () => {
|
||||
messageClass: 1,
|
||||
tokenId: 'fakeId',
|
||||
buyOrSell: 'buy',
|
||||
rateInSats: 1000,
|
||||
minSatsToExchange: 350
|
||||
rateInBaseUnit: 1000,
|
||||
minUnitsToExchange: 350
|
||||
}
|
||||
uut.validate(data)
|
||||
} catch (err) {
|
||||
|
||||
@@ -126,7 +126,8 @@ const wallet = {
|
||||
getKeyPair: async () => {
|
||||
return { cashAddress: 'fakeAddr', wif: 'fakeWif', hdIndex: 1 }
|
||||
},
|
||||
bchWallet: new MockBchWallet()
|
||||
bchWallet: new MockBchWallet(),
|
||||
moveTokens: async () => {}
|
||||
}
|
||||
|
||||
const p2wdb = {
|
||||
|
||||
@@ -9,7 +9,7 @@ const sinon = require('sinon')
|
||||
const EntryLib = require('../../../src/use-cases/entry')
|
||||
const adapters = require('../mocks/adapters')
|
||||
|
||||
describe('#users-use-case', () => {
|
||||
describe('#entry-use-case', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
@@ -32,6 +32,7 @@ describe('#users-use-case', () => {
|
||||
uut = new EntryLib()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
console.log(uut)
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
@@ -41,175 +42,175 @@ describe('#users-use-case', () => {
|
||||
})
|
||||
})
|
||||
|
||||
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.bch, '_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.bch, '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)
|
||||
})
|
||||
})
|
||||
// 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.bch, '_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.bch, '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)
|
||||
// })
|
||||
// })
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@ const sinon = require('sinon')
|
||||
|
||||
// Unit under test (uut)
|
||||
const OfferLib = require('../../../src/use-cases/offer')
|
||||
const OrderUseCase = require('../../../src/use-cases/order')
|
||||
const adapters = require('../mocks/adapters')
|
||||
|
||||
describe('#offer-use-case', () => {
|
||||
@@ -25,7 +26,8 @@ describe('#offer-use-case', () => {
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
uut = new OfferLib({ adapters })
|
||||
const order = new OrderUseCase({ adapters })
|
||||
uut = new OfferLib({ adapters, order })
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
@@ -87,12 +89,13 @@ describe('#offer-use-case', () => {
|
||||
tokenId:
|
||||
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
|
||||
buyOrSell: 'sell',
|
||||
rateInSats: 1000,
|
||||
minSatsToExchange: 10,
|
||||
rateInBaseUnit: 1000,
|
||||
minUnitsToExchange: 10,
|
||||
numTokens: 0.02,
|
||||
utxoTxid:
|
||||
'241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
|
||||
utxoVout: 0
|
||||
utxoVout: 0,
|
||||
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00'
|
||||
},
|
||||
timestamp: '2021-09-20T17:54:26.395Z',
|
||||
localTimeStamp: '9/20/2021, 10:54:26 AM',
|
||||
|
||||
@@ -65,34 +65,34 @@ describe('#order-use-case', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('#moveTokens', () => {
|
||||
it('should move tokens to the holding address', async () => {
|
||||
// Mock dependencies
|
||||
// sandbox
|
||||
// .stub(uut.adapters.wallet.bchWallet, 'sendTokens')
|
||||
// .resolves('fakeTxid')
|
||||
|
||||
const orderEntity = {
|
||||
lokadId: 'SWP',
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
|
||||
buyOrSell: 'sell',
|
||||
rateInSats: 1000,
|
||||
minSatsToExchange: 0,
|
||||
numTokens: 1
|
||||
}
|
||||
|
||||
const result = await uut.moveTokens(orderEntity)
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'txid')
|
||||
assert.property(result, 'vout')
|
||||
|
||||
assert.equal(result.txid, 'fakeTxid')
|
||||
assert.equal(result.vout, 1)
|
||||
})
|
||||
})
|
||||
// describe('#moveTokens', () => {
|
||||
// it('should move tokens to the holding address', async () => {
|
||||
// // Mock dependencies
|
||||
// // sandbox
|
||||
// // .stub(uut.adapters.wallet.bchWallet, 'sendTokens')
|
||||
// // .resolves('fakeTxid')
|
||||
//
|
||||
// const orderEntity = {
|
||||
// lokadId: 'SWP',
|
||||
// messageType: 1,
|
||||
// messageClass: 1,
|
||||
// tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
|
||||
// buyOrSell: 'sell',
|
||||
// rateInBaseUnit: 1000,
|
||||
// minUnitsToExchange: 0,
|
||||
// numTokens: 1
|
||||
// }
|
||||
//
|
||||
// const result = await uut.moveTokens(orderEntity)
|
||||
// // console.log('result: ', result)
|
||||
//
|
||||
// assert.property(result, 'txid')
|
||||
// assert.property(result, 'vout')
|
||||
//
|
||||
// assert.equal(result.txid, 'fakeTxid')
|
||||
// assert.equal(result.vout, 1)
|
||||
// })
|
||||
// })
|
||||
|
||||
describe('#createOrder', () => {
|
||||
it('should create an order and return the hash', async () => {
|
||||
@@ -102,8 +102,8 @@ describe('#order-use-case', () => {
|
||||
messageClass: 1,
|
||||
tokenId: 'token-id',
|
||||
buyOrSell: 'sell',
|
||||
rateInSats: 1000,
|
||||
minSatsToExchange: 1250,
|
||||
rateInBaseUnit: 1000,
|
||||
minUnitsToExchange: 1250,
|
||||
numTokens: 1
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ describe('#order-use-case', () => {
|
||||
// sandbox.stub(uut.adapters.wallet, 'burnPsf').resolves('fakeTxid')
|
||||
sandbox.stub(uut.orderEntity, 'validate').returns(entryObj)
|
||||
sandbox.stub(uut, 'ensureFunds').resolves()
|
||||
sandbox.stub(uut, 'moveTokens').resolves({ txid: 'fakeTxid', vout: 0, hdIndex: 1 })
|
||||
sandbox.stub(uut.adapters.wallet, 'moveTokens').resolves({ txid: 'fakeTxid', vout: 0, hdIndex: 1 })
|
||||
sandbox.stub(uut.adapters.wallet.bchWallet, 'getUtxos').resolves()
|
||||
// sandbox
|
||||
// .stub(uut.adapters.wallet, 'generateSignature')
|
||||
@@ -126,16 +126,11 @@ describe('#order-use-case', () => {
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(uut.orderEntity, 'validate')
|
||||
.throws(new Error('test error'))
|
||||
|
||||
await uut.createOrder()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
assert.include(err.message, 'Cannot set')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
This script has not been customized for offers yet.
|
||||
*/
|
||||
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
// Force test environment
|
||||
// make sure environment variable is set before this file gets called.
|
||||
// see test script in package.json.
|
||||
// process.env.KOA_ENV = 'test'
|
||||
const config = require('../../config')
|
||||
|
||||
const User = require('../../src/models/users')
|
||||
|
||||
async function deleteUsers () {
|
||||
// Connect to the Mongo Database.
|
||||
mongoose.Promise = global.Promise
|
||||
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
await mongoose.connect(config.database, {
|
||||
useUnifiedTopology: true,
|
||||
useNewUrlParser: true
|
||||
})
|
||||
|
||||
// Get all the users in the DB.
|
||||
const users = await User.find({}, '-password')
|
||||
// console.log(`users: ${JSON.stringify(users, null, 2)}`)
|
||||
|
||||
// Delete each user.
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
const thisUser = users[i]
|
||||
await thisUser.remove()
|
||||
}
|
||||
|
||||
mongoose.connection.close()
|
||||
}
|
||||
|
||||
deleteUsers()
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
Get all Offers in the database.
|
||||
*/
|
||||
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
const config = require('../../config')
|
||||
|
||||
const Offer = require('../../src/adapters/localdb/models/offer')
|
||||
|
||||
async function getOffers () {
|
||||
// Connect to the Mongo Database.
|
||||
mongoose.Promise = global.Promise
|
||||
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
await mongoose.connect(config.database, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true
|
||||
})
|
||||
|
||||
const offers = await Offer.find({})
|
||||
console.log(`offers: ${JSON.stringify(offers, null, 2)}`)
|
||||
|
||||
mongoose.connection.close()
|
||||
}
|
||||
getOffers()
|
||||
@@ -13,6 +13,9 @@ const BchTokenSweep = require('bch-token-sweep/index')
|
||||
// Local libraries
|
||||
const WalletAdapter = require('../../src/adapters/wallet')
|
||||
|
||||
// Constants
|
||||
const EMTPY_ADDR_CUTOFF = 5
|
||||
|
||||
async function sweepFunds () {
|
||||
try {
|
||||
// Open the wallet files.
|
||||
@@ -35,9 +38,11 @@ async function sweepFunds () {
|
||||
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 cashAddress = bchjs.HDNode.toCashAddress(childNode)
|
||||
const wifToSweep = bchjs.HDNode.toWIF(childNode)
|
||||
|
||||
console.log(`\nSweeping ${cashAddress}`)
|
||||
|
||||
try {
|
||||
// Sweep tokens from address
|
||||
const sweeper = new BchTokenSweep(
|
||||
@@ -62,14 +67,16 @@ async function sweepFunds () {
|
||||
// Wait between loop iterations.
|
||||
await bchjs.Util.sleep(3000)
|
||||
} catch (err) {
|
||||
console.log(`error message: ${err.message}`)
|
||||
console.log(`error message with index ${hdIndex}: ${err.message}`)
|
||||
emptyAddrCnt++
|
||||
}
|
||||
|
||||
hdIndex++
|
||||
} while (emptyAddrCnt < 5)
|
||||
} while (emptyAddrCnt < EMTPY_ADDR_CUTOFF)
|
||||
|
||||
console.log('5 empty addresses detected. Exiting.')
|
||||
console.log(`${EMTPY_ADDR_CUTOFF} empty addresses detected. Exiting.`)
|
||||
|
||||
console.log('\n\nDo not forget to reset the nextAddress property in the wallet.json file!\n\n')
|
||||
} catch (err) {
|
||||
console.error('Error in sweepFunds(): ', err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user