diff --git a/config/env/common.js b/config/env/common.js index df4cc60..6d49d0e 100644 --- a/config/env/common.js +++ b/config/env/common.js @@ -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', diff --git a/package-lock.json b/package-lock.json index 8223083..267db7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,10 @@ "license": "MIT", "dependencies": { "@psf/bch-js": "6.2.1", - "@psf/bitcoincashjs-lib": "4.0.2", "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", @@ -3114,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", @@ -18842,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, @@ -24511,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", @@ -35682,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 diff --git a/package.json b/package.json index d33ba31..6156c6c 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,10 @@ "repository": "Permissionless-Software-Foundation/bch-dex", "dependencies": { "@psf/bch-js": "6.2.1", - "@psf/bitcoincashjs-lib": "4.0.2", "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", diff --git a/src/adapters/wallet.js b/src/adapters/wallet.js index bb6e03c..425a063 100644 --- a/src/adapters/wallet.js +++ b/src/adapters/wallet.js @@ -4,7 +4,7 @@ // Public npm libraries const BchWallet = require('minimal-slp-wallet/index') -const bitcoinJs = require('@psf/bitcoincashjs-lib') +const bitcoinJs = require('bitcoincashjs-lib') // Local libraries const JsonFiles = require('./json-files') @@ -86,6 +86,7 @@ class WalletAdapter { // Wait for wallet to initialize. await this.bchWallet.walletInfoPromise + console.log('BCH wallet initialized.') return this.bchWallet } catch (err) { @@ -124,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 @@ -198,7 +202,7 @@ class WalletAdapter { // Get token info on the offered UTXO const txData = await this.bchWallet.getTxData([offerInfo.utxoTxid]) - console.log(`txData: ${JSON.stringify(txData, null, 2)}`) + // console.log(`txData: ${JSON.stringify(txData, null, 2)}`) // Construct the UTXO being offered for sale. const offeredUtxo = { @@ -270,10 +274,7 @@ class WalletAdapter { ) // Get seller address - // TODO: Seller should explicitly define the address to send to in the - // offerInfo 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) @@ -292,7 +293,7 @@ class WalletAdapter { buyerECPair, null, transactionBuilder.hashTypes.SIGHASH_ALL, - satsNeeded + utxoInfo.sats ) const tx = transactionBuilder.transaction.buildIncomplete() @@ -324,6 +325,9 @@ class WalletAdapter { qty } + // Update the UTXO store of the wallet. + await this.bchWallet.getUtxos() + const txid = await this.bchWallet.sendTokens(receiver, 3) const utxoInfo = { @@ -375,23 +379,6 @@ class WalletAdapter { // representing the transaction. async deseralizeTx (txHex) { try { - // console.log('txHex: ', txHex) - // - // // Convert the hex string version of the transaction into a Buffer. - // const paymentBuffer = Buffer.from(txHex, 'hex') - // - // // Generate a Transaction object from the transaction binary data. - // const csTransaction = this.bitcoinJs.Transaction.fromBuffer(paymentBuffer) - // // console.log(`payment tx: ${JSON.stringify(csTransaction, null, 2)}`) - // - // // Instantiate the Transaction Builder. - // const csTransactionBuilder = this.bitcoinJs.TransactionBuilder.fromTransaction( - // csTransaction, - // 'mainnet' - // ) - // - // return csTransactionBuilder - // 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 @@ -410,6 +397,77 @@ class WalletAdapter { 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 diff --git a/src/use-cases/offer/index.js b/src/use-cases/offer/index.js index 45fdd85..bb4f72a 100644 --- a/src/use-cases/offer/index.js +++ b/src/use-cases/offer/index.js @@ -140,6 +140,7 @@ class OfferUseCases { // 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. @@ -148,6 +149,10 @@ class OfferUseCases { 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.partialTxHex = partialTxHex @@ -264,9 +269,31 @@ class OfferUseCases { const txObj = await this.adapters.wallet.deseralizeTx(txHex) console.log(`txObj: ${JSON.stringify(txObj, null, 2)}`) - // Ensure the TX inputs and outputs match the original Order + // 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 diff --git a/test/e2e/manual/02-take-offer.js b/test/e2e/manual/02-take-offer.js index 1224c9d..50208bb 100644 --- a/test/e2e/manual/02-take-offer.js +++ b/test/e2e/manual/02-take-offer.js @@ -12,7 +12,7 @@ async function start () { method: 'post', url: `${LOCALHOST}/offer/take`, data: { - offerCid: 'zdpuAs5Djp9VrnheYTSDVes6f4KJVds2u3juM3MxwFYbTV8m2' + offerCid: 'zdpuAwVTq7nF19VaDw6n49S2RvsVHNkYufZNP515Pqocyae9m' } } diff --git a/test/integration/adapters/wallet-adapter-integration.js b/test/integration/adapters/wallet-adapter-integration.js index a2868eb..4c1d6e6 100644 --- a/test/integration/adapters/wallet-adapter-integration.js +++ b/test/integration/adapters/wallet-adapter-integration.js @@ -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,32 +15,71 @@ 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 () => { - 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 () => { + describe('#completeTx', () => { + it('should complete the transaction with bchjs.TransactionBuilder', async () => { const walletData = await uut.openWallet() // Force usage of FullStack.cash @@ -48,32 +87,9 @@ describe('#wallet', () => { await uut.instanceWallet(walletData) - const inObj = { - tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', - qty: 1 - } + const hex = '0200000002cf24c6f6e55fc84e7699223f7dac568aae991f1a49747758a797d06a516f718c0100000000ffffffff0c7a53c39a7f215c9fa0f409710cf5cb8926e40c104efcedb43d69d5e113fd9d000000006a47304402204d5d1ce837594a151ef8ccecf95b5c7ebb2e71560c6d2597fa559b92f9f032bc02206ef67cd2ef4b4df7207225c26c9982df184959ac389c3f49f1ff5dce641a72ee412103fdc2366a32184220e77fd50e7fff30e7c76b2b7246dea6d56b8177f9fca6fec7ffffffff030000000000000000376a04534c500001010453454e4420a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b208000000000000006422020000000000001976a914d46461bf6a5f3a8e0e0e92b85465ad79696ccb7688ac88130000000000001976a914d46461bf6a5f3a8e0e0e92b85465ad79696ccb7688ac00000000' - 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) + const result = await uut.completeTx(hex, 10) console.log('result: ', result) }) })