Compare commits

...
9 Commits
14 changed files with 596 additions and 113 deletions
+3 -3
View File
@@ -10,16 +10,16 @@ There are three major pieces of software behind the bch-dex concept. They work t
![bch-dex major subcomponents](./diagrams/software-interaction.png)
- _Client_ could be a web browser, or a command-line client like [psf-bch-wallet](https://github.com/Permissionless-Software-Foundation/psf-bch-wallet) or [psf-avax-wallet](https://github.com/Permissionless-Software-Foundation/psf-avax-wallet).
- _Client_ could be a web browser like [wallet.fullstack.cash](https://bchn-wallet.fullstack.cash), or a command-line client like [psf-bch-wallet](https://github.com/Permissionless-Software-Foundation/psf-bch-wallet) or [psf-avax-wallet](https://github.com/Permissionless-Software-Foundation/psf-avax-wallet).
- [bch-dex](https://github.com/Permissionless-Software-Foundation/bch-dex) is the back end REST API that maintains a local database of information that the client reads from.
- [P2WDB](https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service) is the pay-to-write global database with a REST API for interfacing with the other two pieces of software.
The arrows in the image represent the information flow between the three pieces of software:
- The _Client_ is essentially a 'dummy terminal' with a bidirectional interface to bch-dex. bch-dex does the heavy lifting, and the _Client_ is a 'thin' UI wrapper.
- `bch-dex` imports data from the global P2WDB database into its local database, using a [webhook](https://en.wikipedia.org/wiki/Webhook) (dashed line). It can also custody funds, pay transaction fees, and create an _Offer_ by submitting the data to the P2WDB to generate an _Order_ (solid line).
- `bch-dex` imports data from the global P2WDB database into its local database, using a [webhook](https://en.wikipedia.org/wiki/Webhook) (dashed line). It can also custody funds, pay transaction fees, and create an _Order_ by submitting the data to the P2WDB to generate an _Offer_ (solid line).
This architecture keeps the global database highly censorship resistant, while allowing local installations to maintain tight control over the user experience. The goal is to have many redundant copies of `bch-dex` on the network, and to empower individual traders to run their own, private copy.
This architecture keeps the global database highly censorship resistant, while allowing local installations to maintain tight control over the user experience. The goal is to have many redundant copies of `bch-dex` on the network, and to empower individual traders to run their own, private copy, while maintaining a single source of truth via the P2WDB.
# Back End
+191 -35
View File
@@ -9,13 +9,13 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@psf/bch-js": "5.3.2",
"@psf/bch-js": "5.4.0",
"axios": "0.21.1",
"bch-message-lib": "2.1.4",
"bcryptjs": "2.4.3",
"glob": "7.1.6",
"ipfs": "0.60.0",
"ipfs-coord": "7.1.4",
"ipfs-coord": "7.1.6",
"ipfs-http-client": "55.0.0",
"jsonrpc-lite": "2.2.0",
"jsonwebtoken": "8.5.1",
@@ -33,7 +33,7 @@
"koa2-ratelimit": "0.9.1",
"libp2p": "^0.36.2",
"line-reader": "0.4.0",
"minimal-slp-wallet": "4.4.2",
"minimal-slp-wallet": "4.4.3",
"mongoose": "5.13.13",
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
"nodemailer": "6.4.17",
@@ -45,6 +45,7 @@
},
"devDependencies": {
"apidoc": "0.26.0",
"bch-token-sweep": "1.5.16",
"chai": "4.3.0",
"coveralls": "2.11.4",
"eslint": "7.19.0",
@@ -74,8 +75,6 @@
"@psf/bitcoincash-ops": "2.0.0",
"@psf/bitcoincashjs-lib": "4.0.2",
"@psf/coininfo": "4.0.0",
"@uppy/core": "1.10.4",
"@uppy/tus": "1.5.12",
"axios": "^0.21.4",
"bc-bip68": "1.0.5",
"bchaddrjs-slp": "0.2.5",
@@ -97,7 +96,7 @@
"wif": "2.0.6"
},
"devDependencies": {
"apidoc": "0.17.7",
"apidoc": "^0.50.5",
"assert": "2.0.0",
"chai": "4.1.2",
"coveralls": "3.0.2",
@@ -109,10 +108,10 @@
"eslint-plugin-standard": "4.0.0",
"husky": "^4.3.8",
"lodash.clonedeep": "4.5.0",
"mocha": "9.1.3",
"mocha": "^9.2.1",
"node-mocks-http": "1.7.0",
"nyc": "15.1.0",
"semantic-release": "18.0.0",
"semantic-release": "^19.0.2",
"sinon": "9.2.2",
"standard": "^16.0.4"
}
@@ -122,7 +121,7 @@
"extraneous": true,
"license": "MIT",
"dependencies": {
"@psf/bch-js": "5.3.2",
"@psf/bch-js": "../bch-js",
"apidoc": "0.25.0",
"bch-consumer": "1.2.0",
"bch-donation": "1.1.1",
@@ -140,9 +139,9 @@
"eslint-plugin-standard": "4.0.1",
"husky": "4.3.8",
"lodash.clonedeep": "4.5.0",
"mocha": "8.2.0",
"mocha": "^9.2.1",
"nyc": "15.1.0",
"semantic-release": "17.4.4",
"semantic-release": "^19.0.2",
"sinon": "9.2.0",
"standard": "16.0.4",
"tinyify": "3.0.0"
@@ -1604,9 +1603,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@psf/bch-js": {
"version": "5.3.2",
"resolved": "http://94.130.170.209:4873/@psf%2fbch-js/-/bch-js-5.3.2.tgz",
"integrity": "sha512-LzYekwuee+T8AEZ1MKBe5KnZKXzpRZ57SpXRL2S8qsHefokTIN+XnDPX7dsOFkN4VGGgyjYyZ33iN6xhOSHWJw==",
"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==",
"license": "MIT",
"dependencies": {
"@chris.troutner/bip32-utils": "1.0.5",
@@ -1614,8 +1613,6 @@
"@psf/bitcoincash-ops": "2.0.0",
"@psf/bitcoincashjs-lib": "4.0.2",
"@psf/coininfo": "4.0.0",
"@uppy/core": "1.10.4",
"@uppy/tus": "1.5.12",
"axios": "^0.21.4",
"bc-bip68": "1.0.5",
"bchaddrjs-slp": "0.2.5",
@@ -2972,6 +2969,17 @@
"minimal-slp-wallet": "4.4.1"
}
},
"node_modules/bch-token-sweep": {
"version": "1.5.16",
"resolved": "http://94.130.170.209:4873/bch-token-sweep/-/bch-token-sweep-1.5.16.tgz",
"integrity": "sha512-9Fsb0qXUZ62OUDdH/tIF8QZDs12vWduoGDtU9txpSEF6GEyK0NOHfu8YYx5henoHz7SGS/fMUWDCibmedSpsmA==",
"dev": true,
"license": "MIT",
"dependencies": {
"bch-donation": "1.1.1",
"bignumber.js": "9.0.0"
}
},
"node_modules/bchaddrjs-slp": {
"version": "0.2.5",
"resolved": "http://94.130.170.209:4873/bchaddrjs-slp/-/bchaddrjs-slp-0.2.5.tgz",
@@ -8411,9 +8419,10 @@
}
},
"node_modules/ipfs-coord": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/ipfs-coord/-/ipfs-coord-7.1.4.tgz",
"integrity": "sha512-YsCdrQ30JW03uOLNrpZKqNmWr5zmxF9AyKeXSQA/OTPBoOOqM2nomEYhTPVJjGxee9Qa2dtxCQzw3JwsLDCe4Q==",
"version": "7.1.6",
"resolved": "http://94.130.170.209:4873/ipfs-coord/-/ipfs-coord-7.1.6.tgz",
"integrity": "sha512-wtdJ/PgRMv3r2pOZ1WIc7blTkk3YftMbXTNX9hN9Lu+IblUhNoWUyjq7E4i7J3zBZQVQs7J3TyckRdLCekmc9A==",
"license": "MIT",
"dependencies": {
"axios": "0.21.4",
"bch-encrypt-lib": "2.0.0",
@@ -13012,12 +13021,12 @@
}
},
"node_modules/minimal-slp-wallet": {
"version": "4.4.2",
"resolved": "http://94.130.170.209:4873/minimal-slp-wallet/-/minimal-slp-wallet-4.4.2.tgz",
"integrity": "sha512-6JNRnH8mQQb/gmfwdd0KgJeHFJycK60EDmeEdjghvm1I2J0z2HVI3gZzrVqjBC/tBDYKtuDuvRtix7q2LqKqhw==",
"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==",
"license": "MIT",
"dependencies": {
"@psf/bch-js": "5.3.2",
"@psf/bch-js": "5.4.0",
"apidoc": "0.25.0",
"bch-consumer": "1.2.0",
"bch-donation": "1.1.1",
@@ -17923,6 +17932,49 @@
"minimal-slp-wallet": "4.4.2"
}
},
"node_modules/p2wdb/node_modules/@psf/bch-js": {
"version": "5.3.2",
"resolved": "http://94.130.170.209:4873/@psf%2fbch-js/-/bch-js-5.3.2.tgz",
"integrity": "sha512-LzYekwuee+T8AEZ1MKBe5KnZKXzpRZ57SpXRL2S8qsHefokTIN+XnDPX7dsOFkN4VGGgyjYyZ33iN6xhOSHWJw==",
"license": "MIT",
"dependencies": {
"@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",
"@uppy/core": "1.10.4",
"@uppy/tus": "1.5.12",
"axios": "^0.21.4",
"bc-bip68": "1.0.5",
"bchaddrjs-slp": "0.2.5",
"bigi": "1.4.2",
"bignumber.js": "9.0.0",
"bip-schnorr": "0.3.0",
"bip38": "2.0.2",
"bip39": "3.0.2",
"bip66": "1.1.5",
"bitcoinjs-message": "2.0.0",
"bs58": "4.0.1",
"ecashaddrjs": "1.0.7",
"ini": "1.3.8",
"randombytes": "2.0.6",
"safe-buffer": "5.1.2",
"satoshi-bitcoin": "1.0.4",
"slp-mdm": "0.0.6",
"slp-parser": "0.0.4",
"wif": "2.0.6"
}
},
"node_modules/p2wdb/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==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.14.0"
}
},
"node_modules/p2wdb/node_modules/apidoc": {
"version": "0.25.0",
"resolved": "http://94.130.170.209:4873/apidoc/-/apidoc-0.25.0.tgz",
@@ -17954,6 +18006,34 @@
"follow-redirects": "^1.14.4"
}
},
"node_modules/p2wdb/node_modules/minimal-slp-wallet": {
"version": "4.4.2",
"resolved": "http://94.130.170.209:4873/minimal-slp-wallet/-/minimal-slp-wallet-4.4.2.tgz",
"integrity": "sha512-6JNRnH8mQQb/gmfwdd0KgJeHFJycK60EDmeEdjghvm1I2J0z2HVI3gZzrVqjBC/tBDYKtuDuvRtix7q2LqKqhw==",
"license": "MIT",
"dependencies": {
"@psf/bch-js": "5.3.2",
"apidoc": "0.25.0",
"bch-consumer": "1.2.0",
"bch-donation": "1.1.1",
"crypto-js": "4.0.0"
}
},
"node_modules/p2wdb/node_modules/randombytes": {
"version": "2.0.6",
"resolved": "http://94.130.170.209:4873/randombytes/-/randombytes-2.0.6.tgz",
"integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==",
"license": "MIT",
"dependencies": {
"safe-buffer": "^5.1.0"
}
},
"node_modules/p2wdb/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "http://94.130.170.209:4873/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/package-hash": {
"version": "4.0.0",
"dev": true,
@@ -23321,17 +23401,15 @@
"version": "1.1.0"
},
"@psf/bch-js": {
"version": "5.3.2",
"resolved": "http://94.130.170.209:4873/@psf%2fbch-js/-/bch-js-5.3.2.tgz",
"integrity": "sha512-LzYekwuee+T8AEZ1MKBe5KnZKXzpRZ57SpXRL2S8qsHefokTIN+XnDPX7dsOFkN4VGGgyjYyZ33iN6xhOSHWJw==",
"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==",
"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",
"@uppy/core": "1.10.4",
"@uppy/tus": "1.5.12",
"axios": "^0.21.4",
"bc-bip68": "1.0.5",
"bchaddrjs-slp": "0.2.5",
@@ -24320,6 +24398,16 @@
"version": "2.1.4",
"requires": {}
},
"bch-token-sweep": {
"version": "1.5.16",
"resolved": "http://94.130.170.209:4873/bch-token-sweep/-/bch-token-sweep-1.5.16.tgz",
"integrity": "sha512-9Fsb0qXUZ62OUDdH/tIF8QZDs12vWduoGDtU9txpSEF6GEyK0NOHfu8YYx5henoHz7SGS/fMUWDCibmedSpsmA==",
"dev": true,
"requires": {
"bch-donation": "1.1.1",
"bignumber.js": "9.0.0"
}
},
"bchaddrjs-slp": {
"version": "0.2.5",
"resolved": "http://94.130.170.209:4873/bchaddrjs-slp/-/bchaddrjs-slp-0.2.5.tgz",
@@ -28077,9 +28165,9 @@
}
},
"ipfs-coord": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/ipfs-coord/-/ipfs-coord-7.1.4.tgz",
"integrity": "sha512-YsCdrQ30JW03uOLNrpZKqNmWr5zmxF9AyKeXSQA/OTPBoOOqM2nomEYhTPVJjGxee9Qa2dtxCQzw3JwsLDCe4Q==",
"version": "7.1.6",
"resolved": "http://94.130.170.209:4873/ipfs-coord/-/ipfs-coord-7.1.6.tgz",
"integrity": "sha512-wtdJ/PgRMv3r2pOZ1WIc7blTkk3YftMbXTNX9hN9Lu+IblUhNoWUyjq7E4i7J3zBZQVQs7J3TyckRdLCekmc9A==",
"requires": {
"axios": "0.21.4",
"bch-encrypt-lib": "2.0.0",
@@ -31595,11 +31683,11 @@
"dev": true
},
"minimal-slp-wallet": {
"version": "4.4.2",
"resolved": "http://94.130.170.209:4873/minimal-slp-wallet/-/minimal-slp-wallet-4.4.2.tgz",
"integrity": "sha512-6JNRnH8mQQb/gmfwdd0KgJeHFJycK60EDmeEdjghvm1I2J0z2HVI3gZzrVqjBC/tBDYKtuDuvRtix7q2LqKqhw==",
"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==",
"requires": {
"@psf/bch-js": "5.3.2",
"@psf/bch-js": "5.4.0",
"apidoc": "0.25.0",
"bch-consumer": "1.2.0",
"bch-donation": "1.1.1",
@@ -34958,6 +35046,49 @@
"minimal-slp-wallet": "4.4.2"
},
"dependencies": {
"@psf/bch-js": {
"version": "5.3.2",
"resolved": "http://94.130.170.209:4873/@psf%2fbch-js/-/bch-js-5.3.2.tgz",
"integrity": "sha512-LzYekwuee+T8AEZ1MKBe5KnZKXzpRZ57SpXRL2S8qsHefokTIN+XnDPX7dsOFkN4VGGgyjYyZ33iN6xhOSHWJw==",
"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",
"@uppy/core": "1.10.4",
"@uppy/tus": "1.5.12",
"axios": "^0.21.4",
"bc-bip68": "1.0.5",
"bchaddrjs-slp": "0.2.5",
"bigi": "1.4.2",
"bignumber.js": "9.0.0",
"bip-schnorr": "0.3.0",
"bip38": "2.0.2",
"bip39": "3.0.2",
"bip66": "1.1.5",
"bitcoinjs-message": "2.0.0",
"bs58": "4.0.1",
"ecashaddrjs": "1.0.7",
"ini": "1.3.8",
"randombytes": "2.0.6",
"safe-buffer": "5.1.2",
"satoshi-bitcoin": "1.0.4",
"slp-mdm": "0.0.6",
"slp-parser": "0.0.4",
"wif": "2.0.6"
},
"dependencies": {
"axios": {
"version": "0.21.4",
"resolved": "http://94.130.170.209:4873/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"requires": {
"follow-redirects": "^1.14.0"
}
}
}
},
"apidoc": {
"version": "0.25.0",
"resolved": "http://94.130.170.209:4873/apidoc/-/apidoc-0.25.0.tgz",
@@ -34980,6 +35111,31 @@
"requires": {
"follow-redirects": "^1.14.4"
}
},
"minimal-slp-wallet": {
"version": "4.4.2",
"resolved": "http://94.130.170.209:4873/minimal-slp-wallet/-/minimal-slp-wallet-4.4.2.tgz",
"integrity": "sha512-6JNRnH8mQQb/gmfwdd0KgJeHFJycK60EDmeEdjghvm1I2J0z2HVI3gZzrVqjBC/tBDYKtuDuvRtix7q2LqKqhw==",
"requires": {
"@psf/bch-js": "5.3.2",
"apidoc": "0.25.0",
"bch-consumer": "1.2.0",
"bch-donation": "1.1.1",
"crypto-js": "4.0.0"
}
},
"randombytes": {
"version": "2.0.6",
"resolved": "http://94.130.170.209:4873/randombytes/-/randombytes-2.0.6.tgz",
"integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==",
"requires": {
"safe-buffer": "^5.1.0"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "http://94.130.170.209:4873/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
}
}
},
+4 -3
View File
@@ -27,13 +27,13 @@
},
"repository": "Permissionless-Software-Foundation/bch-dex",
"dependencies": {
"@psf/bch-js": "5.3.2",
"@psf/bch-js": "5.4.0",
"axios": "0.21.1",
"bch-message-lib": "2.1.4",
"bcryptjs": "2.4.3",
"glob": "7.1.6",
"ipfs": "0.60.0",
"ipfs-coord": "7.1.4",
"ipfs-coord": "7.1.6",
"ipfs-http-client": "55.0.0",
"jsonrpc-lite": "2.2.0",
"jsonwebtoken": "8.5.1",
@@ -51,7 +51,7 @@
"koa2-ratelimit": "0.9.1",
"libp2p": "^0.36.2",
"line-reader": "0.4.0",
"minimal-slp-wallet": "4.4.2",
"minimal-slp-wallet": "4.4.3",
"mongoose": "5.13.13",
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
"nodemailer": "6.4.17",
@@ -63,6 +63,7 @@
},
"devDependencies": {
"apidoc": "0.26.0",
"bch-token-sweep": "1.5.16",
"chai": "4.3.0",
"coveralls": "2.11.4",
"eslint": "7.19.0",
+2 -1
View File
@@ -17,7 +17,8 @@ const Order = new mongoose.Schema({
timestamp: { type: String },
localTimestamp: { type: String },
p2wdbTxid: { type: String },
p2wdbHash: { type: String }
p2wdbHash: { type: String },
orderStatus: { type: String }
})
module.exports = mongoose.model('order', Order)
+125 -57
View File
@@ -178,63 +178,131 @@ class WalletAdapter {
}
}
// Burn enough PSF to generate a valide proof-of-burn for writing to the P2WDB.
// async burnPsf () {
// try {
// // TODO: Throw error if this.bchWallet has not been instantiated.
//
// // console.log('walletData: ', walletData)
// // console.log(
// // `walletData.utxos.utxoStore.slpUtxos: ${JSON.stringify(
// // walletData.utxos.utxoStore.slpUtxos,
// // null,
// // 2,
// // )}`,
// // )
//
// // Get token UTXOs held by the wallet.
// const tokenUtxos = this.bchWallet.utxos.utxoStore.slpUtxos.type1.tokens
// // console.log(`tokenUtxos: ${JSON.stringify(tokenUtxos, null, 2)}`)
//
// // Find a token UTXO that contains PSF with a quantity higher than needed
// // to generate a proof-of-burn.
// let tokenUtxo = {}
// for (let i = 0; i < tokenUtxos.length; i++) {
// const thisUtxo = tokenUtxos[i]
//
// // If token ID matches.
// if (thisUtxo.tokenId === P2WDB_TOKEN_ID) {
// if (parseFloat(thisUtxo.qtyStr) >= PROOF_OF_BURN_QTY) {
// tokenUtxo = thisUtxo
// break
// }
// }
// }
//
// if (tokenUtxo.tokenId !== P2WDB_TOKEN_ID) {
// throw new Error(
// `Token UTXO of with ID of ${P2WDB_TOKEN_ID} and quantity greater than ${PROOF_OF_BURN_QTY} could not be found in wallet.`
// )
// }
// // console.log(`tokenUtxo: ${JSON.stringify(tokenUtxo, null, 2)}`)
//
// const result = await this.bchWallet.burnTokens(
// PROOF_OF_BURN_QTY,
// P2WDB_TOKEN_ID
// )
// // console.log('walletData.burnTokens() result: ', result)
//
// return result
//
// // return {
// // success: true,
// // txid: 'fakeTxid',
// // }
// } catch (err) {
// console.error('Error in burnPsf(): ', err)
// throw err
// }
// }
// Generate a partial transcation to *take* a 'sell' order.
async generatePartialTx (orderInfo) {
try {
console.log(`orderInfo: ${JSON.stringify(orderInfo, null, 2)}`)
const bchjs = this.bchWallet.bchjs
// instance of transaction builder
const transactionBuilder = new bchjs.TransactionBuilder()
// 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)
// Get token info on the offered UTXO
const txData = await this.bchWallet.getTxData([orderInfo.utxoTxid])
console.log(`txData: ${JSON.stringify(txData, null, 2)}`)
// Construct the UTXO being offered for sale.
const offeredUtxo = {
txid: orderInfo.utxoTxid,
vout: orderInfo.utxoVout,
tokenId: orderInfo.tokenId,
decimals: txData[0].tokenDecimals,
tokenQty: orderInfo.numTokens.toString()
}
console.log(`offeredUtxo: ${JSON.stringify(offeredUtxo, null, 2)}`)
// Build First part of the collaborative Tx a.k.a. Alice
// Generate the OP_RETURN code.
const slpSendObj = bchjs.SLP.TokenType1.generateSendOpReturn(
[offeredUtxo],
orderInfo.numTokens.toString()
)
const slpData = slpSendObj.script
console.log(`slpOutputs: ${slpSendObj.outputs}`)
// Currently this app only supports a single SLP token UTXO for exact
// token quantities (no token change). e.g. 1 UTXO representing the
// exact number of 'numTokens'
if (slpSendObj.outputs > 1) {
console.log('WARNING: choose one UTXO with all tokens to exchange')
return
}
// Calculate sats needed to pay the offer.
const satsNeeded = orderInfo.numTokens * parseInt(orderInfo.rateInSats)
// Calculate miner fees.
// Get byte count (minimum 2 inputs, 3 outputs)
const opReturnBufLength = slpData.byteLength + 32 // add padding
const byteCount =
bchjs.BitcoinCash.getByteCount({ P2PKH: 2 }, { P2PKH: 4 }) +
opReturnBufLength
const totalSatsNeeded = byteCount + satsNeeded
// console.log(`satoshis needed: ${satsNeeded}`)
// One last check to ensure the app wallet has enough BCH to complete
// the trade.
if (totalSatsNeeded > paymentUtxo.value) {
console.log(`Selected payment UTXO is not big enough. Sats needed: ${totalSatsNeeded}, UTXO value: ${paymentUtxo.value}`)
}
// add UTXO for sell(STILL CANNOT SPEND - not signed yet)
transactionBuilder.addInput(orderInfo.utxoTxid, orderInfo.utxoVout)
// add payment UTXO
transactionBuilder.addInput(paymentUtxo.tx_hash, paymentUtxo.tx_pos)
const originalAmount = paymentUtxo.value
const dust = 546
const remainder = originalAmount - satsNeeded - dust // exchange fee + token UTXO dust
// Add the SLP OP_RETURN data as the first output.
transactionBuilder.addOutput(slpData, 0)
const buyerAddr = this.bchWallet.walletInfo.legacyAddress
// console.log(`buyAddr: ${JSON.stringify(buyAddr, null, 2)}`)
// Send dust transaction representing tokens being sent.
transactionBuilder.addOutput(
buyerAddr,
dust
)
// Get seller address
// TODO: Seller should explicitly define the address to send to in the
// orderInfo object. Right now it retrieves the address from the UTXO
// for sale, which is not ideal.
const sellerAddr = txData[0].vout[1].scriptPubKey.addresses[0]
// Send payment to the offer side
transactionBuilder.addOutput(sellerAddr, satsNeeded)
// Send the BCH change back to the buyer
if (remainder > 550) {
transactionBuilder.addOutput(buyerAddr, remainder)
}
const buyerECPair = bchjs.ECPair.fromWIF(this.bchWallet.walletInfo.privateKey)
// Sign the buyers input UTXO for spending.
transactionBuilder.sign(
1,
buyerECPair,
null,
transactionBuilder.hashTypes.SIGHASH_ALL,
originalAmount
)
const tx = transactionBuilder.transaction.buildIncomplete()
const hex = tx.toHex()
console.log('hex: ', hex)
return hex
} catch (err) {
console.error('Error in wallet.js/generatePartialTx(): ', err)
throw err
}
// return true
}
}
module.exports = WalletAdapter
@@ -61,6 +61,27 @@ class OrderRESTControllerLib {
}
}
// Currently only supports 'sell' orders, and will only buy the 'numTokens'
// listed in the order.
async takeOrder (ctx) {
try {
console.log('body: ', ctx.request.body)
const orderCid = ctx.request.body.orderCid
// Find the Order.
// const orderEntity = await _this.useCases.order.findOrder(orderId)
// 'Take' the Order.
const hash = await _this.useCases.order.takeOrder(orderCid)
ctx.body = { hash }
} catch (err) {
console.log('Error in takeOrder REST API handler.')
_this.handleError(ctx, err)
}
}
// DRY error handler
handleError (ctx, err) {
console.log('err', err.message)
+1
View File
@@ -51,6 +51,7 @@ class OrderRouter {
// Define the routes and attach the controller.
this.router.post('/', _this.orderRESTController.createOrder)
this.router.get('/list', _this.orderRESTController.listOrders)
this.router.post('/take', _this.orderRESTController.takeOrder)
// Attach the Controller routes to the Koa app.
app.use(_this.router.routes())
+10 -12
View File
@@ -4,6 +4,10 @@
It's destroyed when the UTXO described in the Signal has been detected as spent.
*/
class OrderEntity {
constructor () {
this.orderStatus = ['posted', 'taken', 'completed']
}
validate (orderData = {}) {
// Throw an error if input object does not have a data property
if (!orderData.data) {
@@ -12,17 +16,7 @@ class OrderEntity {
)
}
const {
messageType,
messageClass,
tokenId,
buyOrSell,
rateInSats,
minSatsToExchange,
numTokens,
utxoTxid,
utxoVout
} = orderData.data
const { messageType, messageClass, tokenId, buyOrSell, rateInSats, minSatsToExchange, numTokens, utxoTxid, utxoVout, orderStatus } = orderData.data
// Input Validation
if (!messageType || typeof messageType !== 'number') {
@@ -52,6 +46,9 @@ class OrderEntity {
if (typeof utxoVout !== 'number') {
throw new Error("Property 'utxoVout' must be an integer number.")
}
if (orderStatus && !this.orderStatus.includes(orderStatus)) {
throw new Error("Property 'orderStatus' must be a valid string")
}
const validatedOrderData = {
messageType,
@@ -66,7 +63,8 @@ class OrderEntity {
timestamp: orderData.timestamp,
localTimestamp: orderData.localTimeStamp,
txid: orderData.txid,
p2wdbHash: orderData.hash
p2wdbHash: orderData.hash,
orderStatus: orderStatus || this.orderStatus[0]
}
return validatedOrderData
+1 -1
View File
@@ -90,7 +90,7 @@ class OfferLib {
const utxoInfo = {
txid,
vout: 0,
vout: 1,
hdIndex: keyPair.hdIndex
}
+135
View File
@@ -8,7 +8,9 @@
local Offers are no different than Orders generated by other peers.
*/
// Local libraries
const OrderEntity = require('../../entities/order')
const config = require('../../../config')
class OrderUseCases {
constructor (localConfig = {}) {
@@ -20,6 +22,9 @@ class OrderUseCases {
)
}
// Encapsulate dependencies
this.config = config
this.orderEntity = new OrderEntity()
this.OrderModel = this.adapters.localdb.Order
}
@@ -43,6 +48,9 @@ class OrderUseCases {
console.log('utxoStatus: ', utxoStatus)
if (utxoStatus === null) return false
// A new order gets a status of 'posted'
orderObj.data.orderStatus = 'posted'
const orderEntity = this.orderEntity.validate(orderObj)
console.log('orderEntity: ', orderEntity)
@@ -65,6 +73,133 @@ class OrderUseCases {
throw error
}
}
// Generate phase 2 of 3 - take the other side of an Order.
// Based on this example:
// https://github.com/Permissionless-Software-Foundation/bch-js-examples/blob/master/bch/applications/collaborate/sell-slp/e2e-exchange/step2-purchase-tx.js
async takeOrder (orderCid) {
try {
console.log('orderCid: ', orderCid)
// Get the Order information
const orderInfo = await this.findOrderByHash(orderCid)
console.log(`orderInfo: ${JSON.stringify(orderInfo, null, 2)}`)
// Ensure the order is in a 'posted' state and not already 'taken'
if (orderInfo.orderStatus && orderInfo.orderStatus !== 'posted') {
throw new Error('order already taken')
}
// Verify that UTXO for sale is unspent. Abort if it's been spent.
const txid = orderInfo.utxoTxid
const vout = orderInfo.utxoVout
const utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut(
txid,
vout
)
console.log('utxoStatus: ', utxoStatus)
if (utxoStatus === null) {
console.log(`utxo txid: ${txid}, vout: ${vout}`)
throw new Error('UTXO does not exist. Aborting.')
}
// Ensure the app has enough funds to complete the trade.
await this.ensureFunds(orderInfo)
// Get UTXOs.
const utxos = this.adapters.wallet.bchWallet.utxos.utxoStore
console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
// TODO: Move funds to create a segrated UTXO for taking the order
// Create a partially signed transaction.
// https://github.com/Permissionless-Software-Foundation/bch-js-examples/blob/master/bch/applications/collaborate/sell-slp/e2e-exchange/step2-purchase-tx.js#L59
const partialTxHex = await this.adapters.wallet.generatePartialTx(orderInfo)
// return partialTxHex
// Create valid Order object
const takenOrderInfo = Object.assign({}, orderInfo)
takenOrderInfo.patialTxHex = partialTxHex
delete takenOrderInfo.p2wdbHash
delete takenOrderInfo._id
takenOrderInfo.offerHash = orderInfo.p2wdbHash
// Write order info to the P2WDB
// TODO: This will trigger the webhook. Find some way of triggering the
// webhook on new orders, but not on counteroffers
const p2wdbObj = {
wif: this.adapters.wallet.bchWallet.walletInfo.privateKey,
data: takenOrderInfo,
appId: this.config.p2wdbAppId
}
const hash = await this.adapters.p2wdb.write(p2wdbObj)
// Return the P2WDB CID
return hash
} catch (err) {
console.error('Error in use-cases/order/takeOrder(): ', err)
throw err
}
}
// Ensure that the wallet has enough BCH and tokens to complete the requested
// trade. Will return true if it does. Will throw an error if it doesn't.
async ensureFunds (orderEntity) {
try {
// console.log('this.adapters.wallet: ', this.adapters.wallet.bchWallet)
// console.log(`walletInfo: ${JSON.stringify(this.adapters.wallet.bchWallet.walletInfo, null, 2)}`)
await this.adapters.wallet.bchWallet.walletInfoPromise
// Ensure the app wallet has enough funds to write to the P2WDB.
const wif = this.adapters.wallet.bchWallet.walletInfo.privateKey
const canWriteToP2WDB = await this.adapters.p2wdb.checkForSufficientFunds(wif)
if (!canWriteToP2WDB) throw new Error('App wallet does not have funds for writing to the P2WDB.')
if (orderEntity.buyOrSell.includes('sell')) {
// Sell Offer
// Ensure the app wallet controlls enough BCH to pay for the tokens.
const satsNeeded = orderEntity.numTokens * parseInt(orderEntity.rateInSats)
const balance = await this.adapters.wallet.bchWallet.getBalance()
console.log(`wallet balance: ${balance}, sats needed: ${satsNeeded}`)
const SATS_MARGIN = 5000
if (satsNeeded + SATS_MARGIN > balance) { throw new Error('App wallet does not control enough BCH to purchase the tokens.') }
//
} else {
// Buy Offer
throw new Error('Buy orders are not supported yet.')
}
return true
} catch (err) {
console.error('Error in ensureFunds()')
throw err
}
}
async findOrderByHash (p2wdbHash) {
try {
if (typeof p2wdbHash !== 'string' || !p2wdbHash) {
throw new Error('p2wdbHash must be a string')
}
const order = await this.OrderModel.findOne({ p2wdbHash })
if (!order) {
throw new Error('order not found')
}
const orderObject = order.toObject()
// return this.orderEntity.validateFromModel(orderObject)
return orderObject
} catch (err) {
console.error('Error in findOrder(): ', err)
throw err
}
}
}
module.exports = OrderUseCases
+25
View File
@@ -0,0 +1,25 @@
/*
Part 2 of 3: Take an order
*/
const axios = require('axios')
const LOCALHOST = 'http://localhost:5700'
async function start () {
try {
const options = {
method: 'post',
url: `${LOCALHOST}/order/take`,
data: {
orderCid: 'zdpuAkp98gTuivaNzGP31jTQi3ADXrFA6uANrceQcrQkTXy2j'
}
}
const result = await axios(options)
console.log('result.data: ', result.data)
} catch (err) {
console.log(err)
}
}
start()
+1 -1
View File
@@ -90,7 +90,7 @@ describe('#offer-use-case', () => {
assert.property(result, 'vout')
assert.equal(result.txid, 'fakeTxid')
assert.equal(result.vout, 0)
assert.equal(result.vout, 1)
})
})
+77
View File
@@ -0,0 +1,77 @@
/*
This script will travers the HD wallet and sweep funds and tokens back
into the root address (index 0). That root address needs to have funds
to pay for the transactions.
The root address will make a final transaction to consolidate all tokens.
*/
// Public npm libraries
const BCHJS = require('@psf/bch-js')
const BchTokenSweep = require('bch-token-sweep/index')
// Local libraries
const WalletAdapter = require('../../src/adapters/wallet')
async function sweepFunds () {
try {
// Open the wallet files.
const wallet = new WalletAdapter()
const walletInfo = await wallet.openWallet()
console.log('walletInfo: ', walletInfo)
const rootAddr = walletInfo.cashAddress
const rootWif = walletInfo.privateKey
console.log(`Sweeping all funds into root address ${rootAddr}...`)
// Generate an HD tree
const bchjs = new BCHJS()
const rootSeed = await bchjs.Mnemonic.toSeed(walletInfo.mnemonic)
const masterHDNode = bchjs.HDNode.fromSeed(rootSeed)
let emptyAddrCnt = 0
let hdIndex = 1
do {
// Generate a keypair from the HD wallet.
const childNode = masterHDNode.derivePath(`m/44'/245'/0'/0/${hdIndex}`)
// const cashAddress = bchjs.HDNode.toCashAddress(childNode)
const wifToSweep = bchjs.HDNode.toWIF(childNode)
try {
// Sweep tokens from address
const sweeper = new BchTokenSweep(
wifToSweep,
rootWif,
bchjs,
550,
rootAddr
)
await sweeper.populateObjectFromNetwork()
const hex = await sweeper.sweepTo(rootAddr)
// console.log(`hex: ${hex}`)
const txid = await sweeper.blockchain.broadcast(hex)
// console.log('Transaction ID', txid)
console.log(`Swept HD index ${hdIndex}. TXID: ${txid}`)
emptyAddrCnt = 0
// Wait between loop iterations.
await bchjs.Util.sleep(3000)
} catch (err) {
console.log(`error message: ${err.message}`)
emptyAddrCnt++
}
hdIndex++
} while (emptyAddrCnt < 5)
console.log('5 empty addresses detected. Exiting.')
} catch (err) {
console.error('Error in sweepFunds(): ', err)
}
}
sweepFunds()