From 7ab046aeb15681529d289c41866de0f975bc3f23 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 5 Jul 2024 13:57:25 -0600 Subject: [PATCH 01/55] chore: remove unused 'keys' --- demo.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/demo.js b/demo.js index f3aebc8..038b72a 100644 --- a/demo.js +++ b/demo.js @@ -424,11 +424,8 @@ async function main() { txInfo.outputs.sort(DashTx.sortOutputs); } - let keys = []; for (let input of txInfo.inputs) { let data = keysMap[input.address]; - let addressKey = await xreceiveKey.deriveAddress(data.index); - keys.push(addressKey.privateKey); // DEBUG check pkh hex let pubKeyHashBytes = await DashKeys.addrToPkh(data.address, { version: 'testnet', @@ -438,9 +435,8 @@ async function main() { } let txInfoSigned = await dashTx.hashAndSignAll(txInfo); - console.log('[debug], txInfo, keys, txSigned'); + console.log('[debug], txInfo, txSigned'); console.log(txInfo); - console.log(keys); console.log(txInfoSigned); await sleep(150); let txRpc = await rpc.sendRawTransaction(txInfoSigned.transaction); From 4b36723144c4ca8226edf97789debc13b5dddc78 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 1 Jul 2024 17:16:10 -0600 Subject: [PATCH 02/55] doc: add example.env --- example.env | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 example.env diff --git a/example.env b/example.env new file mode 100644 index 0000000..a370b3c --- /dev/null +++ b/example.env @@ -0,0 +1,16 @@ +# Get these values from ~/.dashmate/local_seed/core/dash.conf or ~/.dashmate/config.json +# regtest= +DASHD_RPC_USER='abcd1234' +DASHD_RPC_PASS='123456789012' +DASHD_RPC_PASSWORD='123456789012' +DASHD_RPC_HOST='127.0.0.1' +# mainnet=9998, testnet=19998, regtest= +DASHD_RPC_PORT='20302' +DASHD_RPC_TIMEOUT='10.0' + +# Generate this from +# npx -p dashphrase-cli -- dashphrase gen --bits 128 -o ./words.txt +# npx -p dashphrase-cli -- dashphrase seed ./words.txt "" -o ./seed.hex +DASH_WALLET_PHRASE='zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong' +#DASH_WALLET_SALT='TREZOR' +DASH_WALLET_SEED='ac27495480225222079d7be181583751e86f571027b0497b5b5d11218e0a8a13332572917f0f8e5a589620c6f15b11c61dee327651a14c34e18231052e48c069' From 449c9cd7e9829c6085c05ec51d19c7b91b8635af Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 13 Jul 2024 01:21:31 +0000 Subject: [PATCH 03/55] chore: update to dashrpc@20 for browser compat --- package-lock.json | 15 ++++++++------- package.json | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 06cb0cd..f073525 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "dashhd": "^3.3.3", "dashkeys": "^1.1.4", "dashphrase": "^1.4.0", - "dashrpc": "^19.0.1", + "dashrpc": "^20.0.0", "dashtx": "^0.18.1", "dotenv": "^16.4.5" } @@ -39,9 +39,10 @@ "integrity": "sha512-o+LdiPkiYmg07kXBE+2bbcJzBmeTQVPn1GS2XlQeo8lene+KknAprSyiYi5XtqV/QVgNjvzOV7qBst2MijSPAA==" }, "node_modules/dashrpc": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/dashrpc/-/dashrpc-19.0.1.tgz", - "integrity": "sha512-1BLXnYZPHHRwvehIF6HqLLSfv2bTZlU97dq/8XJ2F0cBEk3ofi9/fbxYVmDwWtVjqtIJPfdXhSFx7fJu2hJNPA==" + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/dashrpc/-/dashrpc-20.0.0.tgz", + "integrity": "sha512-43IEnwLs6x33OqLQC0qSBoePWcazXWP95maJxQnByGgRklz2kXzZ1v9RH50573YQfbo7iOVW76QBolr8xEwHgw==", + "license": "MIT" }, "node_modules/dashtx": { "version": "0.18.1", @@ -85,9 +86,9 @@ "integrity": "sha512-o+LdiPkiYmg07kXBE+2bbcJzBmeTQVPn1GS2XlQeo8lene+KknAprSyiYi5XtqV/QVgNjvzOV7qBst2MijSPAA==" }, "dashrpc": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/dashrpc/-/dashrpc-19.0.1.tgz", - "integrity": "sha512-1BLXnYZPHHRwvehIF6HqLLSfv2bTZlU97dq/8XJ2F0cBEk3ofi9/fbxYVmDwWtVjqtIJPfdXhSFx7fJu2hJNPA==" + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/dashrpc/-/dashrpc-20.0.0.tgz", + "integrity": "sha512-43IEnwLs6x33OqLQC0qSBoePWcazXWP95maJxQnByGgRklz2kXzZ1v9RH50573YQfbo7iOVW76QBolr8xEwHgw==" }, "dashtx": { "version": "0.18.1", diff --git a/package.json b/package.json index 2ef8115..4795890 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "JavaScript reference implementation of CoinJoin", "main": "index.js", "files": [ - "*.js" + "*.js" ], "scripts": { "bump": "npm version -m \"chore(release): bump to v%s\"", @@ -38,7 +38,7 @@ "dashhd": "^3.3.3", "dashkeys": "^1.1.4", "dashphrase": "^1.4.0", - "dashrpc": "^19.0.1", + "dashrpc": "^20.0.0", "dashtx": "^0.18.1", "dotenv": "^16.4.5" } From 60ce965de1781c1201f8b25f759dbf630974aefd Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 13 Jul 2024 01:24:55 +0000 Subject: [PATCH 04/55] test(ping-pong): show expected vs actual on fail --- tests/ping-pong.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/ping-pong.js b/tests/ping-pong.js index 8befc48..67abcdc 100644 --- a/tests/ping-pong.js +++ b/tests/ping-pong.js @@ -116,7 +116,9 @@ function test() { let expectedStr = `${headerStr},${staticNonceStr}`; let messageStr = messageBytes.toString(); if (expectedStr !== messageStr) { - throw new Error('complete messages did not match'); + throw new Error( + `complete messages did not match: ${expectedStr} !== ${messageStr}`, + ); } } From 7856fbed19b11d9f6e7401582ede857316815c09 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 27 Jun 2024 15:21:10 -0600 Subject: [PATCH 05/55] wip: websocket --- demo.js | 243 ++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 159 insertions(+), 84 deletions(-) diff --git a/demo.js b/demo.js index 038b72a..95548ac 100644 --- a/demo.js +++ b/demo.js @@ -7,8 +7,6 @@ void DotEnv.config({ path: '.env.secret' }); //@ts-ignore - ts can't understand JSON, still... let pkg = require('./package.json'); -let Net = require('node:net'); - let CoinJoin = require('./coinjoin.js'); let Packer = require('./packer.js'); // TODO rename packer let Parser = require('./parser.js'); @@ -38,6 +36,9 @@ let rpcConfig = { host: process.env.DASHD_RPC_HOST || '127.0.0.1', port: process.env.DASHD_RPC_PORT || '19898', // mainnet=9998, testnet=19998, regtest=19898 timeout: 10 * 1000, // bump default from 5s to 10s for up to 10k addresses + onconnected: async function () { + console.info(`[info] rpc client connected ${rpcConfig.host}`); + }, }; if (process.env.DASHD_RPC_TIMEOUT) { let rpcTimeoutSec = parseFloat(process.env.DASHD_RPC_TIMEOUT); @@ -63,13 +64,8 @@ async function main() { let network = 'regtest'; // let minimumParticipants = Packer.NETWORKS[network].minimumParticiparts; - rpcConfig.onconnected = async function () { - let rpc = this; - console.info(`[info] rpc client connected ${rpc.host}`); - }; let rpc = new DashRpc(rpcConfig); - rpc.onconnected = rpcConfig.onconnected; let height = await rpc.init(rpc); console.info(`[info] rpc server is ready. Height = ${height}`); @@ -199,8 +195,8 @@ async function main() { // addresses: unusedAddresses, }); // console.log( - // '[debug] mempooldeltas.result.length', - // mempooldeltas.result.length, + // '[debug] mempooldeltas.result.length', + // mempooldeltas.result.length, // ); // TODO check that we have a duplicate in both deltas by using txid, vin/vout for (let delta of mempooldeltas.result) { @@ -469,9 +465,9 @@ async function main() { // console.log('[debug] generatetoaddress deltas', deltas); // let results = deltas.result.concat(deltas2.result); // for (let delta of results) { - // totalBalance += delta.satoshis; - // keysMap[delta.address].used = true; - // delete unusedMap[delta.address]; + // totalBalance += delta.satoshis; + // keysMap[delta.address].used = true; + // delete unusedMap[delta.address]; // } let utxosRpc = await rpc.getAddressUtxos({ addresses: [data.address] }); @@ -585,18 +581,20 @@ async function main() { } // async function getPrivateKeys(inputs) { - // let keys = []; - // for (let input of inputs) { - // let privKeyBytes = await keyUtils.getPrivateKey(input); - // keys.push(privKeyBytes); - // } + // let keys = []; + // for (let input of inputs) { + // let privKeyBytes = await keyUtils.getPrivateKey(input); + // keys.push(privKeyBytes); + // } - // return keys; + // return keys; // } let evonodes = []; { - let resp = await rpc.masternodelist(); + //let resp = await rpc.masternodelist(); + let res = await fetch('http://127.0.0.1:8080/rpc/masternodelist'); + let resp = await res.json(); let evonodesMap = resp.result; let evonodeProTxIds = Object.keys(evonodesMap); for (let id of evonodeProTxIds) { @@ -623,13 +621,21 @@ async function main() { console.info('[info] chosen evonode:'); console.log(JSON.stringify(evonode, null, 2)); - let conn = Net.createConnection({ - host: evonode.hostname, + let query = { + access_token: 'secret', + hostname: evonode.hostname, port: evonode.port, - keepAlive: true, - keepAliveInitialDelay: 3, - //localAddress: rpc.host, - }); + }; + let searchParams = new URLSearchParams(query); + let search = searchParams.toString(); + let wsc = new WebSocket(`ws://127.0.0.1:8080/tcp?${search}`); + //let conn = Net.createConnection({ + // host: evonode.hostname, + // port: evonode.port, + // keepAlive: true, + // keepAliveInitialDelay: 3, + // //localAddress: rpc.host, + //}); /** @type {Array} */ let chunks = []; @@ -639,18 +645,31 @@ async function main() { function onError(err) { console.error('Error:'); console.error(err); - conn.removeListener('error', onError); + // conn.removeListener('error', onError); + wsc.onerror = null; errReject(err); } function onEnd() { console.info('[info] disconnected from server'); } - conn.on('error', onError); - conn.once('end', onEnd); - conn.setMaxListeners(2); + // conn.on('error', onError); + // conn.once('end', onEnd); + // conn.setMaxListeners(2); + wsc.onerror = onError; + wsc.onclose = onEnd; + let dataCount = 0; - conn.on('data', function (data) { - console.log('[DEBUG] data'); + // conn.on('data', function (data) { + // console.log('[DEBUG] data'); + // console.log(dataCount, data.length, data.toString('hex')); + // dataCount += 1; + // }); + console.log('[DEBUG] main add wsc.onmessage'); + wsc.addEventListener('message', async function (wsevent) { + console.log('[DEBUG] main wsc.onmessage'); + let ab = await wsevent.data.arrayBuffer(); + let data = Buffer.from(ab); + console.log('[DEBUG] data (main)'); console.log(dataCount, data.length, data.toString('hex')); dataCount += 1; }); @@ -672,7 +691,8 @@ async function main() { message: pongMessageBytes, nonce: msg.payload, }); - conn.write(pongMessageBytes); + // conn.write(pongMessageBytes); + wsc.send(pongMessageBytes); console.log('[debug] sent pong'); continue; } @@ -714,13 +734,16 @@ async function main() { let header; function cleanup() { + console.log('[DEBUG] [readMessage.cleanup] remove data listener'); + wsc.removeEventListener('message', onWsReadableHeader); + wsc.removeEventListener('message', onWsReadablePayload); // console.log("[debug] readMessage handlers: remove 'onReadableHeader'"); - conn.removeListener('data', onReadableHeader); - conn.removeListener('readable', onReadableHeader); + // conn.removeListener('data', onReadableHeader); + // conn.removeListener('readable', onReadableHeader); // console.log("[debug] readMessage handlers: remove 'onReadablePayload'"); - conn.removeListener('data', onReadablePayload); - conn.removeListener('readable', onReadablePayload); + // conn.removeListener('data', onReadablePayload); + // conn.removeListener('readable', onReadablePayload); } function resolve(data) { @@ -768,8 +791,11 @@ async function main() { throw new Error('too big you are, handle you I cannot'); } // console.log('DEBUG header', header); - conn.removeListener('readable', onReadableHeader); - conn.removeListener('data', onReadableHeader); + console.log('[DEBUG] [onReadableHeader] remove data listener'); + // conn.removeListener('readable', onReadableHeader); + // conn.removeListener('data', onReadableHeader); + //wsc.onmessage = null; + wsc.removeEventListener('message', onWsReadableHeader); if (header.payloadSize === 0) { resolve(header); @@ -778,9 +804,19 @@ async function main() { // console.log("[debug] readMessage handlers: add 'onReadablePayload'"); //conn.on('readable', onReadablePayload); - conn.on('data', onReadablePayload); + // conn.on('data', onReadablePayload); + console.log('[DEBUG] onReadableHeader add wsc.onmessage'); + wsc.addEventListener('message', onWsReadablePayload); onReadablePayload(null); } + async function onWsReadableHeader(wsevent) { + console.log('[DEBUG] onReadableHeader wsc.onmessage'); + let ab = await wsevent.data.arrayBuffer(); + let data = Buffer.from(ab); + console.log('[DEBUG] data (readable header)'); + console.log(dataCount, data.length, data.toString('hex')); + onReadableHeader(data); + } function onReadablePayload(data) { let size = data?.length || 0; @@ -816,16 +852,29 @@ async function main() { chunk = chunk.slice(0, header.payloadSize); } header.payload = chunk; - conn.removeListener('readable', onReadablePayload); - conn.removeListener('data', onReadablePayload); + console.log('[DEBUG] [onReadablePayload] remove data listener'); + // conn.removeListener('readable', onReadablePayload); + // conn.removeListener('data', onReadablePayload); + // wsc.onmessage = null; + wsc.removeEventListener('message', onWsReadablePayload); resolve(header); } + async function onWsReadablePayload(wsevent) { + console.log('[DEBUG] onReadablePayload wsc.onmessage'); + let ab = await wsevent.data.arrayBuffer(); + let data = Buffer.from(ab); + console.log('[DEBUG] data (readable payload)'); + console.log(dataCount, data.length, data.toString('hex')); + onReadablePayload(data); + } errReject = reject; // console.log("[debug] readMessage handlers: add 'onReadableHeader'"); //conn.on('readable', onReadableHeader); - conn.on('data', onReadableHeader); + // conn.on('data', onReadableHeader); + console.log('[DEBUG] readMessage add wsc.onmessage'); + wsc.addEventListener('message', onWsReadableHeader); if (chunks.length) { onReadableHeader(null); @@ -840,8 +889,11 @@ async function main() { // TODO setTimeout await new Promise(function (_resolve, _reject) { function cleanup() { - conn.removeListener('readable', onReadable); - conn.removeListener('data', onReadable); + console.log('[DEBUG] [waitForConnect.cleanup] remove data listener'); + // conn.removeListener('readable', onReadable); + // conn.removeListener('data', onReadable); + // wsc.onmessage = null; + wsc.removeEventListener('message', onWsReadable); } function resolve(data) { @@ -855,6 +907,7 @@ async function main() { } function onConnect() { + console.log('[DEBUG] waitForConnect wsc.onopen'); resolve(); } @@ -862,11 +915,23 @@ async function main() { // checking an impossible condition, just in case throw new Error('unexpected response before request'); } + async function onWsReadable(wsevent) { + console.log('[DEBUG] waitForConnect wsc.onmessage'); + let ab = await wsevent.data.arrayBuffer(); + let data = Buffer.from(ab); + console.log('[DEBUG] data (readable)'); + console.log(dataCount, data.length, data.toString('hex')); + onReadable(data); + } errReject = reject; - conn.once('connect', onConnect); + // conn.once('connect', onConnect); + wsc.onopen = null; + wsc.onopen = onConnect; //conn.on('readable', onReadable); - conn.on('data', onReadable); + // conn.on('data', onReadable); + console.log('[DEBUG] waitForConnect add wsc.onmessage'); + wsc.addEventListener('message', onWsReadable); }); } @@ -912,7 +977,8 @@ async function main() { }; }); await sleep(150); - conn.write(versionMsg); + // conn.write(versionMsg); + wsc.send(versionMsg); await versionP; } @@ -936,7 +1002,8 @@ async function main() { payload: null, }); await sleep(150); - conn.write(verackBytes); + // conn.write(verackBytes); + wsc.send(verackBytes); await verackP; } @@ -965,10 +1032,11 @@ async function main() { send: true, }); await sleep(150); - conn.write(sendDsqMessage); + // conn.write(sendDsqMessage); + wsc.send(sendDsqMessage); console.log("[debug] sending 'senddsq':", sendDsqMessage); - resolve(); + resolve(null); listenerMap['senddsq'] = null; delete listenerMap['senddsq']; }; @@ -991,6 +1059,8 @@ async function main() { void (await generateMinBalance()); let collateralTxInfo = await getCollateralTx(); + // let keys = await getPrivateKeys(collateralTxInfo.inputs); + // let txInfoSigned = await dashTx.hashAndSignAll(collateralTxInfo, keys); let txInfoSigned = await dashTx.hashAndSignAll(collateralTxInfo); collateralTx = DashTx.utils.hexToBytes(txInfoSigned.transaction); } @@ -1000,7 +1070,8 @@ async function main() { collateralTx, }); await sleep(150); - conn.write(dsaMsg); + // conn.write(dsaMsg); + wsc.send(dsaMsg); let dsaBuf = Buffer.from(dsaMsg); console.log('[debug] dsa', dsaBuf.toString('hex')); @@ -1133,6 +1204,8 @@ async function main() { { void (await generateMinBalance()); let collateralTxInfo = await getCollateralTx(); + // let keys = await getPrivateKeys(collateralTxInfo.inputs); + // let txInfoSigned = await dashTx.hashAndSignAll(collateralTxInfo, keys); let txInfoSigned = await dashTx.hashAndSignAll(collateralTxInfo); let collateralTx = DashTx.utils.hexToBytes(txInfoSigned.transaction); @@ -1143,7 +1216,8 @@ async function main() { outputs, }); await sleep(150); - conn.write(dsiMessageBytes); + // conn.write(dsiMessageBytes); + wsc.send(dsiMessageBytes); dsf = await dsfP; } @@ -1192,28 +1266,28 @@ async function main() { sighashInput.sigHashType = sigHashType; console.log('[debug] YES, CAN HAZ INPUTS!!!', sighashInput); // sighashInputs.push({ - // txId: input.txId || input.txid, - // txid: input.txid || input.txId, - // outputIndex: input.outputIndex, - // pubKeyHash: input.pubKeyHash, - // sigHashType: input.sigHashType, + // txId: input.txId || input.txid, + // txid: input.txid || input.txId, + // outputIndex: input.outputIndex, + // pubKeyHash: input.pubKeyHash, + // sigHashType: input.sigHashType, // }); break; } // if (sighashInputs.length !== 1) { - // let msg = - // 'expected exactly one selected input to match one tx request input'; - // throw new Error(msg); + // let msg = + // 'expected exactly one selected input to match one tx request input'; + // throw new Error(msg); // } // let anyonecanpayIndex = 0; // let txHashable = DashTx.createHashable( - // { - // version: txInfo.version, - // inputs: sighashInputs, // exactly 1 - // outputs: txInfo.outputs, - // locktime: txInfo.locktime, - // }, - // anyonecanpayIndex, + // { + // version: txInfo.version, + // inputs: sighashInputs, // exactly 1 + // outputs: txInfo.outputs, + // locktime: txInfo.locktime, + // }, + // anyonecanpayIndex, // ); // console.log('[debug] txHashable (pre-sighashbyte)', txHashable); @@ -1251,7 +1325,8 @@ async function main() { let dssHex = DashTx.utils.bytesToHex(dssMessageBytes); console.log(dssHex); await sleep(150); - conn.write(dssMessageBytes); + // conn.write(dssMessageBytes); + wsc.send(dssMessageBytes); await dscP; } @@ -1276,22 +1351,22 @@ function byId(a, b) { // http://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array // function shuffle(arr) { -// let currentIndex = arr.length; - -// // While there remain elements to shuffle... -// for (; currentIndex !== 0; ) { -// // Pick a remaining element... -// let randomIndexFloat = Math.random() * currentIndex; -// let randomIndex = Math.floor(randomIndexFloat); -// currentIndex -= 1; - -// // And swap it with the current element. -// let temporaryValue = arr[currentIndex]; -// arr[currentIndex] = arr[randomIndex]; -// arr[randomIndex] = temporaryValue; -// } - -// return arr; +// let currentIndex = arr.length; + +// // While there remain elements to shuffle... +// for (; currentIndex !== 0; ) { +// // Pick a remaining element... +// let randomIndexFloat = Math.random() * currentIndex; +// let randomIndex = Math.floor(randomIndexFloat); +// currentIndex -= 1; + +// // And swap it with the current element. +// let temporaryValue = arr[currentIndex]; +// arr[currentIndex] = arr[randomIndex]; +// arr[randomIndex] = temporaryValue; +// } + +// return arr; // } function sleep(ms) { From 8809422b3d2475a38c4c6614184c3396ed9f9538 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 13 Jul 2024 01:20:25 +0000 Subject: [PATCH 06/55] ref!: make browser compatible --- coinjoin.js | 53 -- demo.js | 2422 ++++++++++++++++++++++++++------------------------- packer.js | 1427 ++++++++++++++++-------------- parser.js | 771 ++++++++-------- 4 files changed, 2398 insertions(+), 2275 deletions(-) delete mode 100644 coinjoin.js diff --git a/coinjoin.js b/coinjoin.js deleted file mode 100644 index 7c2de2d..0000000 --- a/coinjoin.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -let CoinJoin = module.exports; - -CoinJoin.STANDARD_DENOMINATIONS = [ - // 0.00100001 - 100001, - // 0.01000010 - 1000010, - // 0.10000100 - 10000100, - // 1.00001000 - 100001000, - // 10.0000100 - 1000010000, -]; - -// TODO the spec seems to be more of an ID, though -// the implementation makes it look more like a mask... -CoinJoin.STANDARD_DENOMINATION_MASKS = { - // 0.00100001 - 100001: 0b00010000, - // 0.01000010 - 1000010: 0b00001000, - // 0.10000100 - 10000100: 0b00000100, - // 1.00001000 - 100001000: 0b00000010, - // 10.00010000 - 1000010000: 0b00000001, -}; - -CoinJoin.STANDARD_DENOMINATIONS_MAP = { - // 0.00100001 - 0b00010000: 100001, - // 0.01000010 - 0b00001000: 1000010, - // 0.10000100 - 0b00000100: 10000100, - // 1.00001000 - 0b00000010: 100001000, - // 10.00010000 - 0b00000001: 1000010000, -}; - -// (STANDARD_DENOMINATIONS[0] / 10).floor(); -CoinJoin.COLLATERAL = 10000; -// COLLATERAL * 4 -CoinJoin.MAX_COLLATERAL = 40000; - -CoinJoin.isDenominated = function (sats) { - return CoinJoin.STANDARD_DENOMINATIONS.includes(sats); -}; diff --git a/demo.js b/demo.js index 95548ac..befb93b 100644 --- a/demo.js +++ b/demo.js @@ -1,1387 +1,1411 @@ -'use strict'; - -let DotEnv = require('dotenv'); -void DotEnv.config({ path: '.env' }); -void DotEnv.config({ path: '.env.secret' }); - -//@ts-ignore - ts can't understand JSON, still... -let pkg = require('./package.json'); - -let CoinJoin = require('./coinjoin.js'); -let Packer = require('./packer.js'); // TODO rename packer -let Parser = require('./parser.js'); - -let DashPhrase = require('dashphrase'); -let DashHd = require('dashhd'); -let DashKeys = require('dashkeys'); -let DashRpc = require('dashrpc'); -let DashTx = require('dashtx'); -let Secp256k1 = require('@dashincubator/secp256k1'); - -const DENOM_LOWEST = 100001; -const PREDENOM_MIN = DENOM_LOWEST + 193; -// const MIN_UNUSED = 2500; -const MIN_UNUSED = 1000; -const MIN_BALANCE = 100001 * 1000; -const MIN_DENOMINATED = 200; - -// https://github.com/dashpay/dash/blob/v19.x/src/coinjoin/coinjoin.h#L39 -// const COINJOIN_ENTRY_MAX_SIZE = 9; // real -const COINJOIN_ENTRY_MAX_SIZE = 2; // just for testing right now - -let rpcConfig = { - protocol: 'http', // https for remote, http for local / private networking - user: process.env.DASHD_RPC_USER, - pass: process.env.DASHD_RPC_PASS || process.env.DASHD_RPC_PASSWORD, - host: process.env.DASHD_RPC_HOST || '127.0.0.1', - port: process.env.DASHD_RPC_PORT || '19898', // mainnet=9998, testnet=19998, regtest=19898 - timeout: 10 * 1000, // bump default from 5s to 10s for up to 10k addresses - onconnected: async function () { - console.info(`[info] rpc client connected ${rpcConfig.host}`); - }, -}; -if (process.env.DASHD_RPC_TIMEOUT) { - let rpcTimeoutSec = parseFloat(process.env.DASHD_RPC_TIMEOUT); - rpcConfig.timeout = rpcTimeoutSec * 1000; -} - -async function main() { - /* jshint maxstatements: 1000 */ - /* jshint maxcomplexity: 100 */ - - let walletSalt = process.argv[2] || ''; - let isHelp = walletSalt === 'help' || walletSalt === '--help'; - if (isHelp) { - throw new Error( - `USAGE\n ${process.argv[1]} [wallet-salt]\n\nEXAMPLE\n ${process.argv[1]} 'luke|han|chewie'`, - ); +//@ts-ignore +var CJDemo = ('object' === typeof module && exports) || {}; +(function (window, CJDemo) { + 'use strict'; + + let DotEnv = window.ENVS || require('dotenv'); + if (DotEnv.config) { + void DotEnv.config({ path: '.env' }); + void DotEnv.config({ path: '.env.secret' }); } - let walletPhrase = process.env.DASH_WALLET_PHRASE || ''; - if (!walletPhrase) { - throw new Error('missing DASH_WALLET_PHRASE'); + //@ts-ignore - ts can't understand JSON, still... + let pkg = window.ENVS.package || require('./package.json'); + + let Packer = require('./packer.js'); // TODO rename packer + let Parser = require('./parser.js'); + + let DashPhrase = window.DashPhrase || require('dashphrase'); + let DashHd = window.DashHd || require('dashhd'); + let DashKeys = window.DashKeys || require('dashkeys'); + let DashRpc = window.DashRpc || require('dashrpc'); + let DashTx = window.DashTx || require('dashtx'); + let Secp256k1 = window.Secp256k1 || require('@dashincubator/secp256k1'); + + // (STANDARD_DENOMINATIONS[0] / 10).floor(); + const COLLATERAL = 10000; + // COLLATERAL * 4 + // const MAX_COLLATERAL = 40000; + + const DENOM_LOWEST = 100001; + const PREDENOM_MIN = DENOM_LOWEST + 193; + // const MIN_UNUSED = 2500; + const MIN_UNUSED = 1000; + const MIN_BALANCE = 100001 * 1000; + const MIN_DENOMINATED = 200; + + // https://github.com/dashpay/dash/blob/v19.x/src/coinjoin/coinjoin.h#L39 + // const COINJOIN_ENTRY_MAX_SIZE = 9; // real + const COINJOIN_ENTRY_MAX_SIZE = 2; // just for testing right now + + let rpcConfig = { + protocol: 'http', // https for remote, http for local / private networking + user: process.env.DASHD_RPC_USER, + pass: process.env.DASHD_RPC_PASS || process.env.DASHD_RPC_PASSWORD, + host: process.env.DASHD_RPC_HOST || '127.0.0.1', + port: process.env.DASHD_RPC_PORT || '19898', // mainnet=9998, testnet=19998, regtest=19898 + timeout: 10 * 1000, // bump default from 5s to 10s for up to 10k addresses + onconnected: async function () { + console.info(`[info] rpc client connected ${rpcConfig.host}`); + }, + }; + if (process.env.DASHD_RPC_TIMEOUT) { + let rpcTimeoutSec = parseFloat(process.env.DASHD_RPC_TIMEOUT); + rpcConfig.timeout = rpcTimeoutSec * 1000; } - let network = 'regtest'; - // let minimumParticipants = Packer.NETWORKS[network].minimumParticiparts; + async function main() { + /* jshint maxstatements: 1000 */ + /* jshint maxcomplexity: 100 */ - let rpc = new DashRpc(rpcConfig); - let height = await rpc.init(rpc); - console.info(`[info] rpc server is ready. Height = ${height}`); + let walletSalt = process.argv[2] || ''; + let isHelp = walletSalt === 'help' || walletSalt === '--help'; + if (isHelp) { + throw new Error( + `USAGE\n ${process.argv[1]} [wallet-salt]\n\nEXAMPLE\n ${process.argv[1]} 'luke|han|chewie'`, + ); + } - let keyUtils = { - sign: async function (privKeyBytes, hashBytes) { - let sigOpts = { canonical: true, extraEntropy: true }; - let sigBytes = await Secp256k1.sign(hashBytes, privKeyBytes, sigOpts); - return sigBytes; - }, - getPrivateKey: async function (input) { - if (!input.address) { - //throw new Error('should put the address on the input there buddy...'); - console.warn('missing address:', input.txid, input.outputIndex); - return null; - } - let data = keysMap[input.address]; - let isUint = data.index > -1; - if (!isUint) { - throw new Error(`missing 'index'`); - } - // TODO map xkey by walletid - let addressKey = await xreceiveKey.deriveAddress(data.index); + let walletPhrase = process.env.DASH_WALLET_PHRASE || ''; + if (!walletPhrase) { + throw new Error('missing DASH_WALLET_PHRASE'); + } - { - // sanity check - let privKeyHex = DashTx.utils.bytesToHex(addressKey.privateKey); - if (data._privKeyHex !== privKeyHex) { - if (data._privKeyHex) { - console.log(data._privKeyHex); - console.log(privKeyHex); - throw new Error('mismatch key bytes'); + let network = 'regtest'; + // let minimumParticipants = Packer.NETWORKS[network].minimumParticiparts; + + let rpc = DashRpc.create(rpcConfig); + let height = await rpc.init(rpc); + console.info(`[info] rpc server is ready. Height = ${height}`); + + let keyUtils = { + sign: async function (privKeyBytes, hashBytes) { + let sigOpts = { canonical: true, extraEntropy: true }; + let sigBytes = await Secp256k1.sign(hashBytes, privKeyBytes, sigOpts); + return sigBytes; + }, + getPrivateKey: async function (input) { + if (!input.address) { + //throw new Error('should put the address on the input there buddy...'); + console.warn('missing address:', input.txid, input.outputIndex); + return null; + } + let data = keysMap[input.address]; + let isUint = data.index > -1; + if (!isUint) { + throw new Error(`missing 'index'`); + } + // TODO map xkey by walletid + let addressKey = await xreceiveKey.deriveAddress(data.index); + + { + // sanity check + let privKeyHex = DashTx.utils.bytesToHex(addressKey.privateKey); + if (data._privKeyHex !== privKeyHex) { + if (data._privKeyHex) { + console.log(data._privKeyHex); + console.log(privKeyHex); + throw new Error('mismatch key bytes'); + } + data._privKeyHex = privKeyHex; } - data._privKeyHex = privKeyHex; } + return addressKey.privateKey; + }, + toPublicKey: async function (privKeyBytes) { + // TODO use secp256k1 directly + return await DashKeys.utils.toPublicKey(privKeyBytes); + }, + }; + let dashTx = DashTx.create(keyUtils); + + let testCoin = '1'; + let seedBytes = await DashPhrase.toSeed(walletPhrase, walletSalt); + let walletKey = await DashHd.fromSeed(seedBytes, { + coinType: testCoin, + versions: DashHd.TESTNET, + }); + let walletId = await DashHd.toId(walletKey); + + let accountHdpath = `m/44'/1'/0'`; + let accountKey = await walletKey.deriveAccount(0); + let xreceiveKey = await accountKey.deriveXKey(walletKey, 0); //jshint ignore:line + // let xchangeKey = await accountKey.deriveXKey(walletKey, 1); + // let xprvHdpath = `m/44'/5'/0'/0`; + // let xprvKey = await DashHd.derivePath(walletKey, xprvHdpath); + + // generate bunches of keys + // remove the leading `m/` or `m'/` + let partialPath = accountHdpath.replace(/^m'?\//, ''); + let totalBalance = 0; + let keysMap = {}; //jshint ignore:line + let used = []; + let addresses = []; + let unusedMap = {}; + let index = 0; + let numAddresses = 100; + for (;;) { + let uncheckedAddresses = []; + for (let i = 0; i < numAddresses; i += 1) { + let addressKey = await xreceiveKey.deriveAddress(index); + + // Descriptors are in the form of + // - pkh(xpub123...abc/2) - for the 3rd address of a receiving or change xpub + // - pkh(xpub456...def/0/2) - for the 3rd receive address of an account xpub + // - pkh([walletid/44'/0'/0']xpub123...abc/0/2) - same, plus wallet & hd info + // - pkh([walletid/44'/0'/0'/0/2]Xaddr...#checksum) - same, but the address + // See also: https://github.com/dashpay/dash/blob/master/doc/descriptors.md + // TODO sort out sha vs double-sha vs fingerprint + let descriptor = `pkh([${walletId}/${partialPath}/0/${index}])`; + let address = await DashHd.toAddr(addressKey.publicKey, { + version: 'testnet', + }); + // let utxosRpc = await rpc.getAddressUtxos({ addresses: [address] }); + // let utxos = utxosRpc.result; + // console.log('utxosRpc.result.length', utxosRpc.result.length); + + let data = keysMap[address]; + if (!data) { + data = { + walletId: walletId, + prefix: "m/44'/1'", + account: 0, + usage: 0, + index: index, + descriptor: descriptor, + address: address, + // uxtos: utxos, + used: false, + reserved: 0, + satoshis: 0, + }; + // console.log('[debug] addr info', data); + addresses.push(address); + uncheckedAddresses.push(address); + } + keysMap[index] = data; + keysMap[address] = data; + // console.log('[DEBUG] address:', address); + if (!data.used) { + unusedMap[address] = data; + } + + index += 1; } - return addressKey.privateKey; - }, - toPublicKey: async function (privKeyBytes) { - // TODO use secp256k1 directly - return await DashKeys.utils.toPublicKey(privKeyBytes); - }, - }; - let dashTx = DashTx.create(keyUtils); - - let testCoin = '1'; - let seedBytes = await DashPhrase.toSeed(walletPhrase, walletSalt); - let walletKey = await DashHd.fromSeed(seedBytes, { - coinType: testCoin, - versions: DashHd.TESTNET, - }); - let walletId = await DashHd.toId(walletKey); - - let accountHdpath = `m/44'/1'/0'`; - let accountKey = await walletKey.deriveAccount(0); - let xreceiveKey = await accountKey.deriveXKey(walletKey, 0); //jshint ignore:line - // let xchangeKey = await accountKey.deriveXKey(walletKey, 1); - // let xprvHdpath = `m/44'/5'/0'/0`; - // let xprvKey = await DashHd.derivePath(walletKey, xprvHdpath); - - // generate bunches of keys - // remove the leading `m/` or `m'/` - let partialPath = accountHdpath.replace(/^m'?\//, ''); - let totalBalance = 0; - let keysMap = {}; //jshint ignore:line - let used = []; - let addresses = []; - let unusedMap = {}; - let index = 0; - let numAddresses = 100; - for (;;) { - let uncheckedAddresses = []; - for (let i = 0; i < numAddresses; i += 1) { - let addressKey = await xreceiveKey.deriveAddress(index); - - // Descriptors are in the form of - // - pkh(xpub123...abc/2) - for the 3rd address of a receiving or change xpub - // - pkh(xpub456...def/0/2) - for the 3rd receive address of an account xpub - // - pkh([walletid/44'/0'/0']xpub123...abc/0/2) - same, plus wallet & hd info - // - pkh([walletid/44'/0'/0'/0/2]Xaddr...#checksum) - same, but the address - // See also: https://github.com/dashpay/dash/blob/master/doc/descriptors.md - // TODO sort out sha vs double-sha vs fingerprint - let descriptor = `pkh([${walletId}/${partialPath}/0/${index}])`; - let address = await DashHd.toAddr(addressKey.publicKey, { - version: 'testnet', - }); - // let utxosRpc = await rpc.getAddressUtxos({ addresses: [address] }); - // let utxos = utxosRpc.result; - // console.log('utxosRpc.result.length', utxosRpc.result.length); - - let data = keysMap[address]; - if (!data) { - data = { - walletId: walletId, - prefix: "m/44'/1'", - account: 0, - usage: 0, - index: index, - descriptor: descriptor, - address: address, - // uxtos: utxos, - used: false, - reserved: 0, - satoshis: 0, - }; - // console.log('[debug] addr info', data); - addresses.push(address); - uncheckedAddresses.push(address); - } - keysMap[index] = data; - keysMap[address] = data; - // console.log('[DEBUG] address:', address); - if (!data.used) { - unusedMap[address] = data; - } + // console.log('[debug] addresses.length', addresses.length); + // console.log('[debug] uncheckedAddresses.length', uncheckedAddresses.length); - index += 1; - } - // console.log('[debug] addresses.length', addresses.length); - // console.log('[debug] uncheckedAddresses.length', uncheckedAddresses.length); + // TODO segment unused addresses + // let unusedAddresses = Object.keys(unusedMap); + // console.log('[debug] unusedAddresses.length', unusedAddresses.length); - // TODO segment unused addresses - // let unusedAddresses = Object.keys(unusedMap); - // console.log('[debug] unusedAddresses.length', unusedAddresses.length); + let mempooldeltas = await rpc.getAddressMempool({ + addresses: uncheckedAddresses, + // addresses: unusedAddresses, + }); + // console.log( + // '[debug] mempooldeltas.result.length', + // mempooldeltas.result.length, + // ); + // TODO check that we have a duplicate in both deltas by using txid, vin/vout + for (let delta of mempooldeltas.result) { + totalBalance += delta.satoshis; + + let data = keysMap[delta.address]; + data.satoshis += delta.satoshis; + data.used = true; + if (!used.includes(data)) { + used.push(data); + } + delete unusedMap[data.address]; + } - let mempooldeltas = await rpc.getAddressMempool({ - addresses: uncheckedAddresses, - // addresses: unusedAddresses, - }); - // console.log( - // '[debug] mempooldeltas.result.length', - // mempooldeltas.result.length, - // ); - // TODO check that we have a duplicate in both deltas by using txid, vin/vout - for (let delta of mempooldeltas.result) { - totalBalance += delta.satoshis; - - let data = keysMap[delta.address]; - data.satoshis += delta.satoshis; - data.used = true; - if (!used.includes(data)) { - used.push(data); + let deltas = await rpc.getAddressDeltas({ + addresses: uncheckedAddresses, + }); + // console.log('[debug] deltas.result.length', deltas.result.length); + for (let delta of deltas.result) { + totalBalance += delta.satoshis; + + let data = keysMap[delta.address]; + data.satoshis += delta.satoshis; + data.used = true; + if (!used.includes(data)) { + used.push(data); + } + delete unusedMap[data.address]; } - delete unusedMap[data.address]; - } - let deltas = await rpc.getAddressDeltas({ - addresses: uncheckedAddresses, - }); - // console.log('[debug] deltas.result.length', deltas.result.length); - for (let delta of deltas.result) { - totalBalance += delta.satoshis; - - let data = keysMap[delta.address]; - data.satoshis += delta.satoshis; - data.used = true; - if (!used.includes(data)) { - used.push(data); + let numUnused = addresses.length - used.length; + if (numUnused >= MIN_UNUSED) { + // console.log('[debug] addresses.length', addresses.length); + // console.log('[debug] used.length', used.length); + break; } - delete unusedMap[data.address]; } + console.log('[debug] wallet balance:', totalBalance); - let numUnused = addresses.length - used.length; - if (numUnused >= MIN_UNUSED) { - // console.log('[debug] addresses.length', addresses.length); - // console.log('[debug] used.length', used.length); - break; - } - } - console.log('[debug] wallet balance:', totalBalance); + let denomination = 100001 * 1; - let denomination = 100001 * 1; + void (await generateMinBalance()); + void (await generateDenominations()); - void (await generateMinBalance()); - void (await generateDenominations()); + // TODO sort denominated + // for (let addr of addresses) { ... } - // TODO sort denominated - // for (let addr of addresses) { ... } + async function generateMinBalance() { + for (let addr of addresses) { + // console.log('[debug] totalBalance:', totalBalance); + if (totalBalance >= MIN_BALANCE) { + break; + } - async function generateMinBalance() { - for (let addr of addresses) { - // console.log('[debug] totalBalance:', totalBalance); - if (totalBalance >= MIN_BALANCE) { - break; - } + let data = keysMap[addr]; + let isAvailable = !data.used && !data.reserved; + if (!isAvailable) { + continue; + } - let data = keysMap[addr]; - let isAvailable = !data.used && !data.reserved; - if (!isAvailable) { - continue; + void (await generateToAddressAndUpdateBalance(data)); } - - void (await generateToAddressAndUpdateBalance(data)); } - } - async function generateDenominations() { - // jshint maxcomplexity: 25 - let denomCount = 0; - let denominable = []; - let denominated = {}; - for (let addr of addresses) { - let data = keysMap[addr]; - if (data.reserved) { - continue; - } - if (data.satoshis === 0) { - continue; - } + async function generateDenominations() { + // jshint maxcomplexity: 25 + let denomCount = 0; + let denominable = []; + let denominated = {}; + for (let addr of addresses) { + let data = keysMap[addr]; + if (data.reserved) { + continue; + } + if (data.satoshis === 0) { + continue; + } - // TODO denominations.includes(data.satoshis) - let isUndenominated = data.satoshis % DENOM_LOWEST; - if (isUndenominated) { - if (data.satoshis >= PREDENOM_MIN) { - denominable.push(data); + // TODO denominations.includes(data.satoshis) + let isUndenominated = data.satoshis % DENOM_LOWEST; + if (isUndenominated) { + if (data.satoshis >= PREDENOM_MIN) { + denominable.push(data); + } + continue; } - continue; - } - if (!denominated[data.satoshis]) { - denominated[data.satoshis] = []; + if (!denominated[data.satoshis]) { + denominated[data.satoshis] = []; + } + denomCount += 1; + denominated[data.satoshis].push(data); } - denomCount += 1; - denominated[data.satoshis].push(data); - } - // CAVEAT: this fee-approximation strategy that guarantees - // to denominate all coins _correctly_, but in some cases will - // create _smaller_ denominations than necessary - specifically - // 10 x 100001 instead of 1 x 1000010 when the lowest order of - // coin is near the single coin value (i.e. 551000010) - // (because 551000010 / 100194 yields 5499 x 100001 coins + full fees, - // but we actually only generate 5 + 4 + 9 + 9 = 27 coins, leaving - // well over 5472 * 193 extra value) - for (let data of denominable) { - // console.log('[debug] denominable', data); - if (denomCount >= MIN_DENOMINATED) { - break; - } + // CAVEAT: this fee-approximation strategy that guarantees + // to denominate all coins _correctly_, but in some cases will + // create _smaller_ denominations than necessary - specifically + // 10 x 100001 instead of 1 x 1000010 when the lowest order of + // coin is near the single coin value (i.e. 551000010) + // (because 551000010 / 100194 yields 5499 x 100001 coins + full fees, + // but we actually only generate 5 + 4 + 9 + 9 = 27 coins, leaving + // well over 5472 * 193 extra value) + for (let data of denominable) { + // console.log('[debug] denominable', data); + if (denomCount >= MIN_DENOMINATED) { + break; + } - let fee = data.satoshis; - - // 123 means - // - 3 x 100001 - // - 2 x 1000010 - // - 1 x 10000100 - let order = data.satoshis / PREDENOM_MIN; - order = Math.floor(order); - let orderStr = order.toString(); - // TODO mod and divide to loop and shift positions, rather than stringify - let orders = orderStr.split(''); - orders.reverse(); - - // TODO Math.min(orders.length, STANDARD_DENOMS.length); - // let numOutputs = 0; - let denomOutputs = []; - // let magnitudes = [0]; - for (let i = 0; i < orders.length; i += 1) { - let order = orders[i]; - let count = parseInt(order, 10); - let orderSingle = DENOM_LOWEST * Math.pow(10, i); - // let orderTotal = count * orderSingle; - // numOutputs += count; - for (let i = 0; i < count; i += 1) { - fee -= orderSingle; + let fee = data.satoshis; + + // 123 means + // - 3 x 100001 + // - 2 x 1000010 + // - 1 x 10000100 + let order = data.satoshis / PREDENOM_MIN; + order = Math.floor(order); + let orderStr = order.toString(); + // TODO mod and divide to loop and shift positions, rather than stringify + let orders = orderStr.split(''); + orders.reverse(); + + // TODO Math.min(orders.length, STANDARD_DENOMS.length); + // let numOutputs = 0; + let denomOutputs = []; + // let magnitudes = [0]; + for (let i = 0; i < orders.length; i += 1) { + let order = orders[i]; + let count = parseInt(order, 10); + let orderSingle = DENOM_LOWEST * Math.pow(10, i); + // let orderTotal = count * orderSingle; + // numOutputs += count; + for (let i = 0; i < count; i += 1) { + fee -= orderSingle; + denomOutputs.push({ + satoshis: orderSingle, + }); + } + // magnitudes.push(count); + } + // example: + // [ 0, 3, 2, 1 ] + // - 0 x 100001 * 0 + // - 3 x 100001 * 1 + // - 2 x 100001 * 10 + // - 1 x 100001 * 100 + + // console.log('[debug] denom outputs', denomOutputs); + // console.log('[debug] fee', fee); + // Note: this is where we reconcile the difference between + // the number of the smallest denom, and the number of actual denoms + // (and where we may end up with 10 x LOWEST, which we could carry + // over into the next tier, but won't right now for simplicity). + for (;;) { + let numInputs = 1; + let fees = DashTx._appraiseCounts(numInputs, denomOutputs.length + 1); + let nextCoinCost = DENOM_LOWEST + fees.max; + if (fee < nextCoinCost) { + // TODO split out 10200 (or 10193) collaterals as well + break; + } + fee -= DashTx.OUTPUT_SIZE; + fee -= DENOM_LOWEST; denomOutputs.push({ - satoshis: orderSingle, + satoshis: DENOM_LOWEST, }); + // numOutputs += 1; + // magnitudes[1] += 1; } - // magnitudes.push(count); - } - // example: - // [ 0, 3, 2, 1 ] - // - 0 x 100001 * 0 - // - 3 x 100001 * 1 - // - 2 x 100001 * 10 - // - 1 x 100001 * 100 - - // console.log('[debug] denom outputs', denomOutputs); - // console.log('[debug] fee', fee); - // Note: this is where we reconcile the difference between - // the number of the smallest denom, and the number of actual denoms - // (and where we may end up with 10 x LOWEST, which we could carry - // over into the next tier, but won't right now for simplicity). - for (;;) { - let numInputs = 1; - let fees = DashTx._appraiseCounts(numInputs, denomOutputs.length + 1); - let nextCoinCost = DENOM_LOWEST + fees.max; - if (fee < nextCoinCost) { - // TODO split out 10200 (or 10193) collaterals as well - break; + // console.log('[debug] denom outputs', denomOutputs); + + let changes = []; + for (let addr of addresses) { + if (denomOutputs.length === 0) { + break; + } + + let unused = unusedMap[addr]; + if (!unused) { + continue; + } + + unused.reserved = Date.now(); + delete unusedMap[addr]; + + let denomValue = denomOutputs.pop(); + if (!denomValue) { + break; + } + + unused.satoshis = denomValue.satoshis; + changes.push(unused); } - fee -= DashTx.OUTPUT_SIZE; - fee -= DENOM_LOWEST; - denomOutputs.push({ - satoshis: DENOM_LOWEST, - }); - // numOutputs += 1; - // magnitudes[1] += 1; + + let txInfo; + { + let utxosRpc = await rpc.getAddressUtxos({ + addresses: [data.address], + }); + let utxos = utxosRpc.result; + for (let utxo of utxos) { + console.log('[debug] input utxo', utxo); + // utxo.sigHashType = 0x01; + utxo.address = data.address; + if (utxo.txid) { + // TODO fix in dashtx + utxo.txId = utxo.txid; + } + } + for (let change of changes) { + let pubKeyHashBytes = await DashKeys.addrToPkh(change.address, { + version: 'testnet', + }); + change.pubKeyHash = DashKeys.utils.bytesToHex(pubKeyHashBytes); + } + + txInfo = { + version: 3, + inputs: utxos, + outputs: changes, + locktime: 0, + }; + txInfo.inputs.sort(DashTx.sortInputs); + txInfo.outputs.sort(DashTx.sortOutputs); + } + + let total = 0; + for (let input of txInfo.inputs) { + let data = keysMap[input.address]; + total += input.satoshis; + let addressKey = await xreceiveKey.deriveAddress(data.index); + keys.push(addressKey.privateKey); + // DEBUG check pkh hex + let pubKeyHashBytes = await DashKeys.addrToPkh(data.address, { + version: 'testnet', + }); + data.pubKeyHash = DashKeys.utils.bytesToHex(pubKeyHashBytes); + console.log(data); + } + console.log('[DEBUG] total, txInfo', total, txInfo); + let txInfoSigned = await dashTx.hashAndSignAll(txInfo); + + console.log('[debug], txInfo, txSigned'); + console.log(txInfo); + console.log(txInfoSigned); + await sleep(150); + let txRpc = await rpc.sendRawTransaction(txInfoSigned.transaction); + await sleep(150); + console.log('[debug] txRpc.result', txRpc.result); + + // TODO don't add collateral coins + for (let change of changes) { + denomCount += 1; + if (!denominated[change.satoshis]) { + denominated[change.satoshis] = []; + } + denominated[change.satoshis].push(change); + change.reserved = 0; + } + } + } + + async function generateToAddressAndUpdateBalance(data) { + let numBlocks = 1; + await sleep(150); + void (await rpc.generateToAddress(numBlocks, data.address)); + await sleep(150); + // let blocksRpc = await rpc.generateToAddress(numBlocks, addr); + // console.log('[debug] blocksRpc', blocksRpc); + + // let deltas = await rpc.getAddressMempool({ addresses: [addr] }); + // console.log('[debug] generatetoaddress mempool', deltas); + // let deltas2 = await rpc.getAddressDeltas({ addresses: [addr] }); + // console.log('[debug] generatetoaddress deltas', deltas); + // let results = deltas.result.concat(deltas2.result); + // for (let delta of results) { + // totalBalance += delta.satoshis; + // keysMap[delta.address].used = true; + // delete unusedMap[delta.address]; + // } + + let utxosRpc = await rpc.getAddressUtxos({ addresses: [data.address] }); + let utxos = utxosRpc.result; + for (let utxo of utxos) { + // console.log(data.index, '[debug] utxo.satoshis', utxo.satoshis); + data.satoshis += utxo.satoshis; + totalBalance += utxo.satoshis; + keysMap[utxo.address].used = true; + delete unusedMap[utxo.address]; } - // console.log('[debug] denom outputs', denomOutputs); + } - let changes = []; + // TODO unreserve collateral after positive response + // (and check for use 30 seconds after failure message) + async function getCollateralTx() { + let barelyEnoughest = { satoshis: Infinity, reserved: 0 }; for (let addr of addresses) { - if (denomOutputs.length === 0) { - break; + let data = keysMap[addr]; + if (data.reserved > 0) { + continue; } - let unused = unusedMap[addr]; - if (!unused) { + if (!data.satoshis) { continue; } - unused.reserved = Date.now(); - delete unusedMap[addr]; + if (barelyEnoughest.reserved > 0) { + let isDenom = data.satoshis % DENOM_LOWEST === 0; + if (isDenom) { + continue; + } + } - let denomValue = denomOutputs.pop(); - if (!denomValue) { - break; + if (data.satoshis < COLLATERAL) { + continue; } - unused.satoshis = denomValue.satoshis; - changes.push(unused); + if (data.satoshis < barelyEnoughest.satoshis) { + barelyEnoughest = data; + barelyEnoughest.reserved = Date.now(); + } } + console.log('[debug] barelyEnoughest coin:', barelyEnoughest); - let txInfo; + let collateralTxInfo; { - let utxosRpc = await rpc.getAddressUtxos({ addresses: [data.address] }); + let addr = barelyEnoughest.address; + let utxosRpc = await rpc.getAddressUtxos({ addresses: [addr] }); let utxos = utxosRpc.result; for (let utxo of utxos) { console.log('[debug] input utxo', utxo); // utxo.sigHashType = 0x01; - utxo.address = data.address; + utxo.address = addr; if (utxo.txid) { // TODO fix in dashtx utxo.txId = utxo.txid; } } - for (let change of changes) { - let pubKeyHashBytes = await DashKeys.addrToPkh(change.address, { + + let output; + let leftover = barelyEnoughest.satoshis - COLLATERAL; + if (leftover >= COLLATERAL) { + let change = await reserveChangeAddress(); + output = Object.assign({}, change); + // TODO change.used = true; + // change.reserved = 0; + let pubKeyHashBytes = await DashKeys.addrToPkh(output.address, { version: 'testnet', }); - change.pubKeyHash = DashKeys.utils.bytesToHex(pubKeyHashBytes); + output.pubKeyHash = DashKeys.utils.bytesToHex(pubKeyHashBytes); + output.satoshis = leftover; + } else { + output = DashTx.createDonationOutput(); + // TODO 0-byte memo? no outputs (bypassing the normal restriction)? } - txInfo = { + console.log('[debug] change or memo', output); + let txInfo = { version: 3, inputs: utxos, - outputs: changes, + outputs: [output], locktime: 0, }; txInfo.inputs.sort(DashTx.sortInputs); txInfo.outputs.sort(DashTx.sortOutputs); - } - for (let input of txInfo.inputs) { - let data = keysMap[input.address]; - // DEBUG check pkh hex - let pubKeyHashBytes = await DashKeys.addrToPkh(data.address, { - version: 'testnet', - }); - data.pubKeyHash = DashKeys.utils.bytesToHex(pubKeyHashBytes); - console.log(data); + collateralTxInfo = txInfo; } - let txInfoSigned = await dashTx.hashAndSignAll(txInfo); - - console.log('[debug], txInfo, txSigned'); - console.log(txInfo); - console.log(txInfoSigned); - await sleep(150); - let txRpc = await rpc.sendRawTransaction(txInfoSigned.transaction); - await sleep(150); - console.log('[debug] txRpc.result', txRpc.result); - // TODO don't add collateral coins - for (let change of changes) { - denomCount += 1; - if (!denominated[change.satoshis]) { - denominated[change.satoshis] = []; - } - denominated[change.satoshis].push(change); - change.reserved = 0; - } + console.log('[debug] ds* collateral tx', collateralTxInfo); + return collateralTxInfo; } - } - async function generateToAddressAndUpdateBalance(data) { - let numBlocks = 1; - await sleep(150); - void (await rpc.generateToAddress(numBlocks, data.address)); - await sleep(150); - // let blocksRpc = await rpc.generateToAddress(numBlocks, addr); - // console.log('[debug] blocksRpc', blocksRpc); - - // let deltas = await rpc.getAddressMempool({ addresses: [addr] }); - // console.log('[debug] generatetoaddress mempool', deltas); - // let deltas2 = await rpc.getAddressDeltas({ addresses: [addr] }); - // console.log('[debug] generatetoaddress deltas', deltas); - // let results = deltas.result.concat(deltas2.result); - // for (let delta of results) { - // totalBalance += delta.satoshis; - // keysMap[delta.address].used = true; - // delete unusedMap[delta.address]; - // } - - let utxosRpc = await rpc.getAddressUtxos({ addresses: [data.address] }); - let utxos = utxosRpc.result; - for (let utxo of utxos) { - // console.log(data.index, '[debug] utxo.satoshis', utxo.satoshis); - data.satoshis += utxo.satoshis; - totalBalance += utxo.satoshis; - keysMap[utxo.address].used = true; - delete unusedMap[utxo.address]; - } - } - - // TODO unreserve collateral after positive response - // (and check for use 30 seconds after failure message) - async function getCollateralTx() { - let barelyEnoughest = { satoshis: Infinity, reserved: 0 }; - for (let addr of addresses) { - let data = keysMap[addr]; - if (data.reserved > 0) { - continue; - } - - if (!data.satoshis) { - continue; - } + async function reserveChangeAddress() { + for (let addr of addresses) { + let data = keysMap[addr]; - if (barelyEnoughest.reserved > 0) { - let isDenom = data.satoshis % DENOM_LOWEST === 0; - if (isDenom) { + let isAvailable = !data.used && !data.reserved; + if (!isAvailable) { continue; } - } - if (data.satoshis < CoinJoin.COLLATERAL) { - continue; + data.reserved = Date.now(); + return data; } - if (data.satoshis < barelyEnoughest.satoshis) { - barelyEnoughest = data; - barelyEnoughest.reserved = Date.now(); - } + let msg = + 'sanity fail: ran out of addresses despite having 500+ unused extra'; + throw new Error(msg); } - console.log('[debug] barelyEnoughest coin:', barelyEnoughest); - let collateralTxInfo; + // async function getPrivateKeys(inputs) { + // let keys = []; + // for (let input of inputs) { + // let privKeyBytes = await keyUtils.getPrivateKey(input); + // keys.push(privKeyBytes); + // } + + // return keys; + // } + + let evonodes = []; { - let addr = barelyEnoughest.address; - let utxosRpc = await rpc.getAddressUtxos({ addresses: [addr] }); - let utxos = utxosRpc.result; - for (let utxo of utxos) { - console.log('[debug] input utxo', utxo); - // utxo.sigHashType = 0x01; - utxo.address = addr; - if (utxo.txid) { - // TODO fix in dashtx - utxo.txId = utxo.txid; + //let resp = await rpc.masternodelist(); + let res = await fetch('http://127.0.0.1:8080/rpc/masternodelist'); + let resp = await res.json(); + let evonodesMap = resp.result; + let evonodeProTxIds = Object.keys(evonodesMap); + for (let id of evonodeProTxIds) { + let evonode = evonodesMap[id]; + if (evonode.status === 'ENABLED') { + let hostParts = evonode.address.split(':'); + let evodata = { + id: evonode.id, + hostname: hostParts[0], + port: hostParts[1], + type: evonode.type, + }; + evonodes.push(evodata); } } - - let output; - let leftover = barelyEnoughest.satoshis - CoinJoin.COLLATERAL; - if (leftover >= CoinJoin.COLLATERAL) { - let change = await reserveChangeAddress(); - output = Object.assign({}, change); - // TODO change.used = true; - // change.reserved = 0; - let pubKeyHashBytes = await DashKeys.addrToPkh(output.address, { - version: 'testnet', - }); - output.pubKeyHash = DashKeys.utils.bytesToHex(pubKeyHashBytes); - output.satoshis = leftover; - } else { - output = DashTx.createDonationOutput(); - // TODO 0-byte memo? no outputs (bypassing the normal restriction)? + if (!evonodes.length) { + throw new Error('Sanity Fail: no evonodes online'); } + } - console.log('[debug] change or memo', output); - let txInfo = { - version: 3, - inputs: utxos, - outputs: [output], - locktime: 0, - }; - txInfo.inputs.sort(DashTx.sortInputs); - txInfo.outputs.sort(DashTx.sortOutputs); + // void shuffle(evonodes); + evonodes.sort(byId); + let evonode = evonodes.at(-1); + console.info('[info] chosen evonode:'); + console.log(JSON.stringify(evonode, null, 2)); - collateralTxInfo = txInfo; + let query = { + access_token: 'secret', + hostname: evonode.hostname, + port: evonode.port, + }; + let searchParams = new URLSearchParams(query); + let search = searchParams.toString(); + let wsc = new WebSocket(`ws://127.0.0.1:8080/tcp?${search}`); + //let conn = Net.createConnection({ + // host: evonode.hostname, + // port: evonode.port, + // keepAlive: true, + // keepAliveInitialDelay: 3, + // //localAddress: rpc.host, + //}); + + /** @type {Array} */ + let chunks = []; + let chunksLength = 0; + let errReject; + + function onError(err) { + console.error('Error:'); + console.error(err); + // conn.removeListener('error', onError); + wsc.onerror = null; + errReject(err); + } + function onEnd() { + console.info('[info] disconnected from server'); } + // conn.on('error', onError); + // conn.once('end', onEnd); + // conn.setMaxListeners(2); + wsc.onerror = onError; + wsc.onclose = onEnd; + + let dataCount = 0; + // conn.on('data', function (data) { + // console.log('[DEBUG] data'); + // console.log(dataCount, data.length, data.toString('hex')); + // dataCount += 1; + // }); + console.log('[DEBUG] main add wsc.onmessage'); + wsc.addEventListener('message', async function (wsevent) { + console.log('[DEBUG] main wsc.onmessage'); + let ab = await wsevent.data.arrayBuffer(); + let data = Buffer.from(ab); + console.log('[DEBUG] data (main)'); + console.log(dataCount, data.length, data.toString('hex')); + dataCount += 1; + }); - console.log('[debug] ds* collateral tx', collateralTxInfo); - return collateralTxInfo; - } + /** @type {Array} */ + let messages = []; + /** @type {Object} */ + let listenerMap = {}; + async function goRead() { + let pongSize = Packer.HEADER_SIZE + Packer.PING_SIZE; + let pongMessageBytes = new Uint8Array(pongSize); + for (;;) { + console.log('[debug] readMessage()'); + let msg = await readMessage(); + + if (msg.command === 'ping') { + void Packer.packPong({ + network: network, + message: pongMessageBytes, + nonce: msg.payload, + }); + // conn.write(pongMessageBytes); + wsc.send(pongMessageBytes); + console.log('[debug] sent pong'); + continue; + } - async function reserveChangeAddress() { - for (let addr of addresses) { - let data = keysMap[addr]; + if (msg.command === 'dssu') { + let dssu = await Parser.parseDssu(msg.payload); + console.log('[debug] dssu', dssu); + continue; + } - let isAvailable = !data.used && !data.reserved; - if (!isAvailable) { - continue; + let i = messages.length; + messages.push(msg); + let listeners = Object.values(listenerMap); + for (let ln of listeners) { + void ln(msg, i, messages); + } } - - data.reserved = Date.now(); - return data; } + void goRead(); + + /** + * Reads a for a full 24 bytes, parses those bytes as a header, + * and then reads the length of the payload. Any excess bytes will + * be saved for the next cycle - meaning it can handle multiple + * messages in a single packet. + */ + async function readMessage() { + const HEADER_SIZE = 24; + const PAYLOAD_SIZE_MAX = 4 * 1024 * 1024; + + // TODO setTimeout + let _resolve; + let _reject; + let p = new Promise(function (__resolve, __reject) { + _resolve = __resolve; + _reject = __reject; + }); - let msg = - 'sanity fail: ran out of addresses despite having 500+ unused extra'; - throw new Error(msg); - } + let header; - // async function getPrivateKeys(inputs) { - // let keys = []; - // for (let input of inputs) { - // let privKeyBytes = await keyUtils.getPrivateKey(input); - // keys.push(privKeyBytes); - // } + function cleanup() { + console.log('[DEBUG] [readMessage.cleanup] remove data listener'); + wsc.removeEventListener('message', onWsReadableHeader); + wsc.removeEventListener('message', onWsReadablePayload); + // console.log("[debug] readMessage handlers: remove 'onReadableHeader'"); + // conn.removeListener('data', onReadableHeader); + // conn.removeListener('readable', onReadableHeader); + + // console.log("[debug] readMessage handlers: remove 'onReadablePayload'"); + // conn.removeListener('data', onReadablePayload); + // conn.removeListener('readable', onReadablePayload); + } - // return keys; - // } + function resolve(data) { + cleanup(); + _resolve(data); + } - let evonodes = []; - { - //let resp = await rpc.masternodelist(); - let res = await fetch('http://127.0.0.1:8080/rpc/masternodelist'); - let resp = await res.json(); - let evonodesMap = resp.result; - let evonodeProTxIds = Object.keys(evonodesMap); - for (let id of evonodeProTxIds) { - let evonode = evonodesMap[id]; - if (evonode.status === 'ENABLED') { - let hostParts = evonode.address.split(':'); - let evodata = { - id: evonode.id, - hostname: hostParts[0], - port: hostParts[1], - type: evonode.type, - }; - evonodes.push(evodata); + function reject(err) { + cleanup(); + _reject(err); } - } - if (!evonodes.length) { - throw new Error('Sanity Fail: no evonodes online'); - } - } - // void shuffle(evonodes); - evonodes.sort(byId); - let evonode = evonodes.at(-1); - console.info('[info] chosen evonode:'); - console.log(JSON.stringify(evonode, null, 2)); + function onReadableHeader(data) { + let size = data?.length || 0; + console.log('State: reading header', size); + let chunk; + for (;;) { + chunk = data; + // chunk = conn.read(); // TODO reenable + if (!chunk) { + break; + } + chunks.push(chunk); + chunksLength += chunk.byteLength; + data = null; // TODO nix + } + if (chunksLength < HEADER_SIZE) { + return; + } + if (chunks.length > 1) { + chunk = Buffer.concat(chunks, chunksLength); + } else { + chunk = chunks[0]; + } + chunks = []; + chunksLength = 0; + if (chunk.byteLength > HEADER_SIZE) { + let extra = chunk.slice(HEADER_SIZE); + chunks.push(extra); + chunksLength += chunk.byteLength; + chunk = chunk.slice(0, HEADER_SIZE); + } + header = Parser.parseHeader(chunk); + if (header.payloadSize > PAYLOAD_SIZE_MAX) { + console.log(`[DEBUG] header`, header); + throw new Error('too big you are, handle you I cannot'); + } + // console.log('DEBUG header', header); + console.log('[DEBUG] [onReadableHeader] remove data listener'); + // conn.removeListener('readable', onReadableHeader); + // conn.removeListener('data', onReadableHeader); + //wsc.onmessage = null; + wsc.removeEventListener('message', onWsReadableHeader); + + if (header.payloadSize === 0) { + resolve(header); + return; + } - let query = { - access_token: 'secret', - hostname: evonode.hostname, - port: evonode.port, - }; - let searchParams = new URLSearchParams(query); - let search = searchParams.toString(); - let wsc = new WebSocket(`ws://127.0.0.1:8080/tcp?${search}`); - //let conn = Net.createConnection({ - // host: evonode.hostname, - // port: evonode.port, - // keepAlive: true, - // keepAliveInitialDelay: 3, - // //localAddress: rpc.host, - //}); - - /** @type {Array} */ - let chunks = []; - let chunksLength = 0; - let errReject; - - function onError(err) { - console.error('Error:'); - console.error(err); - // conn.removeListener('error', onError); - wsc.onerror = null; - errReject(err); - } - function onEnd() { - console.info('[info] disconnected from server'); - } - // conn.on('error', onError); - // conn.once('end', onEnd); - // conn.setMaxListeners(2); - wsc.onerror = onError; - wsc.onclose = onEnd; - - let dataCount = 0; - // conn.on('data', function (data) { - // console.log('[DEBUG] data'); - // console.log(dataCount, data.length, data.toString('hex')); - // dataCount += 1; - // }); - console.log('[DEBUG] main add wsc.onmessage'); - wsc.addEventListener('message', async function (wsevent) { - console.log('[DEBUG] main wsc.onmessage'); - let ab = await wsevent.data.arrayBuffer(); - let data = Buffer.from(ab); - console.log('[DEBUG] data (main)'); - console.log(dataCount, data.length, data.toString('hex')); - dataCount += 1; - }); - - /** @type {Array} */ - let messages = []; - /** @type {Object} */ - let listenerMap = {}; - async function goRead() { - let pongSize = Packer.HEADER_SIZE + Packer.PING_SIZE; - let pongMessageBytes = new Uint8Array(pongSize); - for (;;) { - console.log('[debug] readMessage()'); - let msg = await readMessage(); - - if (msg.command === 'ping') { - void Packer.packPong({ - network: network, - message: pongMessageBytes, - nonce: msg.payload, - }); - // conn.write(pongMessageBytes); - wsc.send(pongMessageBytes); - console.log('[debug] sent pong'); - continue; + // console.log("[debug] readMessage handlers: add 'onReadablePayload'"); + //conn.on('readable', onReadablePayload); + // conn.on('data', onReadablePayload); + console.log('[DEBUG] onReadableHeader add wsc.onmessage'); + wsc.addEventListener('message', onWsReadablePayload); + onReadablePayload(null); } - - if (msg.command === 'dssu') { - let dssu = await Parser.parseDssu(msg.payload); - console.log('[debug] dssu', dssu); - continue; + async function onWsReadableHeader(wsevent) { + console.log('[DEBUG] onReadableHeader wsc.onmessage'); + let ab = await wsevent.data.arrayBuffer(); + let data = Buffer.from(ab); + console.log('[DEBUG] data (readable header)'); + console.log(dataCount, data.length, data.toString('hex')); + onReadableHeader(data); } - let i = messages.length; - messages.push(msg); - let listeners = Object.values(listenerMap); - for (let ln of listeners) { - void ln(msg, i, messages); + function onReadablePayload(data) { + let size = data?.length || 0; + console.log('State: reading payload', size); + let chunk; + for (;;) { + chunk = data; + // chunk = conn.read(); // TODO revert + if (!chunk) { + break; + } + chunks.push(chunk); + chunksLength += chunk.byteLength; + data = null; // TODO nix + } + if (chunksLength < header.payloadSize) { + return; + } + if (chunks.length > 1) { + chunk = Buffer.concat(chunks, chunksLength); + } else if (chunks.length === 1) { + chunk = chunks[0]; + } else { + console.log("[warn] 'chunk' is 'null' (probably the debug null)"); + return; + } + chunks = []; + chunksLength = 0; + if (chunk.byteLength > header.payloadSize) { + let extra = chunk.slice(header.payloadSize); + chunks.push(extra); + chunksLength += chunk.byteLength; + chunk = chunk.slice(0, header.payloadSize); + } + header.payload = chunk; + console.log('[DEBUG] [onReadablePayload] remove data listener'); + // conn.removeListener('readable', onReadablePayload); + // conn.removeListener('data', onReadablePayload); + // wsc.onmessage = null; + wsc.removeEventListener('message', onWsReadablePayload); + resolve(header); + } + async function onWsReadablePayload(wsevent) { + console.log('[DEBUG] onReadablePayload wsc.onmessage'); + let ab = await wsevent.data.arrayBuffer(); + let data = Buffer.from(ab); + console.log('[DEBUG] data (readable payload)'); + console.log(dataCount, data.length, data.toString('hex')); + onReadablePayload(data); } - } - } - void goRead(); - /** - * Reads a for a full 24 bytes, parses those bytes as a header, - * and then reads the length of the payload. Any excess bytes will - * be saved for the next cycle - meaning it can handle multiple - * messages in a single packet. - */ - async function readMessage() { - const HEADER_SIZE = 24; - const PAYLOAD_SIZE_MAX = 4 * 1024 * 1024; - - // TODO setTimeout - let _resolve; - let _reject; - let p = new Promise(function (__resolve, __reject) { - _resolve = __resolve; - _reject = __reject; - }); + errReject = reject; - let header; + // console.log("[debug] readMessage handlers: add 'onReadableHeader'"); + //conn.on('readable', onReadableHeader); + // conn.on('data', onReadableHeader); + console.log('[DEBUG] readMessage add wsc.onmessage'); + wsc.addEventListener('message', onWsReadableHeader); - function cleanup() { - console.log('[DEBUG] [readMessage.cleanup] remove data listener'); - wsc.removeEventListener('message', onWsReadableHeader); - wsc.removeEventListener('message', onWsReadablePayload); - // console.log("[debug] readMessage handlers: remove 'onReadableHeader'"); - // conn.removeListener('data', onReadableHeader); - // conn.removeListener('readable', onReadableHeader); + if (chunks.length) { + onReadableHeader(null); + } - // console.log("[debug] readMessage handlers: remove 'onReadablePayload'"); - // conn.removeListener('data', onReadablePayload); - // conn.removeListener('readable', onReadablePayload); + let msg = await p; + return msg; } - function resolve(data) { - cleanup(); - _resolve(data); - } + async function waitForConnect() { + // connect / connected + // TODO setTimeout + await new Promise(function (_resolve, _reject) { + function cleanup() { + console.log('[DEBUG] [waitForConnect.cleanup] remove data listener'); + // conn.removeListener('readable', onReadable); + // conn.removeListener('data', onReadable); + // wsc.onmessage = null; + wsc.removeEventListener('message', onWsReadable); + } - function reject(err) { - cleanup(); - _reject(err); - } + function resolve(data) { + cleanup(); + _resolve(data); + } - function onReadableHeader(data) { - let size = data?.length || 0; - console.log('State: reading header', size); - let chunk; - for (;;) { - chunk = data; - // chunk = conn.read(); // TODO reenable - if (!chunk) { - break; + function reject(err) { + cleanup(); + _reject(err); } - chunks.push(chunk); - chunksLength += chunk.byteLength; - data = null; // TODO nix - } - if (chunksLength < HEADER_SIZE) { - return; - } - if (chunks.length > 1) { - chunk = Buffer.concat(chunks, chunksLength); - } else { - chunk = chunks[0]; - } - chunks = []; - chunksLength = 0; - if (chunk.byteLength > HEADER_SIZE) { - let extra = chunk.slice(HEADER_SIZE); - chunks.push(extra); - chunksLength += chunk.byteLength; - chunk = chunk.slice(0, HEADER_SIZE); - } - header = Parser.parseHeader(chunk); - if (header.payloadSize > PAYLOAD_SIZE_MAX) { - throw new Error('too big you are, handle you I cannot'); - } - // console.log('DEBUG header', header); - console.log('[DEBUG] [onReadableHeader] remove data listener'); - // conn.removeListener('readable', onReadableHeader); - // conn.removeListener('data', onReadableHeader); - //wsc.onmessage = null; - wsc.removeEventListener('message', onWsReadableHeader); - - if (header.payloadSize === 0) { - resolve(header); - return; - } - // console.log("[debug] readMessage handlers: add 'onReadablePayload'"); - //conn.on('readable', onReadablePayload); - // conn.on('data', onReadablePayload); - console.log('[DEBUG] onReadableHeader add wsc.onmessage'); - wsc.addEventListener('message', onWsReadablePayload); - onReadablePayload(null); - } - async function onWsReadableHeader(wsevent) { - console.log('[DEBUG] onReadableHeader wsc.onmessage'); - let ab = await wsevent.data.arrayBuffer(); - let data = Buffer.from(ab); - console.log('[DEBUG] data (readable header)'); - console.log(dataCount, data.length, data.toString('hex')); - onReadableHeader(data); - } + function onConnect() { + console.log('[DEBUG] waitForConnect wsc.onopen'); + resolve(); + } - function onReadablePayload(data) { - let size = data?.length || 0; - console.log('State: reading payload', size); - let chunk; - for (;;) { - chunk = data; - // chunk = conn.read(); // TODO revert - if (!chunk) { - break; + function onReadable() { + // checking an impossible condition, just in case + throw new Error('unexpected response before request'); } - chunks.push(chunk); - chunksLength += chunk.byteLength; - data = null; // TODO nix - } - if (chunksLength < header.payloadSize) { - return; - } - if (chunks.length > 1) { - chunk = Buffer.concat(chunks, chunksLength); - } else if (chunks.length === 1) { - chunk = chunks[0]; - } else { - console.log("[warn] 'chunk' is 'null' (probably the debug null)"); - return; - } - chunks = []; - chunksLength = 0; - if (chunk.byteLength > header.payloadSize) { - let extra = chunk.slice(header.payloadSize); - chunks.push(extra); - chunksLength += chunk.byteLength; - chunk = chunk.slice(0, header.payloadSize); - } - header.payload = chunk; - console.log('[DEBUG] [onReadablePayload] remove data listener'); - // conn.removeListener('readable', onReadablePayload); - // conn.removeListener('data', onReadablePayload); - // wsc.onmessage = null; - wsc.removeEventListener('message', onWsReadablePayload); - resolve(header); + async function onWsReadable(wsevent) { + console.log('[DEBUG] waitForConnect wsc.onmessage'); + let ab = await wsevent.data.arrayBuffer(); + let data = Buffer.from(ab); + console.log('[DEBUG] data (readable)'); + console.log(dataCount, data.length, data.toString('hex')); + onReadable(data); + } + + errReject = reject; + // conn.once('connect', onConnect); + wsc.onopen = null; + wsc.onopen = onConnect; + //conn.on('readable', onReadable); + // conn.on('data', onReadable); + console.log('[DEBUG] waitForConnect add wsc.onmessage'); + wsc.addEventListener('message', onWsReadable); + }); } - async function onWsReadablePayload(wsevent) { - console.log('[DEBUG] onReadablePayload wsc.onmessage'); - let ab = await wsevent.data.arrayBuffer(); - let data = Buffer.from(ab); - console.log('[DEBUG] data (readable payload)'); - console.log(dataCount, data.length, data.toString('hex')); - onReadablePayload(data); + + await waitForConnect(); + console.log('connected'); + + // + // version / verack + // + let versionMsg = Packer.version({ + network: network, // Packer.NETWORKS.regtest, + //protocol_version: Packer.PROTOCOL_VERSION, + //addr_recv_services: [Packer.IDENTIFIER_SERVICES.NETWORK], + addr_recv_ip: evonode.hostname, + addr_recv_port: evonode.port, + //addr_trans_services: [], + //addr_trans_ip: '127.0.01', + //addr_trans_port: null, + // addr_trans_ip: conn.localAddress, + // addr_trans_port: conn.localPort, + start_height: height, + //nonce: null, + user_agent: `DashJoin.js/${pkg.version}`, + // optional-ish + relay: false, + mnauth_challenge: null, + mn_connection: false, + }); + + // let versionBuffer = Buffer.from(versionMsg); + // console.log('version', versionBuffer.toString('hex')); + // console.log(Parser.parseHeader(versionBuffer.slice(0, 24))); + // console.log(Parser.parseVerack(versionBuffer.slice(24))); + + { + let versionP = new Promise(function (resolve, reject) { + listenerMap['version'] = async function (message) { + let versionResp = await Parser.parseVersion(message.payload); + console.log('DEBUG version', versionResp.version); + resolve(null); + listenerMap['version'] = null; + delete listenerMap['version']; + }; + }); + await sleep(150); + // conn.write(versionMsg); + wsc.send(versionMsg); + + await versionP; } - errReject = reject; + { + let verackP = await new Promise(function (resolve, reject) { + listenerMap['verack'] = async function (message) { + if (message.command !== 'verack') { + return; + } - // console.log("[debug] readMessage handlers: add 'onReadableHeader'"); - //conn.on('readable', onReadableHeader); - // conn.on('data', onReadableHeader); - console.log('[DEBUG] readMessage add wsc.onmessage'); - wsc.addEventListener('message', onWsReadableHeader); + console.log('DEBUG verack', message); + resolve(); + listenerMap['verack'] = null; + delete listenerMap['verack']; + }; + }); + let verackBytes = await Packer.packAndHashMessage({ + network, + command: 'verack', + payload: null, + }); + await sleep(150); + // conn.write(verackBytes); + wsc.send(verackBytes); - if (chunks.length) { - onReadableHeader(null); + await verackP; } - let msg = await p; - return msg; - } + { + let mnauthP = new Promise(function (resolve, reject) { + listenerMap['mnauth'] = async function (message) { + if (message.command !== 'mnauth') { + return; + } - async function waitForConnect() { - // connect / connected - // TODO setTimeout - await new Promise(function (_resolve, _reject) { - function cleanup() { - console.log('[DEBUG] [waitForConnect.cleanup] remove data listener'); - // conn.removeListener('readable', onReadable); - // conn.removeListener('data', onReadable); - // wsc.onmessage = null; - wsc.removeEventListener('message', onWsReadable); - } + resolve(); + listenerMap['mnauth'] = null; + delete listenerMap['mnauth']; + }; + }); - function resolve(data) { - cleanup(); - _resolve(data); - } + let senddsqP = new Promise(function (resolve, reject) { + listenerMap['senddsq'] = async function (message) { + if (message.command !== 'senddsq') { + return; + } - function reject(err) { - cleanup(); - _reject(err); - } + let sendDsqMessage = Packer.packSendDsq({ + network: network, + send: true, + }); + await sleep(150); + // conn.write(sendDsqMessage); + wsc.send(sendDsqMessage); + console.log("[debug] sending 'senddsq':", sendDsqMessage); + + resolve(null); + listenerMap['senddsq'] = null; + delete listenerMap['senddsq']; + }; + }); - function onConnect() { - console.log('[DEBUG] waitForConnect wsc.onopen'); - resolve(); - } + await mnauthP; + await senddsqP; + } - function onReadable() { - // checking an impossible condition, just in case - throw new Error('unexpected response before request'); - } - async function onWsReadable(wsevent) { - console.log('[DEBUG] waitForConnect wsc.onmessage'); - let ab = await wsevent.data.arrayBuffer(); - let data = Buffer.from(ab); - console.log('[DEBUG] data (readable)'); - console.log(dataCount, data.length, data.toString('hex')); - onReadable(data); + { + let dsqPromise = new Promise(readDsq); + // + // dsa / dssu + dsq + // + //for (let i = 0; i < minimumParticipants; i += 1) + let collateralTx; + { + void (await generateMinBalance()); + void (await generateDenominations()); + + void (await generateMinBalance()); + let collateralTxInfo = await getCollateralTx(); + // let keys = await getPrivateKeys(collateralTxInfo.inputs); + // let txInfoSigned = await dashTx.hashAndSignAll(collateralTxInfo, keys); + let txInfoSigned = await dashTx.hashAndSignAll(collateralTxInfo); + collateralTx = DashTx.utils.hexToBytes(txInfoSigned.transaction); } + let dsaMsg = await Packer.packAllow({ + network, + denomination, + collateralTx, + }); + await sleep(150); + // conn.write(dsaMsg); + wsc.send(dsaMsg); - errReject = reject; - // conn.once('connect', onConnect); - wsc.onopen = null; - wsc.onopen = onConnect; - //conn.on('readable', onReadable); - // conn.on('data', onReadable); - console.log('[DEBUG] waitForConnect add wsc.onmessage'); - wsc.addEventListener('message', onWsReadable); - }); - } - - await waitForConnect(); - console.log('connected'); - - // - // version / verack - // - let versionMsg = Packer.version({ - network: network, // Packer.NETWORKS.regtest, - //protocol_version: Packer.PROTOCOL_VERSION, - //addr_recv_services: [Packer.IDENTIFIER_SERVICES.NETWORK], - addr_recv_ip: evonode.hostname, - addr_recv_port: evonode.port, - //addr_trans_services: [], - //addr_trans_ip: '127.0.01', - //addr_trans_port: null, - // addr_trans_ip: conn.localAddress, - // addr_trans_port: conn.localPort, - start_height: height, - //nonce: null, - user_agent: `DashJoin.js/${pkg.version}`, - // optional-ish - relay: false, - mnauth_challenge: null, - mn_connection: false, - }); - - // let versionBuffer = Buffer.from(versionMsg); - // console.log('version', versionBuffer.toString('hex')); - // console.log(Parser.parseHeader(versionBuffer.slice(0, 24))); - // console.log(Parser.parseVerack(versionBuffer.slice(24))); - - { - let versionP = new Promise(function (resolve, reject) { - listenerMap['version'] = async function (message) { - let versionResp = await Parser.parseVersion(message.payload); - console.log('DEBUG version', versionResp.version); - resolve(null); - listenerMap['version'] = null; - delete listenerMap['version']; - }; - }); - await sleep(150); - // conn.write(versionMsg); - wsc.send(versionMsg); + let dsaBuf = Buffer.from(dsaMsg); + console.log('[debug] dsa', dsaBuf.toString('hex')); - await versionP; - } + let dsq = await dsqPromise; + for (; !dsq.ready; ) { + dsq = await new Promise(readDsq); + if (dsq.ready) { + break; + } + } + } - { - let verackP = await new Promise(function (resolve, reject) { - listenerMap['verack'] = async function (message) { - if (message.command !== 'verack') { + function readDsq(resolve, reject) { + listenerMap['dsq'] = async function (message) { + if (message.command !== 'dsq') { return; } - console.log('DEBUG verack', message); - resolve(); - listenerMap['verack'] = null; - delete listenerMap['verack']; - }; - }); - let verackBytes = Packer.packMessage({ - network, - command: 'verack', - payload: null, - }); - await sleep(150); - // conn.write(verackBytes); - wsc.send(verackBytes); + let dsq = await Parser.parseDsq(message.payload); + console.log('DEBUG dsq', dsq); - await verackP; - } + resolve(dsq); + listenerMap['dsq'] = null; + delete listenerMap['dsq']; + }; + } - { - let mnauthP = new Promise(function (resolve, reject) { - listenerMap['mnauth'] = async function (message) { - if (message.command !== 'mnauth') { + let dsfP = new Promise(function (resolve, reject) { + listenerMap['dsf'] = async function (message) { + if (message.command !== 'dsf') { return; } - resolve(); - listenerMap['mnauth'] = null; - delete listenerMap['mnauth']; + let dsf = Parser.parseDsf(message.payload); + resolve(dsf); + listenerMap['dsf'] = null; + delete listenerMap['dsf']; }; }); - let senddsqP = new Promise(function (resolve, reject) { - listenerMap['senddsq'] = async function (message) { - if (message.command !== 'senddsq') { + let dscP = new Promise(function (resolve, reject) { + listenerMap['dsc'] = async function (message) { + if (message.command !== 'dsc') { return; } - let sendDsqMessage = Packer.packSendDsq({ - network: network, - send: true, - }); - await sleep(150); - // conn.write(sendDsqMessage); - wsc.send(sendDsqMessage); - console.log("[debug] sending 'senddsq':", sendDsqMessage); - - resolve(null); - listenerMap['senddsq'] = null; - delete listenerMap['senddsq']; + console.log('[debug] DSC Status:', message.payload.slice(4)); + // let dsc = Parser.parseDsc(message.payload); + // resolve(dsc); + resolve(); + listenerMap['dsc'] = null; + delete listenerMap['dsc']; }; }); - await mnauthP; - await senddsqP; - } - - { - let dsqPromise = new Promise(readDsq); - // - // dsa / dssu + dsq - // - //for (let i = 0; i < minimumParticipants; i += 1) - let collateralTx; + let inputs = []; + let outputs = []; { - void (await generateMinBalance()); - void (await generateDenominations()); - - void (await generateMinBalance()); - let collateralTxInfo = await getCollateralTx(); - // let keys = await getPrivateKeys(collateralTxInfo.inputs); - // let txInfoSigned = await dashTx.hashAndSignAll(collateralTxInfo, keys); - let txInfoSigned = await dashTx.hashAndSignAll(collateralTxInfo); - collateralTx = DashTx.utils.hexToBytes(txInfoSigned.transaction); - } - let dsaMsg = await Packer.packAllow({ - network, - denomination, - collateralTx, - }); - await sleep(150); - // conn.write(dsaMsg); - wsc.send(dsaMsg); - - let dsaBuf = Buffer.from(dsaMsg); - console.log('[debug] dsa', dsaBuf.toString('hex')); - - let dsq = await dsqPromise; - for (; !dsq.ready; ) { - dsq = await new Promise(readDsq); - if (dsq.ready) { - break; - } - } - } + // build utxo inputs from addrs + for (let addr of addresses) { + if (inputs.length >= COINJOIN_ENTRY_MAX_SIZE) { + break; + } - function readDsq(resolve, reject) { - listenerMap['dsq'] = async function (message) { - if (message.command !== 'dsq') { - return; - } + let data = keysMap[addr]; + // Note: we'd need to look at utxos (not total address balance) + // to be wholly accurate, but this is good enough for now + if (data.satoshis !== denomination) { + continue; + } + if (data.reserved) { + continue; + } - let dsq = await Parser.parseDsq(message.payload); - console.log('DEBUG dsq', dsq); + data.reserved = Date.now(); + let utxosRpc = await rpc.getAddressUtxos({ addresses: [data.address] }); + let utxos = utxosRpc.result; + for (let utxo of utxos) { + // utxo.sigHashType = 0x01; + utxo.address = data.address; + utxo.index = data.index; + // TODO fix in dashtx + utxo.txId = utxo.txId || utxo.txid; + utxo.txid = utxo.txId || utxo.txid; - resolve(dsq); - listenerMap['dsq'] = null; - delete listenerMap['dsq']; - }; - } + // must have pubKeyHash for script to sign + let pubKeyHashBytes = await DashKeys.addrToPkh(data.address, { + version: 'testnet', + }); + utxo.pubKeyHash = DashKeys.utils.bytesToHex(pubKeyHashBytes); - let dsfP = new Promise(function (resolve, reject) { - listenerMap['dsf'] = async function (message) { - if (message.command !== 'dsf') { - return; + console.log('[debug] input utxo', utxo); + inputs.push(utxo); + } } - let dsf = Parser.parseDsf(message.payload); - resolve(dsf); - listenerMap['dsf'] = null; - delete listenerMap['dsf']; - }; - }); - - let dscP = new Promise(function (resolve, reject) { - listenerMap['dsc'] = async function (message) { - if (message.command !== 'dsc') { - return; - } + // build output addrs + for (let addr of addresses) { + if (outputs.length >= inputs.length) { + break; + } - console.log('[debug] DSC Status:', message.payload.slice(4)); - // let dsc = Parser.parseDsc(message.payload); - // resolve(dsc); - resolve(); - listenerMap['dsc'] = null; - delete listenerMap['dsc']; - }; - }); - - let inputs = []; - let outputs = []; - { - // build utxo inputs from addrs - for (let addr of addresses) { - if (inputs.length >= COINJOIN_ENTRY_MAX_SIZE) { - break; - } + let data = keysMap[addr]; - let data = keysMap[addr]; - // Note: we'd need to look at utxos (not total address balance) - // to be wholly accurate, but this is good enough for now - if (data.satoshis !== denomination) { - continue; - } - if (data.reserved) { - continue; - } + let isFree = !data.used && !data.reserved; + if (!isFree) { + continue; + } - data.reserved = Date.now(); - let utxosRpc = await rpc.getAddressUtxos({ addresses: [data.address] }); - let utxos = utxosRpc.result; - for (let utxo of utxos) { - // utxo.sigHashType = 0x01; - utxo.address = data.address; - utxo.index = data.index; - // TODO fix in dashtx - utxo.txId = utxo.txId || utxo.txid; - utxo.txid = utxo.txId || utxo.txid; - - // must have pubKeyHash for script to sign + data.reserved = Date.now(); let pubKeyHashBytes = await DashKeys.addrToPkh(data.address, { version: 'testnet', }); - utxo.pubKeyHash = DashKeys.utils.bytesToHex(pubKeyHashBytes); + let pubKeyHash = DashKeys.utils.bytesToHex(pubKeyHashBytes); - console.log('[debug] input utxo', utxo); - inputs.push(utxo); - } - } + let output = { + pubKeyHash: pubKeyHash, + satoshis: denomination, + }; - // build output addrs - for (let addr of addresses) { - if (outputs.length >= inputs.length) { - break; + outputs.push(output); } + // inputs.sort(DashTx.sortInputs); + // outputs.sort(DashTx.sortOutputs); + } - let data = keysMap[addr]; - - let isFree = !data.used && !data.reserved; - if (!isFree) { - continue; - } + console.log('sanity check 1: inputs', inputs); + let dsf; + { + void (await generateMinBalance()); + let collateralTxInfo = await getCollateralTx(); + // let keys = await getPrivateKeys(collateralTxInfo.inputs); + // let txInfoSigned = await dashTx.hashAndSignAll(collateralTxInfo, keys); + let txInfoSigned = await dashTx.hashAndSignAll(collateralTxInfo); + let collateralTx = DashTx.utils.hexToBytes(txInfoSigned.transaction); - data.reserved = Date.now(); - let pubKeyHashBytes = await DashKeys.addrToPkh(data.address, { - version: 'testnet', + let dsiMessageBytes = Packer.packDsi({ + network, + inputs, + collateralTx, + outputs, }); - let pubKeyHash = DashKeys.utils.bytesToHex(pubKeyHashBytes); - - let output = { - pubKeyHash: pubKeyHash, - satoshis: denomination, - }; - - outputs.push(output); + await sleep(150); + // conn.write(dsiMessageBytes); + wsc.send(dsiMessageBytes); + dsf = await dsfP; } - // inputs.sort(DashTx.sortInputs); - // outputs.sort(DashTx.sortOutputs); - } - console.log('sanity check 1: inputs', inputs); - let dsf; - { - void (await generateMinBalance()); - let collateralTxInfo = await getCollateralTx(); - // let keys = await getPrivateKeys(collateralTxInfo.inputs); - // let txInfoSigned = await dashTx.hashAndSignAll(collateralTxInfo, keys); - let txInfoSigned = await dashTx.hashAndSignAll(collateralTxInfo); - let collateralTx = DashTx.utils.hexToBytes(txInfoSigned.transaction); - - let dsiMessageBytes = Packer.packDsi({ - network, - inputs, - collateralTx, - outputs, - }); - await sleep(150); - // conn.write(dsiMessageBytes); - wsc.send(dsiMessageBytes); - dsf = await dsfP; - } + console.log('sanity check 2: inputs', inputs); + { + let txRequest = dsf.transaction_unsigned; + console.log('[debug] tx request (unsigned)', txRequest); + let sigHashType = DashTx.SIGHASH_ALL | DashTx.SIGHASH_ANYONECANPAY; //jshint ignore:line + // let sigHashType = DashTx.SIGHASH_ALL; + let txInfo = DashTx.parseUnknown(txRequest); + console.log('[debug] DashTx.parseRequest(dsfTxRequest)'); + console.log(txInfo); + for (let input of inputs) { + console.log('sanity check 3: input', input); + let privKeyBytes = await keyUtils.getPrivateKey(input); + let pubKeyBytes = await keyUtils.toPublicKey(privKeyBytes); + let publicKey = DashTx.utils.bytesToHex(pubKeyBytes); + + { + // sanity check + let addr = await DashKeys.pubkeyToAddr(pubKeyBytes, { + version: 'testnet', + }); + if (addr !== input.address) { + console.error(`privKeyBytes => 'addr': ${addr}`); + console.error(`'input.address': ${input.address}`); + throw new Error('sanity fail: address mismatch'); + } + } - console.log('sanity check 2: inputs', inputs); - { - let txRequest = dsf.transaction_unsigned; - console.log('[debug] tx request (unsigned)', txRequest); - let sigHashType = DashTx.SIGHASH_ALL | DashTx.SIGHASH_ANYONECANPAY; //jshint ignore:line - // let sigHashType = DashTx.SIGHASH_ALL; - let txInfo = DashTx.parseUnknown(txRequest); - console.log('[debug] DashTx.parseRequest(dsfTxRequest)'); - console.log(txInfo); - for (let input of inputs) { - console.log('sanity check 3: input', input); - let privKeyBytes = await keyUtils.getPrivateKey(input); - let pubKeyBytes = await keyUtils.toPublicKey(privKeyBytes); - let publicKey = DashTx.utils.bytesToHex(pubKeyBytes); + // let sighashInputs = []; + for (let sighashInput of txInfo.inputs) { + if (sighashInput.txid !== input.txid) { + continue; + } + if (sighashInput.outputIndex !== input.outputIndex) { + continue; + } - { - // sanity check - let addr = await DashKeys.pubkeyToAddr(pubKeyBytes, { - version: 'testnet', - }); - if (addr !== input.address) { - console.error(`privKeyBytes => 'addr': ${addr}`); - console.error(`'input.address': ${input.address}`); - throw new Error('sanity fail: address mismatch'); + sighashInput.index = input.index; + sighashInput.address = input.address; + sighashInput.satoshis = input.satoshis; + sighashInput.pubKeyHash = input.pubKeyHash; + // sighashInput.script = input.script; + sighashInput.publicKey = publicKey; + sighashInput.sigHashType = sigHashType; + console.log('[debug] YES, CAN HAZ INPUTS!!!', sighashInput); + // sighashInputs.push({ + // txId: input.txId || input.txid, + // txid: input.txid || input.txId, + // outputIndex: input.outputIndex, + // pubKeyHash: input.pubKeyHash, + // sigHashType: input.sigHashType, + // }); + break; } + // if (sighashInputs.length !== 1) { + // let msg = + // 'expected exactly one selected input to match one tx request input'; + // throw new Error(msg); + // } + // let anyonecanpayIndex = 0; + // let txHashable = DashTx.createHashable( + // { + // version: txInfo.version, + // inputs: sighashInputs, // exactly 1 + // outputs: txInfo.outputs, + // locktime: txInfo.locktime, + // }, + // anyonecanpayIndex, + // ); + // console.log('[debug] txHashable (pre-sighashbyte)', txHashable); + + // let signableHashBytes = await DashTx.hashPartial(txHashable, sigHashType); + // let signableHashHex = DashTx.utils.bytesToHex(signableHashBytes); + // console.log('[debug] signableHashHex', signableHashHex); + // let sigBuf = await keyUtils.sign(privKeyBytes, signableHashBytes); + // let signature = DashTx.utils.bytesToHex(sigBuf); + // Object.assign(input, { publicKey, sigHashType, signature }); } - // let sighashInputs = []; - for (let sighashInput of txInfo.inputs) { - if (sighashInput.txid !== input.txid) { - continue; - } - if (sighashInput.outputIndex !== input.outputIndex) { + // for (let input of txInfo.inputs) { + // let inputs = Tx.selectSigHashInputs(txInfo, i, _sigHashType); + // let outputs = Tx.selectSigHashOutputs(txInfo, i, _sigHashType); + // let txForSig = Object.assign({}, txInfo, { inputs, outputs }); + // } + // let txSigned = await dashTx.hashAndSignAll(txForSig); + let txSigned = await dashTx.hashAndSignAll(txInfo); + console.log('[debug] txSigned', txSigned); + let signedInputs = []; + for (let input of txSigned.inputs) { + if (!input?.signature) { continue; } - - sighashInput.index = input.index; - sighashInput.address = input.address; - sighashInput.satoshis = input.satoshis; - sighashInput.pubKeyHash = input.pubKeyHash; - // sighashInput.script = input.script; - sighashInput.publicKey = publicKey; - sighashInput.sigHashType = sigHashType; - console.log('[debug] YES, CAN HAZ INPUTS!!!', sighashInput); - // sighashInputs.push({ - // txId: input.txId || input.txid, - // txid: input.txid || input.txId, - // outputIndex: input.outputIndex, - // pubKeyHash: input.pubKeyHash, - // sigHashType: input.sigHashType, - // }); - break; + signedInputs.push(input); } - // if (sighashInputs.length !== 1) { - // let msg = - // 'expected exactly one selected input to match one tx request input'; - // throw new Error(msg); - // } - // let anyonecanpayIndex = 0; - // let txHashable = DashTx.createHashable( - // { - // version: txInfo.version, - // inputs: sighashInputs, // exactly 1 - // outputs: txInfo.outputs, - // locktime: txInfo.locktime, - // }, - // anyonecanpayIndex, - // ); - // console.log('[debug] txHashable (pre-sighashbyte)', txHashable); - - // let signableHashBytes = await DashTx.hashPartial(txHashable, sigHashType); - // let signableHashHex = DashTx.utils.bytesToHex(signableHashBytes); - // console.log('[debug] signableHashHex', signableHashHex); - // let sigBuf = await keyUtils.sign(privKeyBytes, signableHashBytes); - // let signature = DashTx.utils.bytesToHex(sigBuf); - // Object.assign(input, { publicKey, sigHashType, signature }); - } + console.log('[debug] signed inputs', signedInputs); - // for (let input of txInfo.inputs) { - // let inputs = Tx.selectSigHashInputs(txInfo, i, _sigHashType); - // let outputs = Tx.selectSigHashOutputs(txInfo, i, _sigHashType); - // let txForSig = Object.assign({}, txInfo, { inputs, outputs }); - // } - // let txSigned = await dashTx.hashAndSignAll(txForSig); - let txSigned = await dashTx.hashAndSignAll(txInfo); - console.log('[debug] txSigned', txSigned); - let signedInputs = []; - for (let input of txSigned.inputs) { - if (!input?.signature) { - continue; - } - signedInputs.push(input); + let dssMessageBytes = Packer.packDss({ + network: network, + inputs: signedInputs, + }); + console.log('[debug] dss =>', dssMessageBytes.length); + console.log(dssMessageBytes); + let dssHex = DashTx.utils.bytesToHex(dssMessageBytes); + console.log(dssHex); + await sleep(150); + // conn.write(dssMessageBytes); + wsc.send(dssMessageBytes); + await dscP; } - console.log('[debug] signed inputs', signedInputs); - let dssMessageBytes = Packer.packDss({ - network: network, - inputs: signedInputs, - }); - console.log('[debug] dss =>', dssMessageBytes.length); - console.log(dssMessageBytes); - let dssHex = DashTx.utils.bytesToHex(dssMessageBytes); - console.log(dssHex); - await sleep(150); - // conn.write(dssMessageBytes); - wsc.send(dssMessageBytes); - await dscP; + console.log('Sweet, sweet victory!'); } - console.log('Sweet, sweet victory!'); -} - -/** - * @param {Object} a - * @param {String} a.id - * @param {Object} b - * @param {String} b.id - */ -function byId(a, b) { - if (a.id > b.id) { - return 1; + /** + * @param {Object} a + * @param {String} a.id + * @param {Object} b + * @param {String} b.id + */ + function byId(a, b) { + if (a.id > b.id) { + return 1; + } + if (a.id < b.id) { + return -1; + } + return 0; } - if (a.id < b.id) { - return -1; + + // http://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array + // function shuffle(arr) { + // let currentIndex = arr.length; + + // // While there remain elements to shuffle... + // for (; currentIndex !== 0; ) { + // // Pick a remaining element... + // let randomIndexFloat = Math.random() * currentIndex; + // let randomIndex = Math.floor(randomIndexFloat); + // currentIndex -= 1; + + // // And swap it with the current element. + // let temporaryValue = arr[currentIndex]; + // arr[currentIndex] = arr[randomIndex]; + // arr[randomIndex] = temporaryValue; + // } + + // return arr; + // } + + function sleep(ms) { + return new Promise(function (resolve) { + setTimeout(resolve, ms); + }); } - return 0; -} -// http://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array -// function shuffle(arr) { -// let currentIndex = arr.length; - -// // While there remain elements to shuffle... -// for (; currentIndex !== 0; ) { -// // Pick a remaining element... -// let randomIndexFloat = Math.random() * currentIndex; -// let randomIndex = Math.floor(randomIndexFloat); -// currentIndex -= 1; - -// // And swap it with the current element. -// let temporaryValue = arr[currentIndex]; -// arr[currentIndex] = arr[randomIndex]; -// arr[randomIndex] = temporaryValue; -// } - -// return arr; -// } - -function sleep(ms) { - return new Promise(function (resolve) { - setTimeout(resolve, ms); - }); -} + main() + .then(function () { + console.info('Done'); + process.exit(0); + }) + .catch(function (err) { + console.error('Fail:'); + console.error(err.stack || err); + process.exit(1); + }); -main() - .then(function () { - console.info('Done'); - process.exit(0); - }) - .catch(function (err) { - console.error('Fail:'); - console.error(err.stack || err); - process.exit(1); - }); + // @ts-ignore + window.CJDemo = CJDemo; +})(('object' === typeof window && window) || {}, CJDemo); +if ('object' === typeof module) { + module.exports = CJDemo; +} diff --git a/packer.js b/packer.js index 7a9693c..e948031 100644 --- a/packer.js +++ b/packer.js @@ -1,738 +1,865 @@ -'use strict'; - -let Packer = module.exports; - -let Crypto = require('node:crypto'); - -let CoinJoin = require('./coinjoin.js'); - -Packer.PROTOCOL_VERSION = 70227; - -Packer.FIELD_SIZES = { - VERSION: 4, - SERVICES: 8, - TIMESTAMP: 8, - ADDR_RECV_SERVICES: 8, - ADDR_RECV_IP: 16, - ADDR_RECV_PORT: 2, - ADDR_TRANS_SERVICES: 8, - ADDR_TRANS_IP: 16, - ADDR_TRANS_PORT: 2, - NONCE: 8, - USER_AGENT_BYTES: 1, // can be skipped - USER_AGENT_STRING: 0, - START_HEIGHT: 4, - // The following 2 fields are OPTIONAL - RELAY: 0, - RELAY_NONEMPTY: 1, - MNAUTH_CHALLENGE: 0, - MNAUTH_CHALLENGE_NONEMPTY: 32, - MN_CONNECTION: 0, - MN_CONNECTION_NONEMPTY: 1, -}; - -Packer.RELAY_PROTOCOL_VERSION_INTRODUCTION = 70001; -Packer.MNAUTH_PROTOCOL_VERSION_INTRODUCTION = 70214; - -let textEncoder = new TextEncoder(); - -let SIZES = { - MAGIC_BYTES: 4, - COMMAND_NAME: 12, - PAYLOAD_SIZE: 4, - CHECKSUM: 4, -}; -const TOTAL_HEADER_SIZE = - SIZES.MAGIC_BYTES + SIZES.COMMAND_NAME + SIZES.PAYLOAD_SIZE + SIZES.CHECKSUM; -Packer.HEADER_SIZE = TOTAL_HEADER_SIZE; - -Packer.PING_SIZE = Packer.FIELD_SIZES.NONCE; -Packer.DSQ_SIZE = 1; // bool - -const EMPTY_CHECKSUM = [0x5d, 0xf6, 0xe0, 0xe2]; - -/** - * @typedef {"mainnet"|"testnet"|"regtest"|"devnet"} NetworkName - */ - -Packer.NETWORKS = {}; -Packer.NETWORKS.mainnet = { - port: 9999, - magic: new Uint8Array([ - //0xBD6B0CBF, - 0xbf, 0x0c, 0x6b, 0xbd, - ]), - start: 0xbf0c6bbd, - nBits: 0x1e0ffff0, - minimumParticiparts: 3, -}; -Packer.NETWORKS.testnet = { - port: 19999, - magic: new Uint8Array([ - //0xFFCAE2CE, - 0xce, 0xe2, 0xca, 0xff, - ]), - start: 0xcee2caff, - nBits: 0x1e0ffff0, - minimumParticiparts: 2, -}; -Packer.NETWORKS.regtest = { - port: 19899, - magic: new Uint8Array([ - //0xDCB7C1FC, - 0xfc, 0xc1, 0xb7, 0xdc, - ]), - start: 0xfcc1b7dc, - nBits: 0x207fffff, - minimumParticiparts: 2, -}; -Packer.NETWORKS.devnet = { - port: 19799, - magic: new Uint8Array([ - //0xCEFFCAE2, - 0xe2, 0xca, 0xff, 0xce, - ]), - start: 0xe2caffce, - nBits: 0x207fffff, - minimumParticiparts: 2, -}; +//@ts-ignore +var CJPacker = ('object' === typeof module && exports) || {}; +(function (window, CJPacker) { + 'use strict'; + + let Crypto = window.crypto || require('node:crypto'); + let DashTx = window.DashTx || require('dashtx'); + + // TODO the spec seems to be more of an ID, though + // the implementation makes it look more like a mask... + let STANDARD_DENOMINATION_MASKS = { + // 0.00100001 + 100001: 0b00010000, + // 0.01000010 + 1000010: 0b00001000, + // 0.10000100 + 10000100: 0b00000100, + // 1.00001000 + 100001000: 0b00000010, + // 10.00010000 + 1000010000: 0b00000001, + }; -/** - * @typedef {0x01|0x02|0x04|0x400} ServiceBitmask - * @typedef {"NETWORK"|"GETUTXO "|"BLOOM"|"NETWORK_LIMITED"} ServiceName - */ + CJPacker.PROTOCOL_VERSION = 70227; + + CJPacker.FIELD_SIZES = { + VERSION: 4, + SERVICES: 8, + TIMESTAMP: 8, + ADDR_RECV_SERVICES: 8, + ADDR_RECV_IP: 16, + ADDR_RECV_PORT: 2, + ADDR_TRANS_SERVICES: 8, + ADDR_TRANS_IP: 16, + ADDR_TRANS_PORT: 2, + NONCE: 8, + USER_AGENT_BYTES: 1, // can be skipped + USER_AGENT_STRING: 0, + START_HEIGHT: 4, + // The following 2 fields are OPTIONAL + RELAY: 0, + RELAY_NONEMPTY: 1, + MNAUTH_CHALLENGE: 0, + MNAUTH_CHALLENGE_NONEMPTY: 32, + MN_CONNECTION: 0, + MN_CONNECTION_NONEMPTY: 1, + }; -/** @type {Object.} */ -let SERVICE_IDENTIFIERS = {}; + CJPacker.RELAY_PROTOCOL_VERSION_INTRODUCTION = 70001; + CJPacker.MNAUTH_PROTOCOL_VERSION_INTRODUCTION = 70214; -/** - * 0x00 is the default - not a full node, no guarantees - */ + let textEncoder = new TextEncoder(); -/** - * NODE_NETWORK: - * This is a full node and can be asked for full - * blocks. It should implement all protocol features - * available in its self-reported protocol version. - */ -SERVICE_IDENTIFIERS.NETWORK = 0x01; + let SIZES = { + MAGIC_BYTES: 4, + COMMAND_NAME: 12, + PAYLOAD_SIZE: 4, + CHECKSUM: 4, + }; + const TOTAL_HEADER_SIZE = + SIZES.MAGIC_BYTES + + SIZES.COMMAND_NAME + + SIZES.PAYLOAD_SIZE + + SIZES.CHECKSUM; + CJPacker.HEADER_SIZE = TOTAL_HEADER_SIZE; -/** - * NODE_GETUTXO: - * This node is capable of responding to the getutxo - * protocol request. Dash Core does not support - * this service. - */ -SERVICE_IDENTIFIERS.GETUTXO = 0x02; + CJPacker.PING_SIZE = CJPacker.FIELD_SIZES.NONCE; + CJPacker.DSQ_SIZE = 1; // bool -/** - * NODE_BLOOM: - * This node is capable and willing to handle bloom- - * filtered connections. Dash Core nodes used to support - * this by default, without advertising this bit, but - * no longer do as of protocol version 70201 - * (= NO_BLOOM_VERSION) - */ -SERVICE_IDENTIFIERS.BLOOM = 0x04; + const EMPTY_CHECKSUM = [0x5d, 0xf6, 0xe0, 0xe2]; -/** - * 0x08 is not supported by Dash - */ + /** + * @typedef {"mainnet"|"testnet"|"regtest"|"devnet"} NetworkName + */ -/** - * NODE_NETWORK_LIMITED: - * This is the same as NODE_NETWORK with the - * limitation of only serving the last 288 blocks. - * Not supported prior to Dash Core 0.16.0 - */ -SERVICE_IDENTIFIERS.NETWORK_LIMITED = 0x400; + CJPacker.NETWORKS = {}; + CJPacker.NETWORKS.mainnet = { + port: 9999, + magic: new Uint8Array([ + //0xBD6B0CBF, + 0xbf, 0x0c, 0x6b, 0xbd, + ]), + start: 0xbf0c6bbd, + nBits: 0x1e0ffff0, + minimumParticiparts: 3, + }; + CJPacker.NETWORKS.testnet = { + port: 19999, + magic: new Uint8Array([ + //0xFFCAE2CE, + 0xce, 0xe2, 0xca, 0xff, + ]), + start: 0xcee2caff, + nBits: 0x1e0ffff0, + minimumParticiparts: 2, + }; + CJPacker.NETWORKS.regtest = { + port: 19899, + magic: new Uint8Array([ + //0xDCB7C1FC, + 0xfc, 0xc1, 0xb7, 0xdc, + ]), + start: 0xfcc1b7dc, + nBits: 0x207fffff, + minimumParticiparts: 2, + }; + CJPacker.NETWORKS.devnet = { + port: 19799, + magic: new Uint8Array([ + //0xCEFFCAE2, + 0xe2, 0xca, 0xff, 0xce, + ]), + start: 0xe2caffce, + nBits: 0x207fffff, + minimumParticiparts: 2, + }; -/** - * @typedef VersionOpts - * @prop {NetworkName} network - "mainnet", "testnet", etc - * @prop {Uint32?} [protocol_version] - features (default: Packer.PROTOCOL_VERSION) - * @prop {Array?} [addr_recv_services] - default: NETWORK - * @prop {String} addr_recv_ip - ipv6 address (can be 'ipv4-mapped') of the server - * @prop {Uint16} addr_recv_port - 9999, 19999, etc (can be arbitrary on testnet) - * @prop {Array?} [addr_trans_services] - default: NONE - * @prop {String?} [addr_trans_ip]- null, or the external ipv6 or ipv4-mapped address - * @prop {Uint16} [addr_trans_port] - null, or the external port (ignored for tcp?) - * @prop {Uint32} start_height - start height of your best block - * @prop {Uint8Array?} [nonce] - 8 random bytes to identify this transmission - * @prop {String?} [user_agent] - ex: "DashJoin/1.0 request/1.0 node/20.0.0 macos/14.0" - * @prop {Boolean?} [relay] - request all network tx & inv messages to be relayed to you - * @prop {Uint8Array?} [mnauth_challenge] - 32 bytes for the masternode to sign as proof - */ + /** + * @typedef {0x01|0x02|0x04|0x400} ServiceBitmask + * @typedef {"NETWORK"|"GETUTXO "|"BLOOM"|"NETWORK_LIMITED"} ServiceName + */ -/** - * Constructs a version message, with fields in the correct byte order. - * @param {VersionOpts} opts - * - * See also: - * - https://dashcore.readme.io/docs/core-ref-p2p-network-control-messages#version - */ -/* jshint maxcomplexity: 9001 */ -/* jshint maxstatements:150 */ -/* (it's simply very complex, okay?) */ -Packer.version = function ({ - network, - protocol_version = Packer.PROTOCOL_VERSION, - // alias of addr_trans_services - //services, - addr_recv_services = [SERVICE_IDENTIFIERS.NETWORK], - addr_recv_ip, - addr_recv_port, - addr_trans_services = [], - addr_trans_ip = '127.0.0.1', - addr_trans_port = 65535, - start_height, - nonce = null, - user_agent = null, - relay = null, - mnauth_challenge = null, -}) { - const command = 'version'; - - let args = { - network, - protocol_version, - addr_recv_services, - addr_recv_ip, - addr_recv_port, - addr_trans_services, - addr_trans_ip, - addr_trans_port, - start_height, - nonce, - user_agent, - relay, - mnauth_challenge, - }; - let SIZES = Object.assign({}, Packer.FIELD_SIZES); + /** @type {Object.} */ + let SERVICE_IDENTIFIERS = {}; - if (!Packer.NETWORKS[args.network]) { - throw new Error(`"network" '${args.network}' is invalid.`); - } - if (!Array.isArray(args.addr_recv_services)) { - throw new Error('"addr_recv_services" must be an array'); - } - if ( - //@ts-ignore - protocol_version has a default value - args.protocol_version < Packer.RELAY_PROTOCOL_VERSION_INTRODUCTION && - args.relay !== null - ) { - throw new Error( - `"relay" field is not supported in protocol versions prior to ${Packer.RELAY_PROTOCOL_VERSION_INTRODUCTION}`, - ); - } - if ( - //@ts-ignore - protocol_version has a default value - args.protocol_version < Packer.MNAUTH_PROTOCOL_VERSION_INTRODUCTION && - args.mnauth_challenge !== null - ) { - throw new Error( - '"mnauth_challenge" field is not supported in protocol versions prior to MNAUTH_CHALLENGE_OFFSET', - ); - } - if (args.mnauth_challenge !== null) { - if (!(args.mnauth_challenge instanceof Uint8Array)) { - throw new Error('"mnauth_challenge" field must be a Uint8Array'); - } - if ( - args.mnauth_challenge.length !== Packer.SIZES.MNAUTH_CHALLENGE_NONEMPTY - ) { - throw new Error( - `"mnauth_challenge" field must be ${Packer.SIZES.MNAUTH_CHALLENGE_NONEMPTY} bytes long`, - ); - } - } - SIZES.USER_AGENT_STRING = args.user_agent?.length || 0; - if (args.relay !== null) { - SIZES.RELAY = Packer.FIELD_SIZES.RELAY_NONEMPTY; - } - // if (args.mnauth_challenge !== null) { - SIZES.MNAUTH_CHALLENGE = Packer.FIELD_SIZES.MNAUTH_CHALLENGE_NONEMPTY; - // } - SIZES.MN_CONNECTION = Packer.FIELD_SIZES.MN_CONNECTION_NONEMPTY; - - let TOTAL_SIZE = - SIZES.VERSION + - SIZES.SERVICES + - SIZES.TIMESTAMP + - SIZES.ADDR_RECV_SERVICES + - SIZES.ADDR_RECV_IP + - SIZES.ADDR_RECV_PORT + - SIZES.ADDR_TRANS_SERVICES + - SIZES.ADDR_TRANS_IP + - SIZES.ADDR_TRANS_PORT + - SIZES.NONCE + - SIZES.USER_AGENT_BYTES + - SIZES.USER_AGENT_STRING + - SIZES.START_HEIGHT + - SIZES.RELAY + - SIZES.MNAUTH_CHALLENGE + - SIZES.MN_CONNECTION; - let payload = new Uint8Array(TOTAL_SIZE); - // Protocol version - - //@ts-ignore - protocol_version has a default value - let versionBytes = uint32ToBytesLE(args.protocol_version); - payload.set(versionBytes, 0); + /** + * 0x00 is the default - not a full node, no guarantees + */ /** - * Set services to NODE_NETWORK (1) + NODE_BLOOM (4) + * NODE_NETWORK: + * This is a full node and can be asked for full + * blocks. It should implement all protocol features + * available in its self-reported protocol version. */ - const SERVICES_OFFSET = SIZES.VERSION; - let senderServicesBytes; - { - let senderServicesMask = 0n; - //@ts-ignore - addr_trans_services has a default value of [] - for (const serviceBit of addr_trans_services) { - senderServicesMask += BigInt(serviceBit); - } - let senderServices64 = new BigInt64Array([senderServicesMask]); // jshint ignore:line - senderServicesBytes = new Uint8Array(senderServices64.buffer); - payload.set(senderServicesBytes, SERVICES_OFFSET); - } + SERVICE_IDENTIFIERS.NETWORK = 0x01; - const TIMESTAMP_OFFSET = SERVICES_OFFSET + SIZES.SERVICES; - { - let tsBytes = uint32ToBytesLE(Date.now()); - payload.set(tsBytes, TIMESTAMP_OFFSET); - } + /** + * NODE_GETUTXO: + * This node is capable of responding to the getutxo + * protocol request. Dash Core does not support + * this service. + */ + SERVICE_IDENTIFIERS.GETUTXO = 0x02; - let ADDR_RECV_SERVICES_OFFSET = TIMESTAMP_OFFSET + SIZES.TIMESTAMP; - { - let serverServicesMask = 0n; - //@ts-ignore - addr_recv_services has a default value - for (const serviceBit of addr_recv_services) { - serverServicesMask += BigInt(serviceBit); - } - let serverServices64 = new BigInt64Array([serverServicesMask]); // jshint ignore:line - let serverServicesBytes = new Uint8Array(serverServices64.buffer); - payload.set(serverServicesBytes, ADDR_RECV_SERVICES_OFFSET); - } + /** + * NODE_BLOOM: + * This node is capable and willing to handle bloom- + * filtered connections. Dash Core nodes used to support + * this by default, without advertising this bit, but + * no longer do as of protocol version 70201 + * (= NO_BLOOM_VERSION) + */ + SERVICE_IDENTIFIERS.BLOOM = 0x04; /** - * "ADDR_RECV" means the host that we're sending this traffic to. - * So, in other words, it's the master node + * 0x08 is not supported by Dash */ - let ADDR_RECV_IP_OFFSET = - ADDR_RECV_SERVICES_OFFSET + SIZES.ADDR_RECV_SERVICES; - { - let ipBytesBE = ipv4ToBytesBE(args.addr_recv_ip); - payload.set([0xff, 0xff], ADDR_RECV_IP_OFFSET + 10); - payload.set(ipBytesBE, ADDR_RECV_IP_OFFSET + 12); - } /** - * Copy address recv port + * NODE_NETWORK_LIMITED: + * This is the same as NODE_NETWORK with the + * limitation of only serving the last 288 blocks. + * Not supported prior to Dash Core 0.16.0 */ - let ADDR_RECV_PORT_OFFSET = ADDR_RECV_IP_OFFSET + SIZES.ADDR_RECV_IP; - { - let portBytes16 = Uint16Array.from([args.addr_recv_port]); - let portBytes = new Uint8Array(portBytes16.buffer); - portBytes.reverse(); - payload.set(portBytes, ADDR_RECV_PORT_OFFSET); - } + SERVICE_IDENTIFIERS.NETWORK_LIMITED = 0x400; /** - * Copy address transmitted services + * @typedef VersionOpts + * @prop {NetworkName} network - "mainnet", "testnet", etc + * @prop {Uint32?} [protocol_version] - features (default: CJPacker.PROTOCOL_VERSION) + * @prop {Array?} [addr_recv_services] - default: NETWORK + * @prop {String} addr_recv_ip - ipv6 address (can be 'ipv4-mapped') of the server + * @prop {Uint16} addr_recv_port - 9999, 19999, etc (can be arbitrary on testnet) + * @prop {Array?} [addr_trans_services] - default: NONE + * @prop {String?} [addr_trans_ip]- null, or the external ipv6 or ipv4-mapped address + * @prop {Uint16} [addr_trans_port] - null, or the external port (ignored for tcp?) + * @prop {Uint32} start_height - start height of your best block + * @prop {Uint8Array?} [nonce] - 8 random bytes to identify this transmission + * @prop {String?} [user_agent] - ex: "DashJoin/1.0 request/1.0 node/20.0.0 macos/14.0" + * @prop {Boolean?} [relay] - request all network tx & inv messages to be relayed to you + * @prop {Uint8Array?} [mnauth_challenge] - 32 bytes for the masternode to sign as proof */ - let ADDR_TRANS_SERVICES_OFFSET = ADDR_RECV_PORT_OFFSET + SIZES.ADDR_RECV_PORT; - payload.set(senderServicesBytes, ADDR_TRANS_SERVICES_OFFSET); /** - * We add the extra 10, so that we can encode an ipv4-mapped ipv6 address + * Constructs a version message, with fields in the correct byte order. + * @param {VersionOpts} opts + * + * See also: + * - https://dashcore.readme.io/docs/core-ref-p2p-network-control-messages#version */ - let ADDR_TRANS_IP_OFFSET = - ADDR_TRANS_SERVICES_OFFSET + SIZES.ADDR_TRANS_SERVICES; - { - //@ts-ignore - addr_trans_ip has a default value - if (is_ipv6_mapped_ipv4(args.addr_trans_ip)) { - //@ts-ignore - addr_trans_ip has a default value - let ipv6Parts = args.addr_trans_ip.split(':'); - let ipv4Str = ipv6Parts.at(-1); - //@ts-ignore - guaranteed to be defined, actually - let ipBytesBE = ipv4ToBytesBE(ipv4Str); - payload.set(ipBytesBE, ADDR_TRANS_IP_OFFSET + 12); - payload.set([0xff, 0xff], ADDR_TRANS_IP_OFFSET + 10); // we add the 10 so that we can fill the latter 6 bytes - } else { - /** TODO: ipv4-only & ipv6-only */ - //@ts-ignore - addr_trans_ip has a default value - let ipBytesBE = ipv4ToBytesBE(args.addr_trans_ip); - payload.set(ipBytesBE, ADDR_TRANS_IP_OFFSET + 12); - payload.set([0xff, 0xff], ADDR_TRANS_IP_OFFSET + 10); // we add the 10 so that we can fill the latter 6 bytes + /* jshint maxcomplexity: 9001 */ + /* jshint maxstatements:150 */ + /* (it's simply very complex, okay?) */ + CJPacker.version = function ({ + network, + protocol_version = CJPacker.PROTOCOL_VERSION, + // alias of addr_trans_services + //services, + addr_recv_services = [SERVICE_IDENTIFIERS.NETWORK], + addr_recv_ip, + addr_recv_port, + addr_trans_services = [], + addr_trans_ip = '127.0.0.1', + addr_trans_port = 65535, + start_height, + nonce = null, + user_agent = null, + relay = null, + mnauth_challenge = null, + }) { + const command = 'version'; + + let args = { + network, + protocol_version, + addr_recv_services, + addr_recv_ip, + addr_recv_port, + addr_trans_services, + addr_trans_ip, + addr_trans_port, + start_height, + nonce, + user_agent, + relay, + mnauth_challenge, + }; + let SIZES = Object.assign({}, CJPacker.FIELD_SIZES); + + if (!CJPacker.NETWORKS[args.network]) { + throw new Error(`"network" '${args.network}' is invalid.`); } - } + if (!Array.isArray(args.addr_recv_services)) { + throw new Error('"addr_recv_services" must be an array'); + } + if ( + //@ts-ignore - protocol_version has a default value + args.protocol_version < CJPacker.RELAY_PROTOCOL_VERSION_INTRODUCTION && + args.relay !== null + ) { + throw new Error( + `"relay" field is not supported in protocol versions prior to ${CJPacker.RELAY_PROTOCOL_VERSION_INTRODUCTION}`, + ); + } + if ( + //@ts-ignore - protocol_version has a default value + args.protocol_version < CJPacker.MNAUTH_PROTOCOL_VERSION_INTRODUCTION && + args.mnauth_challenge !== null + ) { + throw new Error( + '"mnauth_challenge" field is not supported in protocol versions prior to MNAUTH_CHALLENGE_OFFSET', + ); + } + if (args.mnauth_challenge !== null) { + if (!(args.mnauth_challenge instanceof Uint8Array)) { + throw new Error('"mnauth_challenge" field must be a Uint8Array'); + } + if ( + args.mnauth_challenge.length !== + CJPacker.SIZES.MNAUTH_CHALLENGE_NONEMPTY + ) { + throw new Error( + `"mnauth_challenge" field must be ${CJPacker.SIZES.MNAUTH_CHALLENGE_NONEMPTY} bytes long`, + ); + } + } + SIZES.USER_AGENT_STRING = args.user_agent?.length || 0; + if (args.relay !== null) { + SIZES.RELAY = CJPacker.FIELD_SIZES.RELAY_NONEMPTY; + } + // if (args.mnauth_challenge !== null) { + SIZES.MNAUTH_CHALLENGE = CJPacker.FIELD_SIZES.MNAUTH_CHALLENGE_NONEMPTY; + // } + SIZES.MN_CONNECTION = CJPacker.FIELD_SIZES.MN_CONNECTION_NONEMPTY; + + let TOTAL_SIZE = + SIZES.VERSION + + SIZES.SERVICES + + SIZES.TIMESTAMP + + SIZES.ADDR_RECV_SERVICES + + SIZES.ADDR_RECV_IP + + SIZES.ADDR_RECV_PORT + + SIZES.ADDR_TRANS_SERVICES + + SIZES.ADDR_TRANS_IP + + SIZES.ADDR_TRANS_PORT + + SIZES.NONCE + + SIZES.USER_AGENT_BYTES + + SIZES.USER_AGENT_STRING + + SIZES.START_HEIGHT + + SIZES.RELAY + + SIZES.MNAUTH_CHALLENGE + + SIZES.MN_CONNECTION; + let payload = new Uint8Array(TOTAL_SIZE); + // Protocol version - let ADDR_TRANS_PORT_OFFSET = ADDR_TRANS_IP_OFFSET + SIZES.ADDR_TRANS_IP; - { - let portBytes16 = Uint16Array.from([args.addr_trans_port]); - let portBytes = new Uint8Array(portBytes16.buffer); - portBytes.reverse(); - payload.set(portBytes, ADDR_TRANS_PORT_OFFSET); - } + //@ts-ignore - protocol_version has a default value + let versionBytes = uint32ToBytesLE(args.protocol_version); + payload.set(versionBytes, 0); + + /** + * Set services to NODE_NETWORK (1) + NODE_BLOOM (4) + */ + const SERVICES_OFFSET = SIZES.VERSION; + let senderServicesBytes; + { + let senderServicesMask = 0n; + //@ts-ignore - addr_trans_services has a default value of [] + for (const serviceBit of addr_trans_services) { + senderServicesMask += BigInt(serviceBit); + } + let senderServices64 = new BigInt64Array([senderServicesMask]); // jshint ignore:line + senderServicesBytes = new Uint8Array(senderServices64.buffer); + payload.set(senderServicesBytes, SERVICES_OFFSET); + } - // TODO we should set this to prevent duplicate broadcast - // this can be left zero - let NONCE_OFFSET = ADDR_TRANS_PORT_OFFSET + SIZES.ADDR_TRANS_PORT; - if (!args.nonce) { - args.nonce = new Uint8Array(SIZES.NONCE); - Crypto.getRandomValues(args.nonce); - } - payload.set(args.nonce, NONCE_OFFSET); - - let USER_AGENT_BYTES_OFFSET = NONCE_OFFSET + SIZES.NONCE; - if (null !== args.user_agent && typeof args.user_agent === 'string') { - let userAgentSize = args.user_agent.length; - payload.set([userAgentSize], USER_AGENT_BYTES_OFFSET); - let uaBytes = textEncoder.encode(args.user_agent); - payload.set(uaBytes, USER_AGENT_BYTES_OFFSET + 1); - } else { - payload.set([0x0], USER_AGENT_BYTES_OFFSET); - } + const TIMESTAMP_OFFSET = SERVICES_OFFSET + SIZES.SERVICES; + { + let tsBytes = uint32ToBytesLE(Date.now()); + payload.set(tsBytes, TIMESTAMP_OFFSET); + } - let START_HEIGHT_OFFSET = - USER_AGENT_BYTES_OFFSET + SIZES.USER_AGENT_BYTES + SIZES.USER_AGENT_STRING; - { - let heightBytes = uint32ToBytesLE(args.start_height); - payload.set(heightBytes, START_HEIGHT_OFFSET); - } + let ADDR_RECV_SERVICES_OFFSET = TIMESTAMP_OFFSET + SIZES.TIMESTAMP; + { + let serverServicesMask = 0n; + //@ts-ignore - addr_recv_services has a default value + for (const serviceBit of addr_recv_services) { + serverServicesMask += BigInt(serviceBit); + } + let serverServices64 = new BigInt64Array([serverServicesMask]); // jshint ignore:line + let serverServicesBytes = new Uint8Array(serverServices64.buffer); + payload.set(serverServicesBytes, ADDR_RECV_SERVICES_OFFSET); + } - let RELAY_OFFSET = START_HEIGHT_OFFSET + SIZES.START_HEIGHT; - if (args.relay !== null) { - let bytes = [0x00]; - if (args.relay) { - bytes[0] = 0x01; + /** + * "ADDR_RECV" means the host that we're sending this traffic to. + * So, in other words, it's the master node + */ + let ADDR_RECV_IP_OFFSET = + ADDR_RECV_SERVICES_OFFSET + SIZES.ADDR_RECV_SERVICES; + { + let ipBytesBE = ipv4ToBytesBE(args.addr_recv_ip); + payload.set([0xff, 0xff], ADDR_RECV_IP_OFFSET + 10); + payload.set(ipBytesBE, ADDR_RECV_IP_OFFSET + 12); } - payload.set(bytes, RELAY_OFFSET); - } - let MNAUTH_CHALLENGE_OFFSET = RELAY_OFFSET + SIZES.RELAY; - if (!args.mnauth_challenge) { - let rnd = new Uint8Array(32); - Crypto.getRandomValues(rnd); - args.mnauth_challenge = rnd; - } - payload.set(args.mnauth_challenge, MNAUTH_CHALLENGE_OFFSET); + /** + * Copy address recv port + */ + let ADDR_RECV_PORT_OFFSET = ADDR_RECV_IP_OFFSET + SIZES.ADDR_RECV_IP; + { + let portBytes16 = Uint16Array.from([args.addr_recv_port]); + let portBytes = new Uint8Array(portBytes16.buffer); + portBytes.reverse(); + payload.set(portBytes, ADDR_RECV_PORT_OFFSET); + } - // let MNAUTH_CONNECTION_OFFSET = MNAUTH_CHALLENGE_OFFSET + SIZES.MN_CONNECTION; - // if (args.mn_connection) { - // payload.set([0x01], MNAUTH_CONNECTION_OFFSET); - // } + /** + * Copy address transmitted services + */ + let ADDR_TRANS_SERVICES_OFFSET = + ADDR_RECV_PORT_OFFSET + SIZES.ADDR_RECV_PORT; + payload.set(senderServicesBytes, ADDR_TRANS_SERVICES_OFFSET); + + /** + * We add the extra 10, so that we can encode an ipv4-mapped ipv6 address + */ + let ADDR_TRANS_IP_OFFSET = + ADDR_TRANS_SERVICES_OFFSET + SIZES.ADDR_TRANS_SERVICES; + { + //@ts-ignore - addr_trans_ip has a default value + if (is_ipv6_mapped_ipv4(args.addr_trans_ip)) { + //@ts-ignore - addr_trans_ip has a default value + let ipv6Parts = args.addr_trans_ip.split(':'); + let ipv4Str = ipv6Parts.at(-1); + //@ts-ignore - guaranteed to be defined, actually + let ipBytesBE = ipv4ToBytesBE(ipv4Str); + payload.set(ipBytesBE, ADDR_TRANS_IP_OFFSET + 12); + payload.set([0xff, 0xff], ADDR_TRANS_IP_OFFSET + 10); // we add the 10 so that we can fill the latter 6 bytes + } else { + /** TODO: ipv4-only & ipv6-only */ + //@ts-ignore - addr_trans_ip has a default value + let ipBytesBE = ipv4ToBytesBE(args.addr_trans_ip); + payload.set(ipBytesBE, ADDR_TRANS_IP_OFFSET + 12); + payload.set([0xff, 0xff], ADDR_TRANS_IP_OFFSET + 10); // we add the 10 so that we can fill the latter 6 bytes + } + } - payload = Packer.packMessage({ network, command, payload }); - return payload; -}; + let ADDR_TRANS_PORT_OFFSET = ADDR_TRANS_IP_OFFSET + SIZES.ADDR_TRANS_IP; + { + let portBytes16 = Uint16Array.from([args.addr_trans_port]); + let portBytes = new Uint8Array(portBytes16.buffer); + portBytes.reverse(); + payload.set(portBytes, ADDR_TRANS_PORT_OFFSET); + } -/** - * In this case the only bytes are the nonce - * Use a .subarray(offset) to define an offset. - * (a manual offset will not work consistently, and .byteOffset is context-sensitive) - * @param {Object} opts - * @param {NetworkName} opts.network - "mainnet", "testnet", etc - * @param {Uint8Array?} [opts.message] - * @param {Uint8Array?} [opts.nonce] - */ -Packer.packPing = function ({ network, message = null, nonce = null }) { - const command = 'ping'; + // TODO we should set this to prevent duplicate broadcast + // this can be left zero + let NONCE_OFFSET = ADDR_TRANS_PORT_OFFSET + SIZES.ADDR_TRANS_PORT; + if (!args.nonce) { + args.nonce = new Uint8Array(SIZES.NONCE); + Crypto.getRandomValues(args.nonce); + } + payload.set(args.nonce, NONCE_OFFSET); + + let USER_AGENT_BYTES_OFFSET = NONCE_OFFSET + SIZES.NONCE; + if (null !== args.user_agent && typeof args.user_agent === 'string') { + let userAgentSize = args.user_agent.length; + payload.set([userAgentSize], USER_AGENT_BYTES_OFFSET); + let uaBytes = textEncoder.encode(args.user_agent); + payload.set(uaBytes, USER_AGENT_BYTES_OFFSET + 1); + } else { + payload.set([0x0], USER_AGENT_BYTES_OFFSET); + } - if (!message) { - let pingSize = Packer.HEADER_SIZE + Packer.PING_SIZE; - message = new Uint8Array(pingSize); - } - let payload = message.subarray(Packer.HEADER_SIZE); + let START_HEIGHT_OFFSET = + USER_AGENT_BYTES_OFFSET + + SIZES.USER_AGENT_BYTES + + SIZES.USER_AGENT_STRING; + { + let heightBytes = uint32ToBytesLE(args.start_height); + payload.set(heightBytes, START_HEIGHT_OFFSET); + } - if (!nonce) { - nonce = payload; - Crypto.getRandomValues(nonce); - } else { - payload.set(nonce, 0); - } + let RELAY_OFFSET = START_HEIGHT_OFFSET + SIZES.START_HEIGHT; + if (args.relay !== null) { + let bytes = [0x00]; + if (args.relay) { + bytes[0] = 0x01; + } + payload.set(bytes, RELAY_OFFSET); + } - void Packer.packMessage({ network, command, bytes: message }); - return message; -}; + let MNAUTH_CHALLENGE_OFFSET = RELAY_OFFSET + SIZES.RELAY; + if (!args.mnauth_challenge) { + let rnd = new Uint8Array(32); + Crypto.getRandomValues(rnd); + args.mnauth_challenge = rnd; + } + payload.set(args.mnauth_challenge, MNAUTH_CHALLENGE_OFFSET); -/** - * In this case the only bytes are the nonce - * Use a .subarray(offset) to define an offset. - * (a manual offset will not work consistently, and .byteOffset is context-sensitive) - * @param {Object} opts - * @param {NetworkName} opts.network - "mainnet", "testnet", etc - * @param {Uint8Array?} [opts.message] - * @param {Uint8Array} opts.nonce - */ -Packer.packPong = function ({ network, message = null, nonce }) { - const command = 'pong'; + // let MNAUTH_CONNECTION_OFFSET = MNAUTH_CHALLENGE_OFFSET + SIZES.MN_CONNECTION; + // if (args.mn_connection) { + // payload.set([0x01], MNAUTH_CONNECTION_OFFSET); + // } - if (!message) { - let pongSize = Packer.HEADER_SIZE + Packer.PING_SIZE; - message = new Uint8Array(pongSize); - } - let payload = message.subarray(Packer.HEADER_SIZE); - payload.set(nonce, 0); + payload = CJPacker.packMessage({ network, command, payload }); + return payload; + }; - void Packer.packMessage({ network, command, bytes: message }); - return message; -}; + /** + * In this case the only bytes are the nonce + * Use a .subarray(offset) to define an offset. + * (a manual offset will not work consistently, and .byteOffset is context-sensitive) + * @param {Object} opts + * @param {NetworkName} opts.network - "mainnet", "testnet", etc + * @param {Uint8Array?} [opts.message] + * @param {Uint8Array?} [opts.nonce] + */ + CJPacker.packPing = function ({ network, message = null, nonce = null }) { + const command = 'ping'; -/** - * Turns on or off DSQ messages (necessary for CoinJoin, but off by default) - * @param {Object} opts - * @param {NetworkName} opts.network - "mainnet", "testnet", etc - * @param {Uint8Array?} [opts.message] - * @param {Boolean?} [opts.send] - */ -Packer.packSendDsq = function ({ network, message = null, send = true }) { - const command = 'senddsq'; + if (!message) { + let pingSize = CJPacker.HEADER_SIZE + CJPacker.PING_SIZE; + message = new Uint8Array(pingSize); + } + let payload = message.subarray(CJPacker.HEADER_SIZE); - if (!message) { - let dsqSize = Packer.HEADER_SIZE + Packer.DSQ_SIZE; - message = new Uint8Array(dsqSize); - } + if (!nonce) { + nonce = payload; + Crypto.getRandomValues(nonce); + } else { + payload.set(nonce, 0); + } - let sendByte = [0x01]; - if (!send) { - sendByte = [0x00]; - } - let payload = message.subarray(Packer.HEADER_SIZE); - payload.set(sendByte, 0); + void CJPacker.packMessage({ network, command, bytes: message }); + return message; + }; - void Packer.packMessage({ network, command, bytes: message }); + /** + * In this case the only bytes are the nonce + * Use a .subarray(offset) to define an offset. + * (a manual offset will not work consistently, and .byteOffset is context-sensitive) + * @param {Object} opts + * @param {NetworkName} opts.network - "mainnet", "testnet", etc + * @param {Uint8Array?} [opts.message] + * @param {Uint8Array} opts.nonce + */ + CJPacker.packPong = function ({ network, message = null, nonce }) { + const command = 'pong'; - return message; -}; + if (!message) { + let pongSize = CJPacker.HEADER_SIZE + CJPacker.PING_SIZE; + message = new Uint8Array(pongSize); + } + let payload = message.subarray(CJPacker.HEADER_SIZE); + payload.set(nonce, 0); -/** - * @param {Object} opts - * @param {NetworkName} opts.network - "mainnet", "testnet", etc - * @param {Uint32} opts.denomination - * @param {Uint8Array} opts.collateralTx - */ -Packer.packAllow = function ({ network, denomination, collateralTx }) { - const command = 'dsa'; - const DENOMINATION_SIZE = 4; - - //@ts-ignore - numbers can be used as map keys - let denomMask = CoinJoin.STANDARD_DENOMINATION_MASKS[denomination]; - if (!denomMask) { - throw new Error( - `contact your local Dash representative to vote for denominations of '${denomination}'`, - ); - } + void CJPacker.packMessage({ network, command, bytes: message }); + return message; + }; - let totalLength = DENOMINATION_SIZE + collateralTx.length; - let payload = new Uint8Array(totalLength); - let dv = new DataView(payload.buffer); - let offset = 0; + /** + * Turns on or off DSQ messages (necessary for CoinJoin, but off by default) + * @param {Object} opts + * @param {NetworkName} opts.network - "mainnet", "testnet", etc + * @param {Uint8Array?} [opts.message] + * @param {Boolean?} [opts.send] + */ + CJPacker.packSendDsq = function ({ network, message = null, send = true }) { + const command = 'senddsq'; - let DV_LITTLE_ENDIAN = true; - dv.setUint32(offset, denomMask, DV_LITTLE_ENDIAN); - offset += DENOMINATION_SIZE; + if (!message) { + let dsqSize = CJPacker.HEADER_SIZE + CJPacker.DSQ_SIZE; + message = new Uint8Array(dsqSize); + } - payload.set(collateralTx, offset); + let sendByte = [0x01]; + if (!send) { + sendByte = [0x00]; + } + let payload = message.subarray(CJPacker.HEADER_SIZE); + payload.set(sendByte, 0); - let message = Packer.packMessage({ network, command, payload }); - return message; -}; + void CJPacker.packMessage({ network, command, bytes: message }); -let DashTx = require('dashtx'); + return message; + }; -/** - * @param {Object} opts - * @param {NetworkName} opts.network - "mainnet", "testnet", etc - * @param {Array} opts.inputs - * @param {Array} opts.outputs - * @param {Uint8Array} opts.collateralTx - */ -Packer.packDsi = function ({ network, inputs, collateralTx, outputs }) { - const command = 'dsi'; - - let neutered = []; - for (let input of inputs) { - let _input = { - txId: input.txId || input.txid, - txid: input.txid || input.txId, - outputIndex: input.outputIndex, - }; - neutered.push(_input); - } + /** + * @param {Object} opts + * @param {NetworkName} opts.network - "mainnet", "testnet", etc + * @param {Uint32} opts.denomination + * @param {Uint8Array} opts.collateralTx + */ + CJPacker.packAllow = function ({ network, denomination, collateralTx }) { + const command = 'dsa'; + const DENOMINATION_SIZE = 4; - let inputsHex = DashTx.serializeInputs(inputs); - let inputHex = inputsHex.join(''); - let outputsHex = DashTx.serializeOutputs(outputs); - let outputHex = outputsHex.join(''); + //@ts-ignore - numbers can be used as map keys + let denomMask = CoinJoin.STANDARD_DENOMINATION_MASKS[denomination]; + if (!denomMask) { + throw new Error( + `contact your local Dash representative to vote for denominations of '${denomination}'`, + ); + } - let len = collateralTx.length; - len += inputHex.length / 2; - len += outputHex.length / 2; - let bytes = new Uint8Array(Packer.HEADER_SIZE + len); + let totalLength = DENOMINATION_SIZE + collateralTx.length; + let payload = new Uint8Array(totalLength); + let dv = new DataView(payload.buffer); + let offset = 0; - let offset = Packer.HEADER_SIZE; + let DV_LITTLE_ENDIAN = true; + dv.setUint32(offset, denomMask, DV_LITTLE_ENDIAN); + offset += DENOMINATION_SIZE; - { - let inputsPayload = bytes.subarray(offset); - let j = 0; - for (let i = 0; i < inputHex.length; i += 2) { - let end = i + 2; - let hex = inputHex.slice(i, end); - inputsPayload[j] = parseInt(hex, 16); - j += 1; + payload.set(collateralTx, offset); + + let message = CJPacker.packMessage({ network, command, payload }); + return message; + }; + + /** + * @param {Object} opts + * @param {NetworkName} opts.network - "mainnet", "testnet", etc + * @param {Array} opts.inputs + * @param {Array} opts.outputs + * @param {Uint8Array} opts.collateralTx + */ + CJPacker.packDsi = function ({ network, inputs, collateralTx, outputs }) { + const command = 'dsi'; + + let neutered = []; + for (let input of inputs) { + let _input = { + txId: input.txId || input.txid, + txid: input.txid || input.txId, + outputIndex: input.outputIndex, + }; + neutered.push(_input); } - offset += inputHex.length / 2; - } - bytes.set(collateralTx, offset); - offset += collateralTx.length; + let inputsHex = DashTx.serializeInputs(inputs); + let inputHex = inputsHex.join(''); + let outputsHex = DashTx.serializeOutputs(outputs); + let outputHex = outputsHex.join(''); + + let len = collateralTx.length; + len += inputHex.length / 2; + len += outputHex.length / 2; + let bytes = new Uint8Array(CJPacker.HEADER_SIZE + len); + + let offset = CJPacker.HEADER_SIZE; + + { + let inputsPayload = bytes.subarray(offset); + let j = 0; + for (let i = 0; i < inputHex.length; i += 2) { + let end = i + 2; + let hex = inputHex.slice(i, end); + inputsPayload[j] = parseInt(hex, 16); + j += 1; + } + offset += inputHex.length / 2; + } - { - let outputsPayload = bytes.subarray(offset); - let j = 0; - for (let i = 0; i < outputHex.length; i += 2) { - let end = i + 2; - let hex = outputHex.slice(i, end); - outputsPayload[j] = parseInt(hex, 16); - j += 1; + bytes.set(collateralTx, offset); + offset += collateralTx.length; + + { + let outputsPayload = bytes.subarray(offset); + let j = 0; + for (let i = 0; i < outputHex.length; i += 2) { + let end = i + 2; + let hex = outputHex.slice(i, end); + outputsPayload[j] = parseInt(hex, 16); + j += 1; + } + offset += outputHex.length / 2; } - offset += outputHex.length / 2; - } - void Packer.packMessage({ network, command, bytes }); - return bytes; -}; + void CJPacker.packMessage({ network, command, bytes }); + return bytes; + }; -/** - * @param {Object} opts - * @param {NetworkName} opts.network - "mainnet", "testnet", etc - * @param {Array} [opts.inputs] - */ -Packer.packDss = function ({ network, inputs }) { - const command = 'dss'; + /** + * @param {Object} opts + * @param {NetworkName} opts.network - "mainnet", "testnet", etc + * @param {Array} [opts.inputs] + */ + CJPacker.packDss = function ({ network, inputs }) { + const command = 'dss'; - if (!inputs?.length) { - // TODO make better - throw new Error('you must provide some inputs'); - } + if (!inputs?.length) { + // TODO make better + throw new Error('you must provide some inputs'); + } - let txInputsHex = DashTx.serializeInputs(inputs); - let txInputHex = txInputsHex.join(''); - let payload = DashTx.utils.hexToBytes(txInputHex); + let txInputsHex = DashTx.serializeInputs(inputs); + let txInputHex = txInputsHex.join(''); + let payload = DashTx.utils.hexToBytes(txInputHex); - // TODO prealloc bytes - let bytes = Packer.packMessage({ network, command, payload }); - return bytes; -}; + // TODO prealloc bytes + let bytes = CJPacker.packMessage({ network, command, payload }); + return bytes; + }; -/** - * @param {Object} opts - * @param {NetworkName} opts.network - "mainnet", "testnet", etc - * @param {String} opts.command - * @param {Uint8Array?} [opts.bytes] - * @param {Uint8Array?} [opts.payload] - */ -Packer.packMessage = function ({ - network, - command, - bytes = null, - payload = null, -}) { - let payloadLength = payload?.byteLength || 0; - let messageSize = Packer.HEADER_SIZE + payloadLength; - let offset = 0; - - let embeddedPayload = false; - let message = bytes; - if (message) { + /** + * @param {Object} opts + * @param {NetworkName} opts.network - "mainnet", "testnet", etc + * @param {String} opts.command + * @param {Uint8Array?} [opts.bytes] + * @param {Uint8Array?} [opts.payload] + */ + CJPacker.packMessage = function ({ + network, + command, + bytes = null, + payload = null, + }) { + let payloadLength = payload?.byteLength || 0; + let messageSize = CJPacker.HEADER_SIZE + payloadLength; + let offset = 0; + + let embeddedPayload = false; + let message = bytes; + if (message) { + if (!payload) { + payload = message.subarray(CJPacker.HEADER_SIZE); + payloadLength = payload.byteLength; + messageSize = CJPacker.HEADER_SIZE + payloadLength; + embeddedPayload = true; + } + } else { + message = new Uint8Array(messageSize); + } + if (message.length !== messageSize) { + throw new Error( + `expected bytes of length ${messageSize}, but got ${message.length}`, + ); + } + message.set(CJPacker.NETWORKS[network].magic, offset); + offset += SIZES.MAGIC_BYTES; + + // Set command_name (char[12]) + let nameBytes = textEncoder.encode(command); + message.set(nameBytes, offset); + offset += SIZES.COMMAND_NAME; + + // Finally, append the payload to the header if (!payload) { - payload = message.subarray(Packer.HEADER_SIZE); - payloadLength = payload.byteLength; - messageSize = Packer.HEADER_SIZE + payloadLength; - embeddedPayload = true; + // skip because it's already initialized to 0 + //message.set(payloadLength, offset); + offset += SIZES.PAYLOAD_SIZE; + + message.set(EMPTY_CHECKSUM, offset); + return message; } - } else { - message = new Uint8Array(messageSize); - } - if (message.length !== messageSize) { - throw new Error( - `expected bytes of length ${messageSize}, but got ${message.length}`, - ); - } - message.set(Packer.NETWORKS[network].magic, offset); - offset += SIZES.MAGIC_BYTES; - - // Set command_name (char[12]) - let nameBytes = textEncoder.encode(command); - message.set(nameBytes, offset); - offset += SIZES.COMMAND_NAME; - - // Finally, append the payload to the header - if (!payload) { - // skip because it's already initialized to 0 - //message.set(payloadLength, offset); + + let payloadSizeBytes = uint32ToBytesLE(payloadLength); + message.set(payloadSizeBytes, offset); offset += SIZES.PAYLOAD_SIZE; - message.set(EMPTY_CHECKSUM, offset); + let checksum = CJPacker.checksum(payload); + message.set(checksum, offset); + offset += SIZES.CHECKSUM; + + if (!embeddedPayload) { + message.set(payload, offset); + } return message; - } + }; - let payloadSizeBytes = uint32ToBytesLE(payloadLength); - message.set(payloadSizeBytes, offset); - offset += SIZES.PAYLOAD_SIZE; + /** + * First 4 bytes of SHA256(SHA256(payload)) in internal byte order. + * @param {Uint8Array} payload + */ + CJPacker.checksum = function (payload) { + // TODO this should be node-specific in node for performance reasons + if (Crypto.createHash) { + let hash = Crypto.createHash('sha256').update(payload).digest(); + let hashOfHash = Crypto.createHash('sha256').update(hash).digest(); + return hashOfHash.slice(0, 4); + } - let checksum = compute_checksum(payload); - message.set(checksum, offset); - offset += SIZES.CHECKSUM; + let hash = sha256(payload); + let hashOfHash = sha256(hash); + return hashOfHash.slice(0, 4); + }; - if (!embeddedPayload) { - message.set(payload, offset); + /** + * @param {Uint8Array} bytes + */ + function sha256(bytes) { + let K = new Uint32Array([ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, + 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, + 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, + 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, + 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, + 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, + ]); + + /** + * @param {Number} value + * @param {Number} amount + */ + function rightRotate(value, amount) { + return (value >>> amount) | (value << (32 - amount)); + } + + let H = new Uint32Array([ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, + 0x1f83d9ab, 0x5be0cd19, + ]); + + let padded = new Uint8Array((bytes.length + 9 + 63) & ~63); + padded.set(bytes); + padded[bytes.length] = 0x80; + let dv = new DataView(padded.buffer); + dv.setUint32(padded.length - 4, bytes.length << 3, false); + + let w = new Uint32Array(64); + for (let i = 0; i < padded.length; i += 64) { + for (let j = 0; j < 16; j += 1) { + w[j] = + (padded[i + 4 * j] << 24) | + (padded[i + 4 * j + 1] << 16) | + (padded[i + 4 * j + 2] << 8) | + padded[i + 4 * j + 3]; + } + for (let j = 16; j < 64; j += 1) { + let w1 = w[j - 15]; + let w2 = w[j - 2]; + let s0 = rightRotate(w1, 7) ^ rightRotate(w1, 18) ^ (w1 >>> 3); + let s1 = rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2 >>> 10); + w[j] = w[j - 16] + s0 + w[j - 7] + s1; + } + + let [a, b, c, d, e, f, g, h] = H; + for (let j = 0; j < 64; j += 1) { + let S1 = rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25); + let ch = (e & f) ^ (~e & g); + let temp1 = h + S1 + ch + K[j] + w[j]; + let S0 = rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22); + let maj = (a & b) ^ (a & c) ^ (b & c); + let temp2 = S0 + maj; + + h = g; + g = f; + f = e; + e = d + temp1; + d = c; + c = b; + b = a; + a = temp1 + temp2; + } + + H[0] += a; + H[1] += b; + H[2] += c; + H[3] += d; + H[4] += e; + H[5] += f; + H[6] += g; + H[7] += h; + } + + let numBytes = H.length * 4; + let hash = new Uint8Array(numBytes); + for (let i = 0; i < H.length; i += 1) { + hash[i * 4] = (H[i] >>> 24) & 0xff; + hash[i * 4 + 1] = (H[i] >>> 16) & 0xff; + hash[i * 4 + 2] = (H[i] >>> 8) & 0xff; + hash[i * 4 + 3] = H[i] & 0xff; + } + return hash; } - return message; -}; -/** - * First 4 bytes of SHA256(SHA256(payload)) in internal byte order. - * @param {Uint8Array} payload - */ -function compute_checksum(payload) { - // TODO this should be node-specific in node for performance reasons - let hash = Crypto.createHash('sha256').update(payload).digest(); - let hashOfHash = Crypto.createHash('sha256').update(hash).digest(); - return hashOfHash.slice(0, 4); -} + /** + * @param {String} ipv4 + */ + function ipv4ToBytesBE(ipv4) { + let u8s = []; + // let u8s = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff /*,0,0,0,0*/]; + + let octets = ipv4.split('.'); + for (let octet of octets) { + let int8 = parseInt(octet); + u8s.push(int8); + } -/** - * @param {String} ipv4 - */ -function ipv4ToBytesBE(ipv4) { - let u8s = []; - // let u8s = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff /*,0,0,0,0*/]; - - let octets = ipv4.split('.'); - for (let octet of octets) { - let int8 = parseInt(octet); - u8s.push(int8); + let bytes = Uint8Array.from(u8s); + return bytes; } - let bytes = Uint8Array.from(u8s); - return bytes; -} + /** + * @param {Uint32} n + */ + function uint32ToBytesLE(n) { + let u32 = new Uint32Array([n]); + let u8 = new Uint8Array(u32.buffer); + return u8; + } -/** - * @param {Uint32} n - */ -function uint32ToBytesLE(n) { - let u32 = new Uint32Array([n]); - let u8 = new Uint8Array(u32.buffer); - return u8; -} + /** + * @param {String} ip + */ + function is_ipv6_mapped_ipv4(ip) { + return !!ip.match(/^[:]{2}[f]{4}[:]{1}.*$/); + } -/** - * @param {String} ip - */ -function is_ipv6_mapped_ipv4(ip) { - return !!ip.match(/^[:]{2}[f]{4}[:]{1}.*$/); + // @ts-ignore + window.CJPacker = CJPacker; +})(('object' === typeof window && window) || {}, CJPacker); +if ('object' === typeof module) { + module.exports = CJPacker; } /** diff --git a/parser.js b/parser.js index f2d7179..db315c2 100644 --- a/parser.js +++ b/parser.js @@ -1,397 +1,422 @@ -'use strict'; - -let Parser = module.exports; - -const DV_LITTLE_ENDIAN = true; -// const DV_BIG_ENDIAN = false; -//let EMPTY_HASH = Buffer.from('5df6e0e2', 'hex'); - -Parser.HEADER_SIZE = 24; -Parser.DSSU_SIZE = 16; -Parser.DSQ_SIZE = 142; -Parser.SESSION_ID_SIZE = 4; - -let CoinJoin = require('./coinjoin.js'); -let DashTx = require('dashtx'); - -/** - * Parse the 24-byte P2P Message Header - * - 4 byte magic bytes (delimiter) (possibly intended for non-tcp messages?) - * - 12 byte string (stop at first null) - * - 4 byte payload size - * - 4 byte checksum - * - * See also: - * - https://docs.dash.org/projects/core/en/stable/docs/reference/p2p-network-message-headers.html#message-headers - * @param {Uint8Array} bytes - */ -Parser.parseHeader = function (bytes) { - let buffer = Buffer.from(bytes); - // console.log( - // new Date(), - // '[debug] parseHeader(bytes)', - // buffer.length, - // buffer.toString('hex'), - // ); - // console.log(buffer.toString('utf8')); - - bytes = new Uint8Array(buffer); - if (bytes.length < Parser.HEADER_SIZE) { - let msg = `developer error: header should be ${Parser.HEADER_SIZE}+ bytes (optional payload), not ${bytes.length}`; - throw new Error(msg); - } - let dv = new DataView(bytes.buffer); - - let commandStart = 4; - let payloadSizeStart = 16; - let checksumStart = 20; - - let magicBytes = buffer.slice(0, commandStart); - - let commandEnd = buffer.indexOf(0x00, commandStart); - if (commandEnd >= payloadSizeStart) { - throw new Error('command name longer than 12 bytes'); - } - let commandBuf = buffer.slice(commandStart, commandEnd); - let command = commandBuf.toString('utf8'); - - let payloadSize = dv.getUint32(payloadSizeStart, DV_LITTLE_ENDIAN); - let checksum = buffer.slice(checksumStart, checksumStart + 4); - - let headerMessage = { - magicBytes, - command, - payloadSize, - checksum, +//@ts-ignore +var CJParser = ('object' === typeof module && exports) || {}; +(function (window, CJParser) { + 'use strict'; + + let DashTx = window.DashTx || require('dashtx'); + + let STANDARD_DENOMINATIONS_MAP = { + // 0.00100001 + 0b00010000: 100001, + // 0.01000010 + 0b00001000: 1000010, + // 0.10000100 + 0b00000100: 10000100, + // 1.00001000 + 0b00000010: 100001000, + // 10.00010000 + 0b00000001: 1000010000, }; - // if (command !== 'inv') { - // console.log(new Date(), headerMessage); - // } - // console.log(); - return headerMessage; -}; - -/** - * @param {Uint8Array} bytes - */ -Parser.parseVersion = function (bytes) { - let buffer = Buffer.from(bytes); - // console.log( - // '[debug] parseVersion(bytes)', - // buffer.length, - // buffer.toString('hex'), - // ); - // console.log(buffer.toString('utf8')); - - bytes = new Uint8Array(buffer); - let dv = new DataView(bytes.buffer); - - let versionStart = 0; - let version = dv.getUint32(versionStart, DV_LITTLE_ENDIAN); - - let servicesStart = versionStart + 4; // + SIZES.VERSION (4) - let servicesMask = dv.getBigUint64(servicesStart, DV_LITTLE_ENDIAN); - - let timestampStart = servicesStart + 8; // + SIZES.SERVICES (8) - let timestamp64n = dv.getBigInt64(timestampStart, DV_LITTLE_ENDIAN); - let timestamp64 = Number(timestamp64n); - let timestampMs = timestamp64 * 1000; - let timestamp = new Date(timestampMs); - - let addrRecvServicesStart = timestampStart + 8; // + SIZES.TIMESTAMP (8) - let addrRecvServicesMask = dv.getBigUint64( - addrRecvServicesStart, - DV_LITTLE_ENDIAN, - ); - - let addrRecvAddressStart = addrRecvServicesStart + 8; // + SIZES.SERVICES (8) - let addrRecvAddress = buffer.slice( - addrRecvAddressStart, - addrRecvAddressStart + 16, - ); - - let addrRecvPortStart = addrRecvAddressStart + 16; // + SIZES.IPV6 (16) - let addrRecvPort = dv.getUint16(addrRecvPortStart, DV_LITTLE_ENDIAN); - - let addrTransServicesStart = addrRecvPortStart + 2; // + SIZES.PORT (2) - let addrTransServicesMask = dv.getBigUint64( - addrTransServicesStart, - DV_LITTLE_ENDIAN, - ); - - let addrTransAddressStart = addrTransServicesStart + 8; // + SIZES.SERVICES (8) - let addrTransAddress = buffer.slice( - addrTransAddressStart, - addrTransAddressStart + 16, - ); - - let addrTransPortStart = addrTransAddressStart + 16; // + SIZES.IPV6 (16) - let addrTransPort = dv.getUint16(addrTransPortStart, DV_LITTLE_ENDIAN); - - let nonceStart = addrTransPortStart + 2; // + SIZES.PORT (2) - let nonce = buffer.slice(nonceStart, nonceStart + 8); - - let uaSizeStart = 80; // + SIZES.PORT (2) - let uaSize = buffer[uaSizeStart]; - - let uaStart = uaSizeStart + 1; - let uaBytes = buffer.slice(uaStart, uaStart + uaSize); - let ua = uaBytes.toString('utf8'); - - let startHeightStart = uaStart + uaSize; - let startHeight = dv.getUint32(startHeightStart, DV_LITTLE_ENDIAN); - - let relayStart = startHeightStart + 4; - /** @type {Boolean?} */ - let relay = null; - if (buffer.length > relayStart) { - relay = buffer[relayStart] > 0; - } - - let mnAuthChStart = relayStart + 1; - /** @type {Uint8Array?} */ - let mnAuthChallenge = null; - if (buffer.length > mnAuthChStart) { - mnAuthChallenge = buffer.slice(mnAuthChStart, mnAuthChStart + 32); - } - - let mnConnStart = mnAuthChStart + 32; - /** @type {Boolean?} */ - let mnConn = null; - if (buffer.length > mnConnStart) { - mnConn = buffer[mnConnStart] > 0; - } - - let versionMessage = { - version, - servicesMask, - timestamp, - addrRecvServicesMask, - addrRecvAddress, - addrRecvPort, - addrTransServicesMask, - addrTransAddress, - addrTransPort, - nonce, - ua, - startHeight, - relay, - mnAuthChallenge, - mnConn, - }; + const DV_LITTLE_ENDIAN = true; + // const DV_BIG_ENDIAN = false; + //let EMPTY_HASH = Buffer.from('5df6e0e2', 'hex'); - // console.log(versionMessage); - // console.log(); - return versionMessage; -}; - -Parser._DSSU_MESSAGE_IDS = { - 0x00: 'ERR_ALREADY_HAVE', - 0x01: 'ERR_DENOM', - 0x02: 'ERR_ENTRIES_FULL', - 0x03: 'ERR_EXISTING_TX', - 0x04: 'ERR_FEES', - 0x05: 'ERR_INVALID_COLLATERAL', - 0x06: 'ERR_INVALID_INPUT', - 0x07: 'ERR_INVALID_SCRIPT', - 0x08: 'ERR_INVALID_TX', - 0x09: 'ERR_MAXIMUM', - 0x0a: 'ERR_MN_LIST', // <-- - 0x0b: 'ERR_MODE', - 0x0c: 'ERR_NON_STANDARD_PUBKEY', // (Not used) - 0x0d: 'ERR_NOT_A_MN', //(Not used) - 0x0e: 'ERR_QUEUE_FULL', - 0x0f: 'ERR_RECENT', - 0x10: 'ERR_SESSION', - 0x11: 'ERR_MISSING_TX', - 0x12: 'ERR_VERSION', - 0x13: 'MSG_NOERR', - 0x14: 'MSG_SUCCESS', - 0x15: 'MSG_ENTRIES_ADDED', - 0x16: 'ERR_SIZE_MISMATCH', -}; - -Parser._DSSU_STATES = { - 0x00: 'IDLE', - 0x01: 'QUEUE', - 0x02: 'ACCEPTING_ENTRIES', - 0x03: 'SIGNING', - 0x04: 'ERROR', - 0x05: 'SUCCESS', -}; - -Parser._DSSU_STATUSES = { - 0x00: 'REJECTED', - 0x01: 'ACCEPTED', -}; - -/** - * @param {Uint8Array} bytes - */ -Parser.parseDssu = function (bytes) { - let buffer = Buffer.from(bytes); - - bytes = new Uint8Array(buffer); - let dv = new DataView(bytes.buffer); - // console.log('[debug] parseDssu(bytes)', bytes.length, buffer.toString('hex')); - // console.log(buffer.toString('utf8')); - if (bytes.length !== Parser.DSSU_SIZE) { - let msg = `developer error: a 'dssu' message is 16 bytes, but got ${bytes.length}`; - throw new Error(msg); - } + CJParser.HEADER_SIZE = 24; + CJParser.DSSU_SIZE = 16; + CJParser.DSQ_SIZE = 142; + CJParser.SESSION_ID_SIZE = 4; /** - * 4 nMsgSessionID - Required - Session ID - * 4 nMsgState - Required - Current state of processing - * 4 nMsgEntriesCount - Required - Number of entries in the pool (deprecated) - * 4 nMsgStatusUpdate - Required - Update state and/or signal if entry was accepted or not - * 4 nMsgMessageID - Required - ID of the typical masternode reply message + * Parse the 24-byte P2P Message Header + * - 4 byte magic bytes (delimiter) (possibly intended for non-tcp messages?) + * - 12 byte string (stop at first null) + * - 4 byte payload size + * - 4 byte checksum + * + * See also: + * - https://docs.dash.org/projects/core/en/stable/docs/reference/p2p-network-message-headers.html#message-headers + * @param {Uint8Array} bytes */ - const SIZES = { - SESSION_ID: Parser.SESSION_ID_SIZE, - STATE: 4, - ENTRIES_COUNT: 4, - STATUS_UPDATE: 4, - MESSAGE_ID: 4, + CJParser.parseHeader = function (bytes) { + let buffer = Buffer.from(bytes); + // console.log( + // new Date(), + // '[debug] parseHeader(bytes)', + // buffer.length, + // buffer.toString('hex'), + // ); + // console.log(buffer.toString('utf8')); + + bytes = new Uint8Array(buffer); + if (bytes.length < CJParser.HEADER_SIZE) { + console.log( + `[DEBUG] malformed header`, + buffer.toString('utf8'), + buffer.toString('hex'), + ); + let msg = `developer error: header should be ${CJParser.HEADER_SIZE}+ bytes (optional payload), not ${bytes.length}`; + throw new Error(msg); + } + let dv = new DataView(bytes.buffer); + + let commandStart = 4; + let payloadSizeStart = 16; + let checksumStart = 20; + + let magicBytes = buffer.slice(0, commandStart); + + let commandEnd = buffer.indexOf(0x00, commandStart); + if (commandEnd >= payloadSizeStart) { + throw new Error('command name longer than 12 bytes'); + } + let commandBuf = buffer.slice(commandStart, commandEnd); + let command = commandBuf.toString('utf8'); + + let payloadSize = dv.getUint32(payloadSizeStart, DV_LITTLE_ENDIAN); + let checksum = buffer.slice(checksumStart, checksumStart + 4); + + let headerMessage = { + magicBytes, + command, + payloadSize, + checksum, + }; + + // if (command !== 'inv') { + // console.log(new Date(), headerMessage); + // } + // console.log(); + return headerMessage; }; - let offset = 0; - - let session_id = dv.getUint32(offset, DV_LITTLE_ENDIAN); - offset += SIZES.SESSION_ID; - - let state_id = dv.getUint32(offset, DV_LITTLE_ENDIAN); - offset += SIZES.STATE; - - ///** - // * Grab the entries count - // * Not parsed because apparently master nodes no longer send - // * the entries count. - // */ - //parsed.entries_count = dv.getUint32(offset, DV_LITTLE_ENDIAN); - //offset += SIZES.ENTRIES_COUNT; - - let status_id = dv.getUint32(offset, DV_LITTLE_ENDIAN); - offset += SIZES.STATUS_UPDATE; - - let message_id = dv.getUint32(offset, DV_LITTLE_ENDIAN); - - let dssuMessage = { - session_id: session_id, - state_id: state_id, - state: Parser._DSSU_STATES[state_id], - // entries_count: 0, - status_id: status_id, - status: Parser._DSSU_STATUSES[status_id], - message_id: message_id, - message: Parser._DSSU_MESSAGE_IDS[message_id], + /** + * @param {Uint8Array} bytes + */ + CJParser.parseVersion = function (bytes) { + let buffer = Buffer.from(bytes); + // console.log( + // '[debug] parseVersion(bytes)', + // buffer.length, + // buffer.toString('hex'), + // ); + // console.log(buffer.toString('utf8')); + + bytes = new Uint8Array(buffer); + let dv = new DataView(bytes.buffer); + + let versionStart = 0; + let version = dv.getUint32(versionStart, DV_LITTLE_ENDIAN); + + let servicesStart = versionStart + 4; // + SIZES.VERSION (4) + let servicesMask = dv.getBigUint64(servicesStart, DV_LITTLE_ENDIAN); + + let timestampStart = servicesStart + 8; // + SIZES.SERVICES (8) + let timestamp64n = dv.getBigInt64(timestampStart, DV_LITTLE_ENDIAN); + let timestamp64 = Number(timestamp64n); + let timestampMs = timestamp64 * 1000; + let timestamp = new Date(timestampMs); + + let addrRecvServicesStart = timestampStart + 8; // + SIZES.TIMESTAMP (8) + let addrRecvServicesMask = dv.getBigUint64( + addrRecvServicesStart, + DV_LITTLE_ENDIAN, + ); + + let addrRecvAddressStart = addrRecvServicesStart + 8; // + SIZES.SERVICES (8) + let addrRecvAddress = buffer.slice( + addrRecvAddressStart, + addrRecvAddressStart + 16, + ); + + let addrRecvPortStart = addrRecvAddressStart + 16; // + SIZES.IPV6 (16) + let addrRecvPort = dv.getUint16(addrRecvPortStart, DV_LITTLE_ENDIAN); + + let addrTransServicesStart = addrRecvPortStart + 2; // + SIZES.PORT (2) + let addrTransServicesMask = dv.getBigUint64( + addrTransServicesStart, + DV_LITTLE_ENDIAN, + ); + + let addrTransAddressStart = addrTransServicesStart + 8; // + SIZES.SERVICES (8) + let addrTransAddress = buffer.slice( + addrTransAddressStart, + addrTransAddressStart + 16, + ); + + let addrTransPortStart = addrTransAddressStart + 16; // + SIZES.IPV6 (16) + let addrTransPort = dv.getUint16(addrTransPortStart, DV_LITTLE_ENDIAN); + + let nonceStart = addrTransPortStart + 2; // + SIZES.PORT (2) + let nonce = buffer.slice(nonceStart, nonceStart + 8); + + let uaSizeStart = 80; // + SIZES.PORT (2) + let uaSize = buffer[uaSizeStart]; + + let uaStart = uaSizeStart + 1; + let uaBytes = buffer.slice(uaStart, uaStart + uaSize); + let ua = uaBytes.toString('utf8'); + + let startHeightStart = uaStart + uaSize; + let startHeight = dv.getUint32(startHeightStart, DV_LITTLE_ENDIAN); + + let relayStart = startHeightStart + 4; + /** @type {Boolean?} */ + let relay = null; + if (buffer.length > relayStart) { + relay = buffer[relayStart] > 0; + } + + let mnAuthChStart = relayStart + 1; + /** @type {Uint8Array?} */ + let mnAuthChallenge = null; + if (buffer.length > mnAuthChStart) { + mnAuthChallenge = buffer.slice(mnAuthChStart, mnAuthChStart + 32); + } + + let mnConnStart = mnAuthChStart + 32; + /** @type {Boolean?} */ + let mnConn = null; + if (buffer.length > mnConnStart) { + mnConn = buffer[mnConnStart] > 0; + } + + let versionMessage = { + version, + servicesMask, + timestamp, + addrRecvServicesMask, + addrRecvAddress, + addrRecvPort, + addrTransServicesMask, + addrTransAddress, + addrTransPort, + nonce, + ua, + startHeight, + relay, + mnAuthChallenge, + mnConn, + }; + + // console.log(versionMessage); + // console.log(); + return versionMessage; }; - // console.log(dssuMessage); - // console.log(); - return dssuMessage; -}; - -/** - * @param {Uint8Array} bytes - */ -Parser.parseDsq = function (bytes) { - let buffer = Buffer.from(bytes); - - bytes = new Uint8Array(buffer); - if (bytes.length !== Parser.DSQ_SIZE) { - let msg = `developer error: 'dsq' messages are ${Parser.DSQ_SIZE} bytes, not ${bytes.length}`; - throw new Error(msg); - } - let dv = new DataView(bytes.buffer); - // console.log('[debug] parseDsq(bytes)', bytes.length, buffer.toString('hex')); - // console.log(buffer.toString('utf8')); - - const SIZES = { - DENOM: 4, - PROTX: 32, - TIME: 8, - READY: 1, - SIG: 97, + CJParser._DSSU_MESSAGE_IDS = { + 0x00: 'ERR_ALREADY_HAVE', + 0x01: 'ERR_DENOM', + 0x02: 'ERR_ENTRIES_FULL', + 0x03: 'ERR_EXISTING_TX', + 0x04: 'ERR_FEES', + 0x05: 'ERR_INVALID_COLLATERAL', + 0x06: 'ERR_INVALID_INPUT', + 0x07: 'ERR_INVALID_SCRIPT', + 0x08: 'ERR_INVALID_TX', + 0x09: 'ERR_MAXIMUM', + 0x0a: 'ERR_MN_LIST', // <-- + 0x0b: 'ERR_MODE', + 0x0c: 'ERR_NON_STANDARD_PUBKEY', // (Not used) + 0x0d: 'ERR_NOT_A_MN', //(Not used) + 0x0e: 'ERR_QUEUE_FULL', + 0x0f: 'ERR_RECENT', + 0x10: 'ERR_SESSION', + 0x11: 'ERR_MISSING_TX', + 0x12: 'ERR_VERSION', + 0x13: 'MSG_NOERR', + 0x14: 'MSG_SUCCESS', + 0x15: 'MSG_ENTRIES_ADDED', + 0x16: 'ERR_SIZE_MISMATCH', }; - let offset = 0; - - let denomination_id = dv.getUint32(offset, DV_LITTLE_ENDIAN); - offset += SIZES.DENOM; + CJParser._DSSU_STATES = { + 0x00: 'IDLE', + 0x01: 'QUEUE', + 0x02: 'ACCEPTING_ENTRIES', + 0x03: 'SIGNING', + 0x04: 'ERROR', + 0x05: 'SUCCESS', + }; - //@ts-ignore - correctness of denomination must be checked higher up - let denomination = CoinJoin.STANDARD_DENOMINATIONS_MAP[denomination_id]; + CJParser._DSSU_STATUSES = { + 0x00: 'REJECTED', + 0x01: 'ACCEPTED', + }; /** - * Grab the protxhash + * @param {Uint8Array} bytes */ - let protxhash_bytes = bytes.slice(offset, offset + SIZES.PROTX); - offset += SIZES.PROTX; + CJParser.parseDssu = function (bytes) { + let buffer = Buffer.from(bytes); + + bytes = new Uint8Array(buffer); + let dv = new DataView(bytes.buffer); + // console.log('[debug] parseDssu(bytes)', bytes.length, buffer.toString('hex')); + // console.log(buffer.toString('utf8')); + if (bytes.length !== CJParser.DSSU_SIZE) { + let msg = `developer error: a 'dssu' message is 16 bytes, but got ${bytes.length}`; + throw new Error(msg); + } + + /** + * 4 nMsgSessionID - Required - Session ID + * 4 nMsgState - Required - Current state of processing + * 4 nMsgEntriesCount - Required - Number of entries in the pool (deprecated) + * 4 nMsgStatusUpdate - Required - Update state and/or signal if entry was accepted or not + * 4 nMsgMessageID - Required - ID of the typical masternode reply message + */ + const SIZES = { + SESSION_ID: CJParser.SESSION_ID_SIZE, + STATE: 4, + ENTRIES_COUNT: 4, + STATUS_UPDATE: 4, + MESSAGE_ID: 4, + }; + + let offset = 0; + + let session_id = dv.getUint32(offset, DV_LITTLE_ENDIAN); + offset += SIZES.SESSION_ID; + + let state_id = dv.getUint32(offset, DV_LITTLE_ENDIAN); + offset += SIZES.STATE; + + ///** + // * Grab the entries count + // * Not parsed because apparently master nodes no longer send + // * the entries count. + // */ + //parsed.entries_count = dv.getUint32(offset, DV_LITTLE_ENDIAN); + //offset += SIZES.ENTRIES_COUNT; + + let status_id = dv.getUint32(offset, DV_LITTLE_ENDIAN); + offset += SIZES.STATUS_UPDATE; + + let message_id = dv.getUint32(offset, DV_LITTLE_ENDIAN); + + let dssuMessage = { + session_id: session_id, + state_id: state_id, + state: CJParser._DSSU_STATES[state_id], + // entries_count: 0, + status_id: status_id, + status: CJParser._DSSU_STATUSES[status_id], + message_id: message_id, + message: CJParser._DSSU_MESSAGE_IDS[message_id], + }; + + // console.log(dssuMessage); + // console.log(); + return dssuMessage; + }; /** - * Grab the time + * @param {Uint8Array} bytes */ - let timestamp64n = dv.getBigInt64(offset, DV_LITTLE_ENDIAN); - offset += SIZES.TIME; - let timestamp_unix = Number(timestamp64n); - let timestampMs = timestamp_unix * 1000; - let timestampDate = new Date(timestampMs); - let timestamp = timestampDate.toISOString(); + CJParser.parseDsq = function (bytes) { + let buffer = Buffer.from(bytes); + + bytes = new Uint8Array(buffer); + if (bytes.length !== CJParser.DSQ_SIZE) { + let msg = `developer error: 'dsq' messages are ${CJParser.DSQ_SIZE} bytes, not ${bytes.length}`; + throw new Error(msg); + } + let dv = new DataView(bytes.buffer); + // console.log('[debug] parseDsq(bytes)', bytes.length, buffer.toString('hex')); + // console.log(buffer.toString('utf8')); + + const SIZES = { + DENOM: 4, + PROTX: 32, + TIME: 8, + READY: 1, + SIG: 97, + }; + + let offset = 0; + + let denomination_id = dv.getUint32(offset, DV_LITTLE_ENDIAN); + offset += SIZES.DENOM; + + //@ts-ignore - correctness of denomination must be checked higher up + let denomination = STANDARD_DENOMINATIONS_MAP[denomination_id]; + + /** + * Grab the protxhash + */ + let protxhash_bytes = bytes.slice(offset, offset + SIZES.PROTX); + offset += SIZES.PROTX; + + /** + * Grab the time + */ + let timestamp64n = dv.getBigInt64(offset, DV_LITTLE_ENDIAN); + offset += SIZES.TIME; + let timestamp_unix = Number(timestamp64n); + let timestampMs = timestamp_unix * 1000; + let timestampDate = new Date(timestampMs); + let timestamp = timestampDate.toISOString(); + + /** + * Grab the fReady + */ + let ready = bytes[offset] > 0x00; + offset += SIZES.READY; + + let signature_bytes = bytes.slice(offset, offset + SIZES.SIG); + + let dsqMessage = { + denomination_id, + denomination, + protxhash_bytes, + // protxhash: '', + timestamp_unix, + timestamp, + ready, + signature_bytes, + // signature: '', + }; + + // console.log(dsqMessage); + // console.log(); + return dsqMessage; + }; /** - * Grab the fReady + * @param {Uint8Array} bytes */ - let ready = bytes[offset] > 0x00; - offset += SIZES.READY; - - let signature_bytes = bytes.slice(offset, offset + SIZES.SIG); - - let dsqMessage = { - denomination_id, - denomination, - protxhash_bytes, - // protxhash: '', - timestamp_unix, - timestamp, - ready, - signature_bytes, - // signature: '', + CJParser.parseDsf = function (bytes) { + // console.log( + // new Date(), + // '[debug] parseDsf (msg len)', + // bytes.length, + // bytes.toString('hex'), + // ); + + let offset = 0; + let sessionId = bytes.subarray(offset, CJParser.SESSION_ID_SIZE); + let session_id = DashTx.utils.bytesToHex(sessionId); + offset += CJParser.SESSION_ID_SIZE; + + // TODO parse transaction completely with DashTx + let transactionUnsigned = bytes.subarray(offset); + let transaction_unsigned = DashTx.utils.bytesToHex(transactionUnsigned); + + // let txLen = transaction_unsigned.length / 2; + // console.log( + // new Date(), + // '[debug] parseDsf (tx len)', + // txLen, + // transaction_unsigned, + // ); + + return { session_id, transaction_unsigned }; }; - // console.log(dsqMessage); - // console.log(); - return dsqMessage; -}; - -/** - * @param {Uint8Array} bytes - */ -Parser.parseDsf = function (bytes) { - // console.log( - // new Date(), - // '[debug] parseDsf (msg len)', - // bytes.length, - // bytes.toString('hex'), - // ); - - let offset = 0; - let sessionId = bytes.subarray(offset, Parser.SESSION_ID_SIZE); - let session_id = DashTx.utils.bytesToHex(sessionId); - offset += Parser.SESSION_ID_SIZE; - - // TODO parse transaction completely with DashTx - let transactionUnsigned = bytes.subarray(offset); - let transaction_unsigned = DashTx.utils.bytesToHex(transactionUnsigned); - - // let txLen = transaction_unsigned.length / 2; - // console.log( - // new Date(), - // '[debug] parseDsf (tx len)', - // txLen, - // transaction_unsigned, - // ); - - return { session_id, transaction_unsigned }; -}; + // @ts-ignore + window.CJParser = CJParser; +})(('object' === typeof window && window) || {}, CJParser); +if ('object' === typeof module) { + module.exports = CJParser; +} From 14052aa437ed61878f2c0b1aa2ddd54de5cff6cd Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 12 Jul 2024 19:48:14 -0600 Subject: [PATCH 07/55] f: browser --- demo.js | 7 +++++-- packer.js | 2 +- parser.js | 17 ++++++++--------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/demo.js b/demo.js index befb93b..a6af5bd 100644 --- a/demo.js +++ b/demo.js @@ -10,7 +10,7 @@ var CJDemo = ('object' === typeof module && exports) || {}; } //@ts-ignore - ts can't understand JSON, still... - let pkg = window.ENVS.package || require('./package.json'); + let pkg = DotEnv.package || require('./package.json'); let Packer = require('./packer.js'); // TODO rename packer let Parser = require('./parser.js'); @@ -247,7 +247,9 @@ var CJDemo = ('object' === typeof module && exports) || {}; let denomination = 100001 * 1; + console.log('[debug] generate min balance...'); void (await generateMinBalance()); + console.log('[debug] generate denoms...'); void (await generateDenominations()); // TODO sort denominated @@ -606,6 +608,7 @@ var CJDemo = ('object' === typeof module && exports) || {}; // return keys; // } + console.log('[debug] get evonode list...'); let evonodes = []; { //let resp = await rpc.masternodelist(); @@ -1013,7 +1016,7 @@ var CJDemo = ('object' === typeof module && exports) || {}; delete listenerMap['verack']; }; }); - let verackBytes = await Packer.packAndHashMessage({ + let verackBytes = Packer.packMessage({ network, command: 'verack', payload: null, diff --git a/packer.js b/packer.js index e948031..6b6b923 100644 --- a/packer.js +++ b/packer.js @@ -538,7 +538,7 @@ var CJPacker = ('object' === typeof module && exports) || {}; const DENOMINATION_SIZE = 4; //@ts-ignore - numbers can be used as map keys - let denomMask = CoinJoin.STANDARD_DENOMINATION_MASKS[denomination]; + let denomMask = STANDARD_DENOMINATION_MASKS[denomination]; if (!denomMask) { throw new Error( `contact your local Dash representative to vote for denominations of '${denomination}'`, diff --git a/parser.js b/parser.js index db315c2..581d8c3 100644 --- a/parser.js +++ b/parser.js @@ -39,7 +39,7 @@ var CJParser = ('object' === typeof module && exports) || {}; * @param {Uint8Array} bytes */ CJParser.parseHeader = function (bytes) { - let buffer = Buffer.from(bytes); + // let buffer = Buffer.from(bytes); // console.log( // new Date(), // '[debug] parseHeader(bytes)', @@ -47,8 +47,8 @@ var CJParser = ('object' === typeof module && exports) || {}; // buffer.toString('hex'), // ); // console.log(buffer.toString('utf8')); + // bytes = new Uint8Array(buffer); - bytes = new Uint8Array(buffer); if (bytes.length < CJParser.HEADER_SIZE) { console.log( `[DEBUG] malformed header`, @@ -94,17 +94,16 @@ var CJParser = ('object' === typeof module && exports) || {}; * @param {Uint8Array} bytes */ CJParser.parseVersion = function (bytes) { - let buffer = Buffer.from(bytes); + // let buffer = Buffer.from(bytes); // console.log( // '[debug] parseVersion(bytes)', // buffer.length, // buffer.toString('hex'), // ); // console.log(buffer.toString('utf8')); + // bytes = new Uint8Array(buffer); - bytes = new Uint8Array(buffer); let dv = new DataView(bytes.buffer); - let versionStart = 0; let version = dv.getUint32(versionStart, DV_LITTLE_ENDIAN); @@ -248,9 +247,9 @@ var CJParser = ('object' === typeof module && exports) || {}; * @param {Uint8Array} bytes */ CJParser.parseDssu = function (bytes) { - let buffer = Buffer.from(bytes); + // let buffer = Buffer.from(bytes); + // bytes = new Uint8Array(buffer); - bytes = new Uint8Array(buffer); let dv = new DataView(bytes.buffer); // console.log('[debug] parseDssu(bytes)', bytes.length, buffer.toString('hex')); // console.log(buffer.toString('utf8')); @@ -315,9 +314,9 @@ var CJParser = ('object' === typeof module && exports) || {}; * @param {Uint8Array} bytes */ CJParser.parseDsq = function (bytes) { - let buffer = Buffer.from(bytes); + // let buffer = Buffer.from(bytes); + // bytes = new Uint8Array(buffer); - bytes = new Uint8Array(buffer); if (bytes.length !== CJParser.DSQ_SIZE) { let msg = `developer error: 'dsq' messages are ${CJParser.DSQ_SIZE} bytes, not ${bytes.length}`; throw new Error(msg); From ad23fd4faebc9737f25f3df1288c9372b84b04a5 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 12 Jul 2024 19:55:24 -0600 Subject: [PATCH 08/55] f: browser: switch from buffer to bytes --- parser.js | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/parser.js b/parser.js index 581d8c3..d80eae3 100644 --- a/parser.js +++ b/parser.js @@ -50,11 +50,11 @@ var CJParser = ('object' === typeof module && exports) || {}; // bytes = new Uint8Array(buffer); if (bytes.length < CJParser.HEADER_SIZE) { - console.log( - `[DEBUG] malformed header`, - buffer.toString('utf8'), - buffer.toString('hex'), - ); + // console.log( + // `[DEBUG] malformed header`, + // buffer.toString('utf8'), + // buffer.toString('hex'), + // ); let msg = `developer error: header should be ${CJParser.HEADER_SIZE}+ bytes (optional payload), not ${bytes.length}`; throw new Error(msg); } @@ -64,17 +64,17 @@ var CJParser = ('object' === typeof module && exports) || {}; let payloadSizeStart = 16; let checksumStart = 20; - let magicBytes = buffer.slice(0, commandStart); + let magicBytes = bytes.slice(0, commandStart); - let commandEnd = buffer.indexOf(0x00, commandStart); + let commandEnd = bytes.indexOf(0x00, commandStart); if (commandEnd >= payloadSizeStart) { throw new Error('command name longer than 12 bytes'); } - let commandBuf = buffer.slice(commandStart, commandEnd); + let commandBuf = bytes.slice(commandStart, commandEnd); let command = commandBuf.toString('utf8'); let payloadSize = dv.getUint32(payloadSizeStart, DV_LITTLE_ENDIAN); - let checksum = buffer.slice(checksumStart, checksumStart + 4); + let checksum = bytes.slice(checksumStart, checksumStart + 4); let headerMessage = { magicBytes, @@ -123,7 +123,7 @@ var CJParser = ('object' === typeof module && exports) || {}; ); let addrRecvAddressStart = addrRecvServicesStart + 8; // + SIZES.SERVICES (8) - let addrRecvAddress = buffer.slice( + let addrRecvAddress = bytes.slice( addrRecvAddressStart, addrRecvAddressStart + 16, ); @@ -138,7 +138,7 @@ var CJParser = ('object' === typeof module && exports) || {}; ); let addrTransAddressStart = addrTransServicesStart + 8; // + SIZES.SERVICES (8) - let addrTransAddress = buffer.slice( + let addrTransAddress = bytes.slice( addrTransAddressStart, addrTransAddressStart + 16, ); @@ -147,13 +147,13 @@ var CJParser = ('object' === typeof module && exports) || {}; let addrTransPort = dv.getUint16(addrTransPortStart, DV_LITTLE_ENDIAN); let nonceStart = addrTransPortStart + 2; // + SIZES.PORT (2) - let nonce = buffer.slice(nonceStart, nonceStart + 8); + let nonce = bytes.slice(nonceStart, nonceStart + 8); let uaSizeStart = 80; // + SIZES.PORT (2) - let uaSize = buffer[uaSizeStart]; + let uaSize = bytes[uaSizeStart]; let uaStart = uaSizeStart + 1; - let uaBytes = buffer.slice(uaStart, uaStart + uaSize); + let uaBytes = bytes.slice(uaStart, uaStart + uaSize); let ua = uaBytes.toString('utf8'); let startHeightStart = uaStart + uaSize; @@ -162,22 +162,22 @@ var CJParser = ('object' === typeof module && exports) || {}; let relayStart = startHeightStart + 4; /** @type {Boolean?} */ let relay = null; - if (buffer.length > relayStart) { - relay = buffer[relayStart] > 0; + if (bytes.length > relayStart) { + relay = bytes[relayStart] > 0; } let mnAuthChStart = relayStart + 1; /** @type {Uint8Array?} */ let mnAuthChallenge = null; - if (buffer.length > mnAuthChStart) { - mnAuthChallenge = buffer.slice(mnAuthChStart, mnAuthChStart + 32); + if (bytes.length > mnAuthChStart) { + mnAuthChallenge = bytes.slice(mnAuthChStart, mnAuthChStart + 32); } let mnConnStart = mnAuthChStart + 32; /** @type {Boolean?} */ let mnConn = null; - if (buffer.length > mnConnStart) { - mnConn = buffer[mnConnStart] > 0; + if (bytes.length > mnConnStart) { + mnConn = bytes[mnConnStart] > 0; } let versionMessage = { From 520992d1fe6a03f6ffc11accc1e094ac21d0f278 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 13 Jul 2024 20:15:13 -0600 Subject: [PATCH 09/55] f: switch to Uint8Array --- demo.js | 95 +++++++++++++++++++++++++++++++++++++++---------------- parser.js | 6 ++-- 2 files changed, 71 insertions(+), 30 deletions(-) diff --git a/demo.js b/demo.js index a6af5bd..0cf54ea 100644 --- a/demo.js +++ b/demo.js @@ -679,17 +679,18 @@ var CJDemo = ('object' === typeof module && exports) || {}; let dataCount = 0; // conn.on('data', function (data) { + // let bytes = new Uint8Array(data); // console.log('[DEBUG] data'); - // console.log(dataCount, data.length, data.toString('hex')); + // console.log(dataCount, bytes.length, DashTx.utils.bytesToHex(bytes)); // dataCount += 1; // }); console.log('[DEBUG] main add wsc.onmessage'); wsc.addEventListener('message', async function (wsevent) { console.log('[DEBUG] main wsc.onmessage'); let ab = await wsevent.data.arrayBuffer(); - let data = Buffer.from(ab); + let bytes = new Uint8Array(ab); console.log('[DEBUG] data (main)'); - console.log(dataCount, data.length, data.toString('hex')); + console.log(dataCount, bytes.length, DashTx.utils.bytesToHex(bytes)); dataCount += 1; }); @@ -775,25 +776,25 @@ var CJDemo = ('object' === typeof module && exports) || {}; _reject(err); } - function onReadableHeader(data) { - let size = data?.length || 0; - console.log('State: reading header', size); + function onReadableHeader(bytes) { + let size = bytes?.length || 0; + console.log('State: reading header', size, typeof bytes); let chunk; for (;;) { - chunk = data; + chunk = bytes; // chunk = conn.read(); // TODO reenable if (!chunk) { break; } chunks.push(chunk); chunksLength += chunk.byteLength; - data = null; // TODO nix + bytes = null; // TODO nix } if (chunksLength < HEADER_SIZE) { return; } if (chunks.length > 1) { - chunk = Buffer.concat(chunks, chunksLength); + chunk = concatBytes(chunks, chunksLength); } else { chunk = chunks[0]; } @@ -810,7 +811,7 @@ var CJDemo = ('object' === typeof module && exports) || {}; console.log(`[DEBUG] header`, header); throw new Error('too big you are, handle you I cannot'); } - // console.log('DEBUG header', header); + console.log('DEBUG header', header); console.log('[DEBUG] [onReadableHeader] remove data listener'); // conn.removeListener('readable', onReadableHeader); // conn.removeListener('data', onReadableHeader); @@ -829,34 +830,41 @@ var CJDemo = ('object' === typeof module && exports) || {}; wsc.addEventListener('message', onWsReadablePayload); onReadablePayload(null); } + async function onNodeReadableHeader(data) { + let bytes = new Uint8Array(data); + onReadableHeader(bytes); + } async function onWsReadableHeader(wsevent) { console.log('[DEBUG] onReadableHeader wsc.onmessage'); let ab = await wsevent.data.arrayBuffer(); - let data = Buffer.from(ab); - console.log('[DEBUG] data (readable header)'); - console.log(dataCount, data.length, data.toString('hex')); - onReadableHeader(data); + let bytes = new Uint8Array(ab); + console.log('[DEBUG] bytes (readable header)'); + console.log(dataCount, bytes.length, DashTx.utils.bytesToHex(bytes)); + onReadableHeader(bytes); } - function onReadablePayload(data) { - let size = data?.length || 0; + /** + * @param {Uint8Array} bytes + */ + function onReadablePayload(bytes) { + let size = bytes?.length || 0; console.log('State: reading payload', size); let chunk; for (;;) { - chunk = data; + chunk = bytes; // chunk = conn.read(); // TODO revert if (!chunk) { break; } chunks.push(chunk); chunksLength += chunk.byteLength; - data = null; // TODO nix + bytes = null; // TODO nix } if (chunksLength < header.payloadSize) { return; } if (chunks.length > 1) { - chunk = Buffer.concat(chunks, chunksLength); + chunk = concatBytes(chunks, chunksLength); } else if (chunks.length === 1) { chunk = chunks[0]; } else { @@ -879,13 +887,17 @@ var CJDemo = ('object' === typeof module && exports) || {}; wsc.removeEventListener('message', onWsReadablePayload); resolve(header); } + async function onNodeReadablePayload(data) { + let bytes = new Uint8Array(data); + onReadablePayload(bytes); + } async function onWsReadablePayload(wsevent) { console.log('[DEBUG] onReadablePayload wsc.onmessage'); let ab = await wsevent.data.arrayBuffer(); - let data = Buffer.from(ab); + let bytes = new Uint8Array(ab); console.log('[DEBUG] data (readable payload)'); - console.log(dataCount, data.length, data.toString('hex')); - onReadablePayload(data); + console.log(dataCount, bytes.length, DashTx.utils.bytesToHex(bytes)); + onReadablePayload(bytes); } errReject = reject; @@ -931,17 +943,21 @@ var CJDemo = ('object' === typeof module && exports) || {}; resolve(); } - function onReadable() { + function onReadable(bytes) { // checking an impossible condition, just in case throw new Error('unexpected response before request'); } + async function onNodeReadable(data) { + let bytes = new Uint8Array(data); + onReadable(bytes); + } async function onWsReadable(wsevent) { console.log('[DEBUG] waitForConnect wsc.onmessage'); let ab = await wsevent.data.arrayBuffer(); - let data = Buffer.from(ab); + let bytes = new Uint8Array(ab); console.log('[DEBUG] data (readable)'); - console.log(dataCount, data.length, data.toString('hex')); - onReadable(data); + console.log(dataCount, bytes.length, DashTx.utils.bytesToHex(bytes)); + onReadable(bytes); } errReject = reject; @@ -1093,8 +1109,9 @@ var CJDemo = ('object' === typeof module && exports) || {}; // conn.write(dsaMsg); wsc.send(dsaMsg); - let dsaBuf = Buffer.from(dsaMsg); - console.log('[debug] dsa', dsaBuf.toString('hex')); + // let dsaBuf = Buffer.from(dsaMsg); + // console.log('[debug] dsa', dsaBuf.toString('hex')); + console.log('[debug] dsa', DashTx.utils.bytesToHex(dsaMsg)); let dsq = await dsqPromise; for (; !dsq.ready; ) { @@ -1389,6 +1406,28 @@ var CJDemo = ('object' === typeof module && exports) || {}; // return arr; // } + /** + * @param {Array} byteArrays + * @param {Number?} [len] + * @returns {Uint8Array} + */ + function concatBytes(byteArrays, len) { + if (!len) { + for (let bytes of byteArrays) { + len += bytes.length; + } + } + + let allBytes = new Uint8Array(len); + let offset = 0; + for (let bytes of byteArrays) { + allBytes.set(bytes, offset); + offset += bytes.length; + } + + return allBytes; + } + function sleep(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); diff --git a/parser.js b/parser.js index d80eae3..c03fefc 100644 --- a/parser.js +++ b/parser.js @@ -27,6 +27,8 @@ var CJParser = ('object' === typeof module && exports) || {}; CJParser.DSQ_SIZE = 142; CJParser.SESSION_ID_SIZE = 4; + let textDecoder = new TextDecoder(); + /** * Parse the 24-byte P2P Message Header * - 4 byte magic bytes (delimiter) (possibly intended for non-tcp messages?) @@ -71,7 +73,7 @@ var CJParser = ('object' === typeof module && exports) || {}; throw new Error('command name longer than 12 bytes'); } let commandBuf = bytes.slice(commandStart, commandEnd); - let command = commandBuf.toString('utf8'); + let command = textDecoder.decode(commandBuf); let payloadSize = dv.getUint32(payloadSizeStart, DV_LITTLE_ENDIAN); let checksum = bytes.slice(checksumStart, checksumStart + 4); @@ -154,7 +156,7 @@ var CJParser = ('object' === typeof module && exports) || {}; let uaStart = uaSizeStart + 1; let uaBytes = bytes.slice(uaStart, uaStart + uaSize); - let ua = uaBytes.toString('utf8'); + let ua = textDecoder.decode(uaBytes); let startHeightStart = uaStart + uaSize; let startHeight = dv.getUint32(startHeightStart, DV_LITTLE_ENDIAN); From 9d304339854cf47ca6e3207e2cea2545bc8cc295 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 13 Jul 2024 20:42:16 -0600 Subject: [PATCH 10/55] chore: add package.json.engines.node >= 22 (for WebSocket) --- package-lock.json | 3 +++ package.json | 3 +++ 2 files changed, 6 insertions(+) diff --git a/package-lock.json b/package-lock.json index f073525..07e4cfb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,9 @@ "dashrpc": "^20.0.0", "dashtx": "^0.18.1", "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=22.0.0" } }, "node_modules/@dashincubator/secp256k1": { diff --git a/package.json b/package.json index 4795890..1d7a843 100644 --- a/package.json +++ b/package.json @@ -41,5 +41,8 @@ "dashrpc": "^20.0.0", "dashtx": "^0.18.1", "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=22.0.0" } } From 9afe4fe89b8b54879c0762132018fd82967f29aa Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 13 Jul 2024 21:06:54 -0600 Subject: [PATCH 11/55] feat: move wss url to ENVs --- demo.js | 2 +- example.env | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/demo.js b/demo.js index 0cf54ea..3d5e092 100644 --- a/demo.js +++ b/demo.js @@ -647,7 +647,7 @@ var CJDemo = ('object' === typeof module && exports) || {}; }; let searchParams = new URLSearchParams(query); let search = searchParams.toString(); - let wsc = new WebSocket(`ws://127.0.0.1:8080/tcp?${search}`); + let wsc = new WebSocket(`${process.env.DASHD_TCP_WS_URL}?${search}`); //let conn = Net.createConnection({ // host: evonode.hostname, // port: evonode.port, diff --git a/example.env b/example.env index a370b3c..a758ba1 100644 --- a/example.env +++ b/example.env @@ -7,6 +7,7 @@ DASHD_RPC_HOST='127.0.0.1' # mainnet=9998, testnet=19998, regtest= DASHD_RPC_PORT='20302' DASHD_RPC_TIMEOUT='10.0' +DASHD_TCP_WS_URL='ws://127.0.0.1:8080/tcp' # Generate this from # npx -p dashphrase-cli -- dashphrase gen --bits 128 -o ./words.txt From adeeaf2886bf8c407fea51ead98928209a3b4807 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 13 Jul 2024 23:53:59 -0600 Subject: [PATCH 12/55] wip: feat: add DASH_WALLET_SALT --- example.env | 2 +- node-env.js | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 node-env.js diff --git a/example.env b/example.env index a758ba1..55d2ae9 100644 --- a/example.env +++ b/example.env @@ -5,7 +5,7 @@ DASHD_RPC_PASS='123456789012' DASHD_RPC_PASSWORD='123456789012' DASHD_RPC_HOST='127.0.0.1' # mainnet=9998, testnet=19998, regtest= -DASHD_RPC_PORT='20302' +DASHD_RPC_PORT='8080' DASHD_RPC_TIMEOUT='10.0' DASHD_TCP_WS_URL='ws://127.0.0.1:8080/tcp' diff --git a/node-env.js b/node-env.js new file mode 100644 index 0000000..3ad3b1c --- /dev/null +++ b/node-env.js @@ -0,0 +1,12 @@ +'use strict'; + +let DotEnv = window.ENV || require('dotenv'); +if (DotEnv.config) { + void DotEnv.config({ path: '.env' }); + void DotEnv.config({ path: '.env.secret' }); +} + +Object.assign(module.exports, process.env); +Object.assign(module.exports, { + DASH_WALLET_SALT: process.argv[2] || '', +}); From f388b3944972be13ca6cfcec999deeb4a4b25e85 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 13 Jul 2024 23:54:13 -0600 Subject: [PATCH 13/55] wip: test.html --- test.html | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 test.html diff --git a/test.html b/test.html new file mode 100644 index 0000000..2b15c76 --- /dev/null +++ b/test.html @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + Check the console. + + From 6cdcdc83ee750664981a68ca5293202db9de9b87 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 13 Jul 2024 23:54:50 -0600 Subject: [PATCH 14/55] wip: make demo work in browser --- demo.js | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/demo.js b/demo.js index 3d5e092..f1b1d5a 100644 --- a/demo.js +++ b/demo.js @@ -3,24 +3,22 @@ var CJDemo = ('object' === typeof module && exports) || {}; (function (window, CJDemo) { 'use strict'; - let DotEnv = window.ENVS || require('dotenv'); - if (DotEnv.config) { - void DotEnv.config({ path: '.env' }); - void DotEnv.config({ path: '.env.secret' }); - } + let ENV = window.ENV || require('./node-env.js'); //@ts-ignore - ts can't understand JSON, still... - let pkg = DotEnv.package || require('./package.json'); + let pkg = ENV.package || require('./package.json'); - let Packer = require('./packer.js'); // TODO rename packer - let Parser = require('./parser.js'); + //@ts-ignore + let Packer = window.CJPacker || require('./packer.js'); + //@ts-ignore + let Parser = window.CJParser || require('./parser.js'); let DashPhrase = window.DashPhrase || require('dashphrase'); let DashHd = window.DashHd || require('dashhd'); let DashKeys = window.DashKeys || require('dashkeys'); let DashRpc = window.DashRpc || require('dashrpc'); let DashTx = window.DashTx || require('dashtx'); - let Secp256k1 = window.Secp256k1 || require('@dashincubator/secp256k1'); + let Secp256k1 = window.nobleSecp256k1 || require('@dashincubator/secp256k1'); // (STANDARD_DENOMINATIONS[0] / 10).floor(); const COLLATERAL = 10000; @@ -40,17 +38,17 @@ var CJDemo = ('object' === typeof module && exports) || {}; let rpcConfig = { protocol: 'http', // https for remote, http for local / private networking - user: process.env.DASHD_RPC_USER, - pass: process.env.DASHD_RPC_PASS || process.env.DASHD_RPC_PASSWORD, - host: process.env.DASHD_RPC_HOST || '127.0.0.1', - port: process.env.DASHD_RPC_PORT || '19898', // mainnet=9998, testnet=19998, regtest=19898 + user: ENV.DASHD_RPC_USER, + pass: ENV.DASHD_RPC_PASS || ENV.DASHD_RPC_PASSWORD, + host: ENV.DASHD_RPC_HOST || '127.0.0.1', + port: ENV.DASHD_RPC_PORT || '19898', // mainnet=9998, testnet=19998, regtest=19898 timeout: 10 * 1000, // bump default from 5s to 10s for up to 10k addresses onconnected: async function () { console.info(`[info] rpc client connected ${rpcConfig.host}`); }, }; - if (process.env.DASHD_RPC_TIMEOUT) { - let rpcTimeoutSec = parseFloat(process.env.DASHD_RPC_TIMEOUT); + if (ENV.DASHD_RPC_TIMEOUT) { + let rpcTimeoutSec = parseFloat(ENV.DASHD_RPC_TIMEOUT); rpcConfig.timeout = rpcTimeoutSec * 1000; } @@ -58,7 +56,7 @@ var CJDemo = ('object' === typeof module && exports) || {}; /* jshint maxstatements: 1000 */ /* jshint maxcomplexity: 100 */ - let walletSalt = process.argv[2] || ''; + let walletSalt = ENV._WALLET_SALT || ''; let isHelp = walletSalt === 'help' || walletSalt === '--help'; if (isHelp) { throw new Error( @@ -66,7 +64,7 @@ var CJDemo = ('object' === typeof module && exports) || {}; ); } - let walletPhrase = process.env.DASH_WALLET_PHRASE || ''; + let walletPhrase = ENV.DASH_WALLET_PHRASE || ''; if (!walletPhrase) { throw new Error('missing DASH_WALLET_PHRASE'); } @@ -74,6 +72,7 @@ var CJDemo = ('object' === typeof module && exports) || {}; let network = 'regtest'; // let minimumParticipants = Packer.NETWORKS[network].minimumParticiparts; + console.log(`[DEBUG] rpcConfig`, rpcConfig); let rpc = DashRpc.create(rpcConfig); let height = await rpc.init(rpc); console.info(`[info] rpc server is ready. Height = ${height}`); @@ -647,7 +646,7 @@ var CJDemo = ('object' === typeof module && exports) || {}; }; let searchParams = new URLSearchParams(query); let search = searchParams.toString(); - let wsc = new WebSocket(`${process.env.DASHD_TCP_WS_URL}?${search}`); + let wsc = new WebSocket(`${ENV.DASHD_TCP_WS_URL}?${search}`); //let conn = Net.createConnection({ // host: evonode.hostname, // port: evonode.port, From 5f5a76a2f6533cfe83596f1e03462a38295df7be Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 13 Jul 2024 23:59:32 -0600 Subject: [PATCH 15/55] wip: feat: add DASH_WALLET_SALT --- demo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo.js b/demo.js index f1b1d5a..091e906 100644 --- a/demo.js +++ b/demo.js @@ -56,7 +56,7 @@ var CJDemo = ('object' === typeof module && exports) || {}; /* jshint maxstatements: 1000 */ /* jshint maxcomplexity: 100 */ - let walletSalt = ENV._WALLET_SALT || ''; + let walletSalt = ENV.DASH_WALLET_SALT || ''; let isHelp = walletSalt === 'help' || walletSalt === '--help'; if (isHelp) { throw new Error( From c0be256115c77b0aa2db6dc51d59ece9ce171e33 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 14 Jul 2024 00:30:08 -0600 Subject: [PATCH 16/55] f: DASH_WALLET_SALT (node-env) --- node-env.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node-env.js b/node-env.js index 3ad3b1c..62d13a3 100644 --- a/node-env.js +++ b/node-env.js @@ -1,6 +1,6 @@ 'use strict'; -let DotEnv = window.ENV || require('dotenv'); +let DotEnv = require('dotenv'); if (DotEnv.config) { void DotEnv.config({ path: '.env' }); void DotEnv.config({ path: '.env.secret' }); From 0d886f673db54684286b2a9309b63854cac7a8f0 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 14 Jul 2024 00:32:40 -0600 Subject: [PATCH 17/55] feat: add DASHD_RPC_PROTOCOL --- demo.js | 2 +- example.env | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/demo.js b/demo.js index 091e906..08b4533 100644 --- a/demo.js +++ b/demo.js @@ -37,7 +37,7 @@ var CJDemo = ('object' === typeof module && exports) || {}; const COINJOIN_ENTRY_MAX_SIZE = 2; // just for testing right now let rpcConfig = { - protocol: 'http', // https for remote, http for local / private networking + protocol: ENV.DASHD_RPC_PROTOTYPE || 'http', // https for remote, http for local / private networking user: ENV.DASHD_RPC_USER, pass: ENV.DASHD_RPC_PASS || ENV.DASHD_RPC_PASSWORD, host: ENV.DASHD_RPC_HOST || '127.0.0.1', diff --git a/example.env b/example.env index 55d2ae9..d14443d 100644 --- a/example.env +++ b/example.env @@ -3,6 +3,7 @@ DASHD_RPC_USER='abcd1234' DASHD_RPC_PASS='123456789012' DASHD_RPC_PASSWORD='123456789012' +DASHD_RPC_PROTOCOL='http' DASHD_RPC_HOST='127.0.0.1' # mainnet=9998, testnet=19998, regtest= DASHD_RPC_PORT='8080' From bb3d08203a659c598f7608f7c85e8b731ee279b1 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 14 Jul 2024 00:32:57 -0600 Subject: [PATCH 18/55] f: feat: add DASHD_RPC_PROTOCOL --- demo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo.js b/demo.js index 08b4533..e82fc4e 100644 --- a/demo.js +++ b/demo.js @@ -37,7 +37,7 @@ var CJDemo = ('object' === typeof module && exports) || {}; const COINJOIN_ENTRY_MAX_SIZE = 2; // just for testing right now let rpcConfig = { - protocol: ENV.DASHD_RPC_PROTOTYPE || 'http', // https for remote, http for local / private networking + protocol: ENV.DASHD_RPC_PROTOCOL || 'http', // https for remote, http for local / private networking user: ENV.DASHD_RPC_USER, pass: ENV.DASHD_RPC_PASS || ENV.DASHD_RPC_PASSWORD, host: ENV.DASHD_RPC_HOST || '127.0.0.1', From 6025aa1b54018fd0468f6664e151bea03b559b95 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 14 Jul 2024 00:33:59 -0600 Subject: [PATCH 19/55] f: ref!: make browser compatible --- demo.js | 1 - 1 file changed, 1 deletion(-) diff --git a/demo.js b/demo.js index e82fc4e..b80986f 100644 --- a/demo.js +++ b/demo.js @@ -72,7 +72,6 @@ var CJDemo = ('object' === typeof module && exports) || {}; let network = 'regtest'; // let minimumParticipants = Packer.NETWORKS[network].minimumParticiparts; - console.log(`[DEBUG] rpcConfig`, rpcConfig); let rpc = DashRpc.create(rpcConfig); let height = await rpc.init(rpc); console.info(`[info] rpc server is ready. Height = ${height}`); From af58cecaaef2f3696b75fa0c8b9f33ca24844b93 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 14 Jul 2024 00:36:17 -0600 Subject: [PATCH 20/55] f: use ENV.DASH_RPC_PROTOCOL for rpc url --- demo.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demo.js b/demo.js index b80986f..4b17baa 100644 --- a/demo.js +++ b/demo.js @@ -610,7 +610,8 @@ var CJDemo = ('object' === typeof module && exports) || {}; let evonodes = []; { //let resp = await rpc.masternodelist(); - let res = await fetch('http://127.0.0.1:8080/rpc/masternodelist'); + let rpcBaseUrl = `${ENV.DASH_RPC_PROTOCOL}://${ENV.DASH_RPC_HOST}:${ENV.DASH_RPC_PORT}`; + let res = await fetch(`${rpcBaseUrl}/rpc/masternodelist`); let resp = await res.json(); let evonodesMap = resp.result; let evonodeProTxIds = Object.keys(evonodesMap); From aa413c34b1194d4719f9e240ddee53536c7338bc Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 14 Jul 2024 00:37:08 -0600 Subject: [PATCH 21/55] f: use ENV.DASH_RPC_PROTOCOL for rpc url --- demo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo.js b/demo.js index 4b17baa..3a703b0 100644 --- a/demo.js +++ b/demo.js @@ -610,7 +610,7 @@ var CJDemo = ('object' === typeof module && exports) || {}; let evonodes = []; { //let resp = await rpc.masternodelist(); - let rpcBaseUrl = `${ENV.DASH_RPC_PROTOCOL}://${ENV.DASH_RPC_HOST}:${ENV.DASH_RPC_PORT}`; + let rpcBaseUrl = `${ENV.DASHD_RPC_PROTOCOL}://${ENV.DASHD_RPC_HOST}:${ENV.DASHD_RPC_PORT}`; let res = await fetch(`${rpcBaseUrl}/rpc/masternodelist`); let resp = await res.json(); let evonodesMap = resp.result; From 63d9314e8d93ef8aa310ac972aecd5f4defdb4a0 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 14 Jul 2024 00:46:24 -0600 Subject: [PATCH 22/55] f: wip: test.html --- example.env.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 example.env.js diff --git a/example.env.js b/example.env.js new file mode 100644 index 0000000..c8b881a --- /dev/null +++ b/example.env.js @@ -0,0 +1,28 @@ +var ENV; + +(function () { + 'use strict'; + + ENV = { + // regtest= + DASHD_RPC_USER: 'abcd1234', + DASHD_RPC_PASS: '123456789012', + DASHD_RPC_PASSWORD: '123456789012', + DASHD_RPC_PROTOCOL: 'http', + DASHD_RPC_HOST: 'localhost', + // mainnet=9998, testnet=19998, regtest= + DASHD_RPC_PORT: '8080', + DASHD_RPC_TIMEOUT: '10.0', + DASHD_TCP_WS_URL: 'ws://localhost:8080/tcp', + // Generate this from + // npx -p dashphrase-cli -- dashphrase gen --bits 128 -o ./words.txt + // npx -p dashphrase-cli -- dashphrase seed ./words.txt "" -o ./seed.hex + DASH_WALLET_PHRASE: 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong', + //DASH_WALLET_SALT: 'TREZOR', + DASH_WALLET_SEED: + 'ac27495480225222079d7be181583751e86f571027b0497b5b5d11218e0a8a13332572917f0f8e5a589620c6f15b11c61dee327651a14c34e18231052e48c069', + package: { + version: '1.0.0', + }, + }; +})(); From 3aca2fd015133bf595d93a094afaf8bc6086e167 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 14 Jul 2024 01:00:10 -0600 Subject: [PATCH 23/55] wip: feat: move config out to begin to librarify --- demo.js | 39 +++-------------------- run-demo.js | 36 ++++++++++++++++++++++ test.html | 89 +++++++++++++++++++---------------------------------- 3 files changed, 72 insertions(+), 92 deletions(-) create mode 100644 run-demo.js diff --git a/demo.js b/demo.js index 3a703b0..63ae92f 100644 --- a/demo.js +++ b/demo.js @@ -3,11 +3,6 @@ var CJDemo = ('object' === typeof module && exports) || {}; (function (window, CJDemo) { 'use strict'; - let ENV = window.ENV || require('./node-env.js'); - - //@ts-ignore - ts can't understand JSON, still... - let pkg = ENV.package || require('./package.json'); - //@ts-ignore let Packer = window.CJPacker || require('./packer.js'); //@ts-ignore @@ -36,26 +31,13 @@ var CJDemo = ('object' === typeof module && exports) || {}; // const COINJOIN_ENTRY_MAX_SIZE = 9; // real const COINJOIN_ENTRY_MAX_SIZE = 2; // just for testing right now - let rpcConfig = { - protocol: ENV.DASHD_RPC_PROTOCOL || 'http', // https for remote, http for local / private networking - user: ENV.DASHD_RPC_USER, - pass: ENV.DASHD_RPC_PASS || ENV.DASHD_RPC_PASSWORD, - host: ENV.DASHD_RPC_HOST || '127.0.0.1', - port: ENV.DASHD_RPC_PORT || '19898', // mainnet=9998, testnet=19998, regtest=19898 - timeout: 10 * 1000, // bump default from 5s to 10s for up to 10k addresses - onconnected: async function () { - console.info(`[info] rpc client connected ${rpcConfig.host}`); - }, - }; - if (ENV.DASHD_RPC_TIMEOUT) { - let rpcTimeoutSec = parseFloat(ENV.DASHD_RPC_TIMEOUT); - rpcConfig.timeout = rpcTimeoutSec * 1000; - } - - async function main() { + CJDemo.run = async function (ENV, rpcConfig) { /* jshint maxstatements: 1000 */ /* jshint maxcomplexity: 100 */ + //@ts-ignore - ts can't understand JSON, still... + let pkg = ENV.package || require('./package.json'); + let walletSalt = ENV.DASH_WALLET_SALT || ''; let isHelp = walletSalt === 'help' || walletSalt === '--help'; if (isHelp) { @@ -1367,7 +1349,7 @@ var CJDemo = ('object' === typeof module && exports) || {}; } console.log('Sweet, sweet victory!'); - } + }; /** * @param {Object} a @@ -1433,17 +1415,6 @@ var CJDemo = ('object' === typeof module && exports) || {}; }); } - main() - .then(function () { - console.info('Done'); - process.exit(0); - }) - .catch(function (err) { - console.error('Fail:'); - console.error(err.stack || err); - process.exit(1); - }); - // @ts-ignore window.CJDemo = CJDemo; })(('object' === typeof window && window) || {}, CJDemo); diff --git a/run-demo.js b/run-demo.js new file mode 100644 index 0000000..710ac62 --- /dev/null +++ b/run-demo.js @@ -0,0 +1,36 @@ +'use strict'; + +let CJDemo = require('./demo.js'); + +// TODO move to +let ENV = require('./node-env.js'); +let rpcConfig = { + protocol: ENV.DASHD_RPC_PROTOCOL || 'http', // https for remote, http for local / private networking + user: ENV.DASHD_RPC_USER, + pass: ENV.DASHD_RPC_PASS || ENV.DASHD_RPC_PASSWORD, + host: ENV.DASHD_RPC_HOST || '127.0.0.1', + port: ENV.DASHD_RPC_PORT || '19898', // mainnet=9998, testnet=19998, regtest=19898 + timeout: 10 * 1000, // bump default from 5s to 10s for up to 10k addresses + onconnected: async function () { + console.info(`[info] rpc client connected ${rpcConfig.host}`); + }, +}; +if (ENV.DASHD_RPC_TIMEOUT) { + let rpcTimeoutSec = parseFloat(ENV.DASHD_RPC_TIMEOUT); + rpcConfig.timeout = rpcTimeoutSec * 1000; +} + +CJDemo.run(ENV, rpcConfig) + .then(function () { + console.info('Done'); + if (typeof process !== 'undefined') { + process.exit(0); + } + }) + .catch(function (err) { + console.error('Fail:'); + console.error(err.stack || err); + if (typeof process !== 'undefined') { + process.exit(1); + } + }); diff --git a/test.html b/test.html index 2b15c76..decac38 100644 --- a/test.html +++ b/test.html @@ -1,7 +1,5 @@ - - @@ -12,72 +10,47 @@ + + From 42889e38391e4807867f5ba1d52b444df758dbe7 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 13 Aug 2024 16:24:31 -0600 Subject: [PATCH 24/55] feat: add browser wallet --- public/index.html | 391 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 391 insertions(+) create mode 100644 public/index.html diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..f62b78a --- /dev/null +++ b/public/index.html @@ -0,0 +1,391 @@ + + + + + + Wallet - Digital Cash + + + + + + + + + +
+ +
+
+
+
+ + + + +
+ + Spent Addresses + addresses with utxos + +
       
+
+ + + + +
+
+
+
+
+ +
+
+ +
+ + + + From a2215c1a38992a0613054b24d3019943e0f3e34e Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 13 Aug 2024 16:33:15 -0600 Subject: [PATCH 25/55] f: feat: add browser wallet --- public/index.html | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/public/index.html b/public/index.html index f62b78a..36b4056 100644 --- a/public/index.html +++ b/public/index.html @@ -39,11 +39,14 @@ margin: 0; padding: 0; } - pre code, - code { + pre code { margin: 0; padding: 0.3rem 1rem 0.3rem 1rem; } + code { + margin: 0.1rem; + padding: 0rem 0.3rem 0rem 0.3rem; + } @@ -141,7 +144,7 @@

Digital Cash Wallet

data-id="spent">        -
- - Spent Addresses - addresses with utxos - -
       
-
+
+ + + + + + + + + + + + + + + + + + + + +
AmountAddressTXIDIndex
+ +
+ + +
+ + + 200 dust + + + + + + + + + + + +
- +
+
+ + Spent Addresses (0) + addresses with spent outputs + +
       
+
+
@@ -179,229 +277,6 @@

Digital Cash Wallet

- + diff --git a/public/package-lock.json b/public/package-lock.json index 298c1ad..9328996 100644 --- a/public/package-lock.json +++ b/public/package-lock.json @@ -12,7 +12,8 @@ "@dashincubator/secp256k1": "^1.7.1-5", "dashhd": "^3.3.3", "dashkeys": "^1.1.5", - "dashphrase": "^1.4.0" + "dashphrase": "^1.4.0", + "dashtx": "^0.18.2" } }, "node_modules/@dashincubator/secp256k1": { @@ -38,6 +39,15 @@ "resolved": "https://registry.npmjs.org/dashphrase/-/dashphrase-1.4.0.tgz", "integrity": "sha512-o+LdiPkiYmg07kXBE+2bbcJzBmeTQVPn1GS2XlQeo8lene+KknAprSyiYi5XtqV/QVgNjvzOV7qBst2MijSPAA==", "license": "SEE LICENSE IN LICENSE" + }, + "node_modules/dashtx": { + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/dashtx/-/dashtx-0.18.2.tgz", + "integrity": "sha512-Q8X2Xkw2mtkd3FTtghSHi85AS/e7i98AVd8kHPL8PRLQMxYMPc3f3mvtVphM26ST+Fp2WNEjQZMIBgMWeZsECg==", + "license": "SEE LICENSE IN LICENSE", + "bin": { + "dashtx-inspect": "bin/inspect.js" + } } } } diff --git a/public/package.json b/public/package.json index 727ad4d..fd121f2 100644 --- a/public/package.json +++ b/public/package.json @@ -27,6 +27,7 @@ "@dashincubator/secp256k1": "^1.7.1-5", "dashhd": "^3.3.3", "dashkeys": "^1.1.5", - "dashphrase": "^1.4.0" + "dashphrase": "^1.4.0", + "dashtx": "^0.18.2" } } diff --git a/public/wallet-app.js b/public/wallet-app.js new file mode 100644 index 0000000..4d4927d --- /dev/null +++ b/public/wallet-app.js @@ -0,0 +1,552 @@ +(function () { + 'use strict'; + + function $(sel, el) { + return (el || document).querySelector(sel); + } + + function $$(sel, el) { + return Array.from((el || document).querySelectorAll(sel)); + } + + let DashPhrase = window.DashPhrase; + let DashHd = window.DashHd; + let DashKeys = window.DashKeys; + let DashTx = window.DashTx; + let Secp256k1 = window.nobleSecp256k1; + + const SATS = 100000000; + const MIN_BALANCE = 100001 * 1000; + + let network = 'testnet'; + let rpcBaseUrl = 'https://trpc.digitalcash.dev/'; + let rpcBasicAuth = btoa(`api:null`); + + let addresses = []; + let changeAddrs = []; + let receiveAddrs = []; + let spentAddrs = []; + let deltasMap = {}; + let keysMap = {}; + + let keyUtils = { + getPrivateKey: async function (txInput, i) { + // let address; + let address = txInput.address; + if (!address) { + let pkhBytes = DashKeys.utils.hexToBytes(txInput.pubKeyHash); + address = await DashKeys.pkhToAddr(pkhBytes, { version: network }); + } + + let yourKeyData = keysMap[address]; + + let privKeyBytes = await DashKeys.wifToPrivKey(yourKeyData.wif, { + version: network, + }); + return privKeyBytes; + }, + + getPublicKey: async function (txInput, i) { + let privKeyBytes = await keyUtils.getPrivateKey(txInput, i); + let pubKeyBytes = await keyUtils.toPublicKey(privKeyBytes); + + return pubKeyBytes; + }, + // TODO + // toPkh: DashKeys.pubkeyToPkh, + + sign: async function (privKeyBytes, txHashBytes) { + let sigOpts = { canonical: true, extraEntropy: true }; + let sigBytes = await Secp256k1.sign(txHashBytes, privKeyBytes, sigOpts); + + return sigBytes; + }, + + toPublicKey: async function (privKeyBytes) { + let isCompressed = true; + let pubKeyBytes = Secp256k1.getPublicKey(privKeyBytes, isCompressed); + + return pubKeyBytes; + }, + }; + let dashTx = DashTx.create(keyUtils); + + async function rpc(method, ...params) { + // typically http://localhost:19998/ + let payload = JSON.stringify({ method, params }); + let resp = await fetch(rpcBaseUrl, { + method: 'POST', + headers: { + Authorization: `Basic ${rpcBasicAuth}`, + 'Content-Type': 'application/json', + }, + body: payload, + }); + + let data = await resp.json(); + if (data.error) { + let err = new Error(data.error.message); + Object.assign(err, data.error); + throw err; + } + + return data.result; + } + + function dbGet(key, defVal) { + let dataJson = localStorage.getItem(key); + if (!dataJson) { + dataJson = JSON.stringify(defVal); + } + + let data; + try { + data = JSON.parse(dataJson); + } catch (e) { + data = defVal; + } + return data; + } + + function dbSet(key, val) { + if (val === null) { + localStorage.removeItem(key); + return; + } + + let dataJson = JSON.stringify(val); + localStorage.setItem(key, dataJson); + } + + function removeElement(arr, val) { + let index = arr.indexOf(val); + if (index !== -1) { + arr.splice(index, 1); + } + } + + window.toggleAll = function (event) { + let checked = event.target.checked; + + let $table = event.target.closest('table'); + for (let $input of $$('[type=checkbox]', $table)) { + $input.checked = checked; + } + return true; + }; + + window.setMax = function (event) { + let totalSats = 0; + let addrs = Object.keys(deltasMap); + let fee = 100; + for (let addr of addrs) { + let info = deltasMap[addr]; + if (info.balance === 0) { + continue; + } + for (let delta of info.deltas) { + totalSats += delta.satoshis; + fee += 100; + } + } + + totalSats -= fee; + const FOUR_ZEROS = 10000; + let sigDigits = Math.floor(totalSats / FOUR_ZEROS); + let totalSigSats = sigDigits * FOUR_ZEROS; + let totalAmount = totalSigSats / SATS; + let dust = totalSats - totalSigSats; + dust += fee; + + $('[data-id=send-amount]').value = totalAmount.toFixed(4); + //$('[data-id=send-dust]').value = dust; + $('[data-id=send-dust]').textContent = dust; + }; + + window.sendDash = async function (event) { + event.preventDefault(); + + let amountStr = $('[data-id=send-amount]').value || 0; + let amount = parseFloat(amountStr); + let satoshis = Math.round(amount * SATS); + // if (satoshis === 0) { + // satoshis = null; + // } + + let address = $('[data-id=send-address]').value; + if (!address) { + let err = new Error(`missing payment 'address' to send funds to`); + window.alert(err.message); + throw err; + } + + let balance = 0; + + /** @type {Array?} */ + let inputs = null; + /** @type {Array?} */ + let utxos = null; + + let $coins = $$('[data-name=coin]:checked'); + if ($coins.length) { + inputs = []; + for (let $coin of $coins) { + let [address, txid, indexStr] = $coin.value.split(','); + let index = parseInt(indexStr, 10); + let coin = selectCoin(address, txid, index); + balance += coin.satoshis; + Object.assign(coin, { outputIndex: coin.index }); + inputs.push(coin); + } + } else { + utxos = []; + let spendables = Object.keys(deltasMap); + for (let address of spendables) { + let info = deltasMap[address]; + if (info.balance === 0) { + continue; + } + for (let coin of info.deltas) { + balance += coin.satoshis; + Object.assign(coin, { outputIndex: coin.index }); + utxos.push(coin); + } + } + } + + if (balance < satoshis) { + // there's a helper for this in DashTx, including fee calc, + // but this is quick-n-dirty just to get an alert rather than + // checking error types and translating cthe error message + let available = balance / SATS; + let availableStr = available.toFixed(4); + let err = new Error( + `requested to send '${amountStr}' when only '${availableStr}' is available`, + ); + window.alert(err.message); + throw err; + } + + console.log('DEBUG Payment Address:', address); + console.log('DEBUG Available coins:', utxos?.length || inputs?.length); + console.log('DEBUG Available balance:', balance); + console.log('DEBUG Amount:', amount); + + let output = { satoshis, address }; + let draftTx = dashTx.legacy.draftSingleOutput({ utxos, inputs, output }); + console.log('DEBUG draftTx', draftTx); + + let changeOutput = draftTx.outputs[1]; + if (changeOutput) { + let address = changeAddrs.shift(); + changeOutput.address = address; + } + + // See https://github.com/dashhive/DashTx.js/pull/77 + for (let input of draftTx.inputs) { + let addressInfo = keysMap[input.address]; + Object.assign(input, { + publicKey: addressInfo.publicKey, + pubKeyHash: addressInfo.pubKeyHash, + }); + } + for (let output of draftTx.outputs) { + if (output.pubKeyHash) { + continue; + } + if (!output.address) { + let err = new Error(`output is missing 'address' and 'pubKeyHash'`); + window.alert(err.message); + throw err; + } + let pkhBytes = await DashKeys.addrToPkh(output.address, { + version: network, + }); + Object.assign(output, { + pubKeyHash: DashKeys.utils.bytesToHex(pkhBytes), + }); + } + + draftTx.inputs.sort(DashTx.sortInputs); + draftTx.outputs.sort(DashTx.sortOutputs); + amount = output.satoshis / SATS; + + $('[data-id=send-dust]').textContent = draftTx.feeTarget; + $('[data-id=send-amount]').textContent = amount.toFixed(8); + + let tx = await dashTx.legacy.finalizePresorted(draftTx); + console.log('DEBUG signed tx', tx); + { + let amountStr = amount.toFixed(4); + let confirmed = window.confirm(`Really send ${amountStr} to ${address}?`); + if (!confirmed) { + return; + } + } + void (await rpc('sendrawtransaction', tx.transaction)); + + let updatedAddrs = []; + for (let input of tx.inputs) { + updatedAddrs.push(input.address); + let knownSpent = spentAddrs.includes(input.address); + if (!knownSpent) { + spentAddrs.push(input.address); + } + removeElement(addresses, input.address); + removeElement(receiveAddrs, input.address); + removeElement(changeAddrs, input.address); + delete deltasMap[input.address]; + dbSet(input.address, null); + } + for (let output of tx.outputs) { + updatedAddrs.push(output.address); + removeElement(addresses, output.address); + removeElement(receiveAddrs, output.address); + removeElement(changeAddrs, output.address); + delete deltasMap[output.address]; + dbSet(output.address, null); + } + + await updateDeltas(updatedAddrs); + renderAddresses(); + renderCoins(); + }; + + function renderAddresses() { + $('[data-id=spent-count]').textContent = spentAddrs.length; + $('[data-id=spent]').textContent = spentAddrs.join('\n'); + $('[data-id=receive-addresses]').textContent = receiveAddrs.join('\n'); + $('[data-id=change-addresses]').textContent = changeAddrs.join('\n'); + } + + function selectCoin(address, txid, index) { + let info = deltasMap[address]; + if (!info) { + let err = new Error(`coins for '${address}' disappeared`); + window.alert(err.message); + throw err; + } + for (let delta of info.deltas) { + if (delta.txid !== txid) { + continue; + } + if (delta.index !== index) { + continue; + } + return delta; + } + } + + async function init() { + let phrases = dbGet('wallet-phrases', []); + let primaryPhrase = phrases[0]; + if (!primaryPhrase) { + primaryPhrase = await DashPhrase.generate(128); + dbSet('wallet-phrases', [primaryPhrase]); + } + + let primarySalt = ''; + let primarySeedBytes = await DashPhrase.toSeed(primaryPhrase, primarySalt); + let primarySeedHex = DashKeys.utils.bytesToHex(primarySeedBytes); + $('[data-id=wallet-phrase]').value = primaryPhrase; + $('[data-id=wallet-seed]').innerText = primarySeedHex; + + let accountIndex = 0; + let coinType = 5; // DASH + let versions = DashHd.MAINNET; + if (network === `testnet`) { + coinType = 1; // testnet (for all coins) + versions = DashHd.TESTNET; + } + $('[data-id=wallet-account]').value = `m/44'/${coinType}'/${accountIndex}'`; + + let walletId; + let xprvReceiveKey; + let xprvChangeKey; + { + let walletKey = await DashHd.fromSeed(primarySeedBytes); + walletId = await DashHd.toId(walletKey); + + let accountKey = await walletKey.deriveAccount(0, { + purpose: 44, // BIP-44 (default) + coinType: coinType, + versions: versions, + }); + xprvReceiveKey = await accountKey.deriveXKey(DashHd.RECEIVE); + xprvChangeKey = await accountKey.deriveXKey(DashHd.CHANGE); + } + + let previousIndex = 0; + let last = previousIndex + 50; + for (let i = previousIndex; i < last; i += 1) { + let failed; + try { + let receiveKey = await xprvReceiveKey.deriveAddress(i); // xprvKey from step 2 + await addKey(receiveKey, DashHd.RECEIVE, i); + } catch (e) { + failed = true; + } + try { + let changeKey = await xprvChangeKey.deriveAddress(i); // xprvKey from step 2 + addKey(changeKey, DashHd.CHANGE, i); + } catch (e) { + failed = true; + } + if (failed) { + // to make up for skipping on error + last += 1; + } + } + + async function addKey(key, usage, i) { + let wif = await DashHd.toWif(key.privateKey, { version: 'testnet' }); + let address = await DashHd.toAddr(key.publicKey, { + version: 'testnet', + }); + let hdpath = `m/44'/${coinType}'/${accountIndex}'/${usage}`; // accountIndex from step 2 + + addresses.push(address); + if (usage === DashHd.RECEIVE) { + receiveAddrs.push(address); + } else if (usage === DashHd.CHANGE) { + changeAddrs.push(address); + } else { + let err = new Error(`unknown usage '${usage}'`); + window.alert(err.message); + throw err; + } + + // note: pkh is necessary here because 'getaddressutxos' is unreliable + // and neither 'getaddressdeltas' nor 'getaddressmempool' have 'script' + let pkhBytes = await DashKeys.pubkeyToPkh(key.publicKey); + keysMap[address] = { + walletId: walletId, + index: i, + hdpath: hdpath, // useful for multi-account indexing + address: address, // XrZJJfEKRNobcuwWKTD3bDu8ou7XSWPbc9 + wif: wif, // XCGKuZcKDjNhx8DaNKK4xwMMNzspaoToT6CafJAbBfQTi57buhLK + key: key, + publicKey: DashKeys.utils.bytesToHex(key.publicKey), + pubKeyHash: DashKeys.utils.bytesToHex(pkhBytes), + }; + } + + await updateDeltas(addresses); + renderAddresses(); + + $('body').removeAttribute('hidden'); + renderCoins(); + } + + async function updateDeltas(addrs) { + for (let address of addrs) { + let info = dbGet(address); + let isSpent = info && info.deltas?.length && !info.balance; + if (!isSpent) { + continue; // used address (only check on manual sync) + } + + let knownSpent = spentAddrs.includes(address); + if (!knownSpent) { + spentAddrs.push(address); + } + removeElement(addrs, info.address); + removeElement(addresses, info.address); + removeElement(receiveAddrs, info.address); + removeElement(changeAddrs, info.address); + } + + let deltaLists = await Promise.all([ + // See + // - + // - + await rpc('getaddressdeltas', { addresses: addrs }), + // TODO check for proof of instantsend / acceptance + await rpc('getaddressmempool', { addresses: addrs }), + ]); + for (let deltaList of deltaLists) { + for (let delta of deltaList) { + console.log('DEBUG delta', delta); + removeElement(addrs, delta.address); + removeElement(addresses, delta.address); + removeElement(receiveAddrs, delta.address); + removeElement(changeAddrs, delta.address); + if (!deltasMap[delta.address]) { + deltasMap[delta.address] = { balance: 0, deltas: [] }; + } + deltasMap[delta.address].deltas.push(delta); + deltasMap[delta.address].balance += delta.satoshis; + } + } + } + + function renderCoins() { + let totalBalance = 0; + //let balances = []; + let addrs = Object.keys(deltasMap); + let elementStrs = []; + let template = $('[data-id=coin-row-tmpl]').content; + for (let addr of addrs) { + let info = deltasMap[addr]; + console.log('DEBUG delta info', info); + dbSet(addr, info); + if (info.balance === 0) { + continue; + } + totalBalance += info.balance; + //let amount = delta.balance / SATS; + //let amountStr = amount.toFixed(8); + // balances.push(`${addr}: ${info.deltas.length}: ${amountStr}`); + + for (let delta of info.deltas) { + let amount = delta.satoshis / SATS; + Object.assign(delta, { amount: amount }); + + let clone = document.importNode(template, true); + $('[data-name=coin]', clone).value = [ + delta.address, + delta.txid, + delta.index, + ].join(','); + $('[data-name=address]', clone).textContent = delta.address; + $('[data-name=amount]', clone).textContent = delta.amount.toFixed(4); + $('[data-name=txid]', clone).textContent = delta.txid; + $('[data-name=output-index]', clone).textContent = delta.index; + + elementStrs.push(clone.firstElementChild.outerHTML); + //tableBody.appendChild(clone); + } + } + + let totalAmount = totalBalance / SATS; + $('[data-id=total-balance]').innerText = totalAmount.toFixed(4); + + let tableBody = $('[data-id=coins-table]'); + tableBody.textContent = ''; + tableBody.insertAdjacentHTML('beforeend', elementStrs.join('\n')); + //$('[data-id=balances]').innerText = balances.join('\n'); + + if (totalBalance < MIN_BALANCE) { + setTimeout(function () { + window.alert( + 'Error: Balance too low. Please fill up at CN 💸 and/or DCG 💸.', + ); + }, 300); + } + } + + async function main() { + if (network === `testnet`) { + let $testnets = $$('[data-network=testnet]'); + for (let $testnet of $testnets) { + $testnet.removeAttribute('hidden'); + } + } + + await init(); + } + + main().catch(function (err) { + console.error(`Error in main:`, err); + }); +})(); From 4df3a17dfeee018d35bc1c6e868771cd45314eb3 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 14 Aug 2024 14:47:19 -0600 Subject: [PATCH 30/55] feat: add cash drawer control --- public/index.html | 144 +++++++++++++++++++++++++++++++++++++++++++ public/wallet-app.js | 91 +++++++++++++++++++++++++++ 2 files changed, 235 insertions(+) diff --git a/public/index.html b/public/index.html index 3a4dc75..8be7fa6 100644 --- a/public/index.html +++ b/public/index.html @@ -239,6 +239,150 @@

Digital Cash Wallet

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DenomPriorityHaveWantNeed
10.000 + + 0 + + 0
1.000 + + 0 + + 0
0.100 + + 0 + + 0
0.010 + + 0 + + 0
0.001 + + 0 + + 0

diff --git a/public/wallet-app.js b/public/wallet-app.js index 4d4927d..f7ce103 100644 --- a/public/wallet-app.js +++ b/public/wallet-app.js @@ -438,6 +438,96 @@ renderCoins(); } + let defaultCjSlots = [ + { + denom: 10000100000, + priority: 1, + have: 0, + want: 2, + need: 0, + }, + { + denom: 100001000, + priority: 10, + have: 0, + want: 10, + need: 0, + }, + { + denom: 10000100, + priority: 10, + have: 0, + want: 50, + need: 0, + }, + { + denom: 1000010, + priority: 1, + have: 0, + want: 20, + need: 0, + }, + { + denom: 100001, + priority: 0, + have: 0, + want: 5, + need: 0, + }, + ]; + function getCashDrawer() { + let slots = dbGet('cash-drawer-control', []); + if (!slots.length) { + slots = defaultCjSlots.slice(0); + dbSet('cash-drawer-control', slots); + } + return slots; + } + window.syncCashDrawer = function (event) { + console.log('DEBUG syncCashDrawer'); + let isDirty = false; + + let slots = getCashDrawer(); + for (let slot of slots) { + let $row = $(`[data-denom="${slot.denom}"]`); + console.log('DEBUG syncCashDrawer slot', slot, $row); + + let priorityStr = $('[name=priority]', $row).value; + if (priorityStr) { + let priority = parseFloat(priorityStr); + if (slot.priority !== priority) { + console.log('DEBUG update priority', slot.priority, priority); + isDirty = true; + slot.priority = priority; + } + } + + let wantStr = $('[name=want]', $row).value; + if (wantStr) { + let want = parseFloat(wantStr); + if (slot.want !== want) { + console.log('DEBUG update priority', slot.want, want); + isDirty = true; + slot.want = want; + } + } + } + + if (isDirty) { + dbSet('cash-drawer-control', slots); + } + + return true; + }; + function renderCashDrawer() { + let slots = getCashDrawer(); + for (let slot of slots) { + let $row = $(`[data-denom="${slot.denom}"]`); + $('[name=priority]', $row).value = slot.priority; + $('[name=want]', $row).value = slot.want; + } + } + async function updateDeltas(addrs) { for (let address of addrs) { let info = dbGet(address); @@ -544,6 +634,7 @@ } await init(); + renderCashDrawer(); } main().catch(function (err) { From 77de585b03783f6b0e4e67ece6523abb1dd3280c Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 14 Aug 2024 15:30:19 -0600 Subject: [PATCH 31/55] feat: find denominated coins in wallet --- public/index.html | 33 ++++++++++++++-------- public/wallet-app.js | 67 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 82 insertions(+), 18 deletions(-) diff --git a/public/index.html b/public/index.html index 8be7fa6..b4f84be 100644 --- a/public/index.html +++ b/public/index.html @@ -10,6 +10,7 @@ + @@ -474,9 +475,14 @@

Digital Cash Wallet

- +
+ + +

diff --git a/public/wallet-app.js b/public/wallet-app.js index b49a0e1..96d5fff 100644 --- a/public/wallet-app.js +++ b/public/wallet-app.js @@ -289,10 +289,37 @@ message = memo; memo = null; } - let satoshis = 0; + let burn = 0; msg = memo || message; - let outputs = [{ satoshis, memo, message }]; + let signedTx = await App._signMemo({ burn, memo, message }); + { + let confirmed = window.confirm( + `Really send '${memoEncoding}' memo '${msg}'?`, + ); + if (!confirmed) { + return; + } + } + let txid = await rpc('sendrawtransaction', signedTx.transaction); + $('[data-id=memo-txid]').textContent = txid; + let link = `${rpcExplorer}#?method=getrawtransaction¶ms=["${txid}",1]&submit`; + $('[data-id=memo-link]').textContent = link; + $('[data-id=memo-link]').href = link; + void (await commitWalletTx(signedTx)); + }; + + App._signMemo = async function ({ + burn = 0, + memo = null, + message = null, + collateral = 0, + }) { + let satoshis = burn; + satoshis += collateral; // temporary, for fee calculations only + + let memoOutput = { satoshis, memo, message }; + let outputs = [memoOutput]; let changeOutput = { address: '', pubKeyHash: '', @@ -301,53 +328,40 @@ }; let utxos = getAllUtxos({ denom: false }); - let txInfo = await DashTx.createLegacyTx(utxos, outputs, changeOutput); + let txInfo = DashTx.createLegacyTx(utxos, outputs, changeOutput); if (txInfo.changeIndex >= 0) { let realChange = txInfo.outputs[txInfo.changeIndex]; - // TODO reserve address realChange.address = changeAddrs.shift(); let pkhBytes = await DashKeys.addrToPkh(realChange.address, { version: network, }); realChange.pubKeyHash = DashKeys.utils.bytesToHex(pkhBytes); } + memoOutput.satoshis -= collateral; // adjusting for fee - let signedTx = await dashTx.hashAndSignAll(txInfo); - { - let confirmed = window.confirm( - `Really send '${memoEncoding}' memo '${msg}'?`, - ); - if (!confirmed) { - return; - } + let now = Date.now(); + for (let input of txInfo.inputs) { + input.reserved = now; + } + for (let output of txInfo.outputs) { + output.reserved = now; } - let txid = await rpc('sendrawtransaction', signedTx.transaction); - $('[data-id=memo-txid]').textContent = txid; - let link = `${rpcExplorer}#?method=getrawtransaction¶ms=["${txid}",1]&submit`; - $('[data-id=memo-link]').textContent = link; - $('[data-id=memo-link]').href = link; - void (await commitWalletTx(signedTx)); - }; - App.sendCollateral = async function (event) { - // at least the collateral amount - let dustF = Math.random() * DashJoin.COLLATERAL; - dustF = dustF / 10; - dustF += DashJoin.COLLATERAL; + txInfo.inputs.sort(DashTx.sortInputs); + txInfo.outputs.sort(DashTx.sortOutputs); - let dust = Math.floor(dustF); - let utxos = getAllUtxos(); - let memo = { satoshis: dust, memo: '' }; - let draft = await draftWalletTx(utxos, null, memo); - console.log('draftTx'); - console.log(draft); - draft.tx.outputs[0].satoshis = 0; - draft.tx.feeTarget += dust; - - draft.tx.inputs.sort(DashTx.sortInputs); - draft.tx.outputs.sort(DashTx.sortOutputs); - let signedTx = await dashTx.legacy.finalizePresorted(draft.tx); - console.log(signedTx); + let signedTx = await dashTx.hashAndSignAll(txInfo); + return signedTx; + }; + + App._signCollateral = async function (collateral = DashJoin.MIN_COLLATERAL) { + let signedTx = App._signMemo({ + burn: 0, + memo: '', + message: null, + collateral: DashJoin.MIN_COLLATERAL, + }); + return signedTx; }; async function draftWalletTx(utxos, inputs, output) { @@ -967,8 +981,8 @@ } function siftDenoms() { - if (!denomsMap[DashJoin.COLLATERAL]) { - denomsMap[DashJoin.COLLATERAL] = {}; + if (!denomsMap[DashJoin.MIN_COLLATERAL]) { + denomsMap[DashJoin.MIN_COLLATERAL] = {}; } for (let denom of DashJoin.DENOMS) { if (!denomsMap[denom]) { @@ -986,12 +1000,12 @@ for (let coin of info.deltas) { let denom = DashJoin.getDenom(coin.satoshis); if (!denom) { - let halfCollateral = DashJoin.COLLATERAL / 2; + let halfCollateral = DashJoin.MIN_COLLATERAL / 2; let fitsCollateral = coin.satoshis >= halfCollateral && coin.satoshis < DashJoin.DENOM_LOWEST; if (fitsCollateral) { - denomsMap[DashJoin.COLLATERAL][coin.address] = coin; + denomsMap[DashJoin.MIN_COLLATERAL][coin.address] = coin; } continue; } @@ -1181,6 +1195,7 @@ inputs: signedInputs, }); p2p.send(dssBytes); + void (await evstream.once('dsc')); } return dsfTxRequest; @@ -1280,23 +1295,67 @@ App.peers = {}; void (await connectToPeer(App._evonode, App._chaininfo.blocks)); - - // collateral, denominated - // let collateralTxInfo = await getCollateralTx(); - // // let keys = await getPrivateKeys(collateralTxInfo.inputs); - // // let txInfoSigned = await dashTx.hashAndSignAll(collateralTxInfo, keys); - // let txInfoSigned = await dashTx.hashAndSignAll(collateralTxInfo); - // let collateralTx = DashTx.utils.hexToBytes(txInfoSigned.transaction); - - // await createCoinJoinSession( - // App.evonode, - // // inputs, // [{address, txid, pubKeyHash, ...getPrivateKeyInfo }] - // // outputs, // [{ pubKeyHash, satoshis }] - // // dsaCollateralTx, // any tx with fee >= 0.00010000 - // // dsiCollateralTx, // any tx with fee >= 0.00010000 - // ); } + App.createCoinJoinSession = async function () { + let $coins = $$('[data-name=coin]:checked'); + if (!$coins.length) { + let msg = + 'Use the Coins table to select which coins to include in the CoinJoin session.'; + window.alert(msg); + return; + } + + let inputs = []; + let outputs = []; + let denom; + for (let $coin of $coins) { + let [address, txid, indexStr] = $coin.value.split(','); + let index = parseInt(indexStr, 10); + let coin = selectCoin(address, txid, index); + coin.denom = DashJoin.getDenom(coin.satoshis); + if (!coin.denom) { + let msg = 'CoinJoin requires 10s-Denominated coins, shown in BOLD.'; + window.alert(msg); + return; + } + if (!denom) { + denom = coin.denom; + } + if (coin.denom !== denom) { + let msg = + 'CoinJoin requires all coins to be of the same denomination (ex: three 0.01, or two 1.0, but not a mix of the two).'; + window.alert(msg); + return; + } + Object.assign(coin, { outputIndex: coin.index }); + inputs.push(coin); + + let output = { + address: receiveAddrs.shift(), + satoshis: denom, + pubKeyHash: '', + }; + let pkhBytes = await DashKeys.addrToPkh(output.address, { + version: network, + }); + output.pubKeyHash = DashKeys.utils.bytesToHex(pkhBytes); + outputs.push(output); + } + + let collateralTxes = [ + await App._signCollateral(DashJoin.MIN_COLLATERAL), + await App._signCollateral(DashJoin.MIN_COLLATERAL), + ]; + + await createCoinJoinSession( + App._evonode, + inputs, // [{address, txid, pubKeyHash, ...getPrivateKeyInfo }] + outputs, // [{ pubKeyHash, satoshis }] + collateralTxes, // any tx with fee >= 0.00010000 + ); + }; + main().catch(function (err) { console.error(`Error in main:`, err); }); From 02930df2e461f4ab70d71fcdc7f18bf520f48d88 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 17 Aug 2024 05:25:12 -0600 Subject: [PATCH 50/55] fix: update DashTx to latest version for bugfixes --- public/package-lock.json | 8 ++++---- public/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/public/package-lock.json b/public/package-lock.json index 9328996..b9d3b57 100644 --- a/public/package-lock.json +++ b/public/package-lock.json @@ -13,7 +13,7 @@ "dashhd": "^3.3.3", "dashkeys": "^1.1.5", "dashphrase": "^1.4.0", - "dashtx": "^0.18.2" + "dashtx": "^0.19.1" } }, "node_modules/@dashincubator/secp256k1": { @@ -41,9 +41,9 @@ "license": "SEE LICENSE IN LICENSE" }, "node_modules/dashtx": { - "version": "0.18.2", - "resolved": "https://registry.npmjs.org/dashtx/-/dashtx-0.18.2.tgz", - "integrity": "sha512-Q8X2Xkw2mtkd3FTtghSHi85AS/e7i98AVd8kHPL8PRLQMxYMPc3f3mvtVphM26ST+Fp2WNEjQZMIBgMWeZsECg==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/dashtx/-/dashtx-0.19.1.tgz", + "integrity": "sha512-mPiZQxw05pSrI3zFqLk610FKlcG4PHGdKHp5Eul52M4G/n33+4JgeCJBEupRW8wBaY4GzTdKl7tZVNmmOJoLMA==", "license": "SEE LICENSE IN LICENSE", "bin": { "dashtx-inspect": "bin/inspect.js" diff --git a/public/package.json b/public/package.json index fd121f2..fd25fc8 100644 --- a/public/package.json +++ b/public/package.json @@ -28,6 +28,6 @@ "dashhd": "^3.3.3", "dashkeys": "^1.1.5", "dashphrase": "^1.4.0", - "dashtx": "^0.18.2" + "dashtx": "^0.19.1" } } From 34b6334994b1b8cb6c7aa1ff2d2ed32b2a53e767 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 17 Aug 2024 05:53:39 -0600 Subject: [PATCH 51/55] fix+test: DataView requires subarray offset --- public/dashjoin.js | 8 ++++---- public/dashp2p.js | 4 ++-- tests/dsa.js | 13 ++++++++++--- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/public/dashjoin.js b/public/dashjoin.js index 9146fa3..77cdf8d 100644 --- a/public/dashjoin.js +++ b/public/dashjoin.js @@ -2,7 +2,7 @@ var DashJoin = ('object' === typeof module && exports) || {}; (function (window, DashJoin) { 'use strict'; - let DashP2P = window.DashP2P || require('dashp2p'); + let DashP2P = window.DashP2P || require('./dashp2p.js'); let DashTx = window.DashTx || require('dashtx'); const DV_LITTLE_ENDIAN = true; @@ -139,7 +139,7 @@ var DashJoin = ('object' === typeof module && exports) || {}; ); } - let dv = new DataView(payload.buffer); + let dv = new DataView(payload.buffer, payload.byteOffset); let offset = 0; dv.setUint32(offset, denomMask, DV_LITTLE_ENDIAN); @@ -259,7 +259,7 @@ var DashJoin = ('object' === typeof module && exports) || {}; let msg = `developer error: 'dsq' must be ${Sizes.DSQ} bytes, but received ${bytes.length}`; throw new Error(msg); } - let dv = new DataView(bytes.buffer); + let dv = new DataView(bytes.buffer, bytes.byteOffset); let offset = 0; @@ -359,7 +359,7 @@ var DashJoin = ('object' === typeof module && exports) || {}; let msg = `developer error: a 'dssu' message is 16 bytes, but got ${bytes.length}`; throw new Error(msg); } - let dv = new DataView(bytes.buffer); + let dv = new DataView(bytes.buffer, bytes.byteOffset); let offset = 0; let session_id = dv.getUint32(offset, DV_LITTLE_ENDIAN); diff --git a/public/dashp2p.js b/public/dashp2p.js index eafd3e8..e087c37 100644 --- a/public/dashp2p.js +++ b/public/dashp2p.js @@ -811,7 +811,7 @@ var DashP2P = ('object' === typeof module && exports) || {}; let msg = `developer error: header should be ${Sizes.HEADER}+ bytes (optional payload), not ${bytes.length}`; throw new Error(msg); } - let dv = new DataView(bytes.buffer); + let dv = new DataView(bytes.buffer, bytes.byteOffset); let index = 0; @@ -1186,7 +1186,7 @@ var DashP2P = ('object' === typeof module && exports) || {}; let padded = new Uint8Array((bytes.length + 9 + 63) & ~63); padded.set(bytes); padded[bytes.length] = 0x80; - let dv = new DataView(padded.buffer); + let dv = new DataView(padded.buffer, padded.byteOffset); dv.setUint32(padded.length - 4, bytes.length << 3, false); let w = new Uint32Array(64); diff --git a/tests/dsa.js b/tests/dsa.js index 745c412..233fb45 100644 --- a/tests/dsa.js +++ b/tests/dsa.js @@ -1,6 +1,6 @@ 'use strict'; -let Packer = require('../packer.js'); +let DashJoin = require('../public/dashjoin.js'); // TODO copy .utils.bytesToHex rather than depend on it let DashKeys = require('dashkeys'); @@ -30,7 +30,7 @@ let collateralTxHex = function test() { let expectedHex = `${regtest}${command}${payloadSize}${checksum}${denomMask}${collateralTxHex}`; let collateralTx = DashKeys.utils.hexToBytes(collateralTxHex); - let message = Packer.packAllow({ network, denomination, collateralTx }); + let message = DashJoin.packers.dsa({ network, denomination, collateralTx }); let messageHex = DashKeys.utils.bytesToHex(message); if (expectedHex.length !== messageHex.length) { let length = expectedHex.length / 2; @@ -39,13 +39,20 @@ function test() { ); } if (expectedHex !== messageHex) { + console.log(); + console.log(`EXPECTED: (${expectedHex.length})`); + console.log(expectedHex); + console.log(); + console.log(`ACTUAL: (${messageHex.length})`); + console.log(messageHex); + console.log(); throw new Error( 'bytes of dsa (allow / join request) messages do not match', ); } console.info( - `PASS: Packer.packAllow({ network, denomination, collateralTx }) matches`, + `PASS: DashJoin.packers.dsa({ network, denomination, collateralTx }) matches`, ); } From f8357eb2207067dd0460a1939e834ec430cd37db Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 17 Aug 2024 06:19:43 -0600 Subject: [PATCH 52/55] fix+test: packer sizes are exposed differently --- public/dashjoin.js | 1 + tests/dsq.js | 15 ++++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/public/dashjoin.js b/public/dashjoin.js index 77cdf8d..0133b2a 100644 --- a/public/dashjoin.js +++ b/public/dashjoin.js @@ -458,6 +458,7 @@ var DashJoin = ('object' === typeof module && exports) || {}; DashJoin.packers = Packers; DashJoin.parsers = Parsers; + DashJoin.sizes = Sizes; DashJoin.utils = Utils; //@ts-ignore diff --git a/tests/dsq.js b/tests/dsq.js index 9cc9746..6e08730 100644 --- a/tests/dsq.js +++ b/tests/dsq.js @@ -4,12 +4,13 @@ let Assert = require('node:assert/strict'); let Fs = require('node:fs/promises'); let Path = require('node:path'); -let Parser = require('../parser.js'); +let DashP2P = require('../public/dashp2p.js'); +let DashJoin = require('../public/dashjoin.js'); // TODO copy .utils.bytesToHex rather than depend on it let DashKeys = require('dashkeys'); async function test() { - let totalSize = Parser.HEADER_SIZE + Parser.DSQ_SIZE; + let totalSize = DashP2P.sizes.HEADER + DashJoin.sizes.DSQ; let fixtureDsqBytes = await readFixtureHex('dsq'); let fixtureDsqJson = require('../fixtures/dsq.json'); @@ -18,21 +19,21 @@ async function test() { throw new Error(msg); } - let header = Parser.parseHeader(fixtureDsqBytes); + let header = DashP2P.parsers.header(fixtureDsqBytes); if (header.command !== 'dsq') { throw new Error('sanity fail: loaded incorrect fixture'); } - if (header.payloadSize !== Parser.DSQ_SIZE) { + if (header.payloadSize !== DashJoin.sizes.DSQ) { throw new Error('sanity fail: wrong payload size in header'); } - let payload = fixtureDsqBytes.subarray(Parser.HEADER_SIZE); - if (payload.length !== Parser.DSQ_SIZE) { + let payload = fixtureDsqBytes.subarray(DashP2P.sizes.HEADER); + if (payload.length !== DashJoin.sizes.DSQ) { throw new Error('sanity fail: payload has trailing bytes'); } - let dsq = Parser.parseDsq(payload); + let dsq = DashJoin.parsers.dsq(payload); // JSON-ify dsq.protxhash = DashKeys.utils.bytesToHex(dsq.protxhash_bytes); From 309ab66c5c8bbb56d91231b895c9766cf2552beb Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 17 Aug 2024 06:31:29 -0600 Subject: [PATCH 53/55] WIP: final countdown - just some bad byte offsets --- public/dashjoin.js | 1 + public/wallet-app.js | 26 +++++++++++++++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/public/dashjoin.js b/public/dashjoin.js index 0133b2a..f478ccf 100644 --- a/public/dashjoin.js +++ b/public/dashjoin.js @@ -396,6 +396,7 @@ var DashJoin = ('object' === typeof module && exports) || {}; let transactionUnsigned = bytes.subarray(offset); let transaction_unsigned = DashTx.utils.bytesToHex(transactionUnsigned); + console.log('DEBUG [[dsf]] tx', transaction_unsigned); let txRequest = DashTx.parseUnknown(transaction_unsigned); let dsfTxRequest = { diff --git a/public/wallet-app.js b/public/wallet-app.js index 96d5fff..a808a6a 100644 --- a/public/wallet-app.js +++ b/public/wallet-app.js @@ -351,17 +351,20 @@ txInfo.outputs.sort(DashTx.sortOutputs); let signedTx = await dashTx.hashAndSignAll(txInfo); + console.log('memo signed', signedTx); return signedTx; }; App._signCollateral = async function (collateral = DashJoin.MIN_COLLATERAL) { - let signedTx = App._signMemo({ + let signedTx = await App._signMemo({ burn: 0, memo: '', message: null, collateral: DashJoin.MIN_COLLATERAL, }); - return signedTx; + console.log('collat signed', signedTx); + let signedTxBytes = DashTx.utils.hexToBytes(signedTx.transaction); + return signedTxBytes; }; async function draftWalletTx(utxos, inputs, output) { @@ -1065,13 +1068,20 @@ hostname: evonode.hostname, port: evonode.port, // dsq status + denomination: dsq.denomination, ready: dsq.ready, timestamp: dsq.timestamp, timestamp_unix: dsq.timestamp_unix, }; App.coinjoinQueues[dsq.denomination][evonode.host] = dsqStatus; - console.log('%c[[DSQ]]', 'color: #bada55', dsqStatus); + console.log( + '%c[[DSQ]]', + 'color: #bada55', + dsqStatus.denomination, + dsqStatus.ready, + dsqStatus.host, + ); }); function cleanup() { @@ -1131,12 +1141,14 @@ { let collateralTx = collateralTxes.shift(); - let dsaBytes = DashJoin.packers.dsa({ + let dsa = { network, message, denomination, collateralTx, - }); + }; + let dsaBytes = DashJoin.packers.dsa(dsa); + console.log('DEBUG dsa, dsaBytes', dsa, dsaBytes); p2p.send(dsaBytes); for (;;) { let msg = await evstream.once(); @@ -1175,8 +1187,8 @@ p2p.send(dsiBytes); let msg = await evstream.once('dsf'); let dsf = DashJoin.parsers.dsf(msg.payload); + console.log('DEBUG dsf', dsf); - dsfTxRequest = DashTx.parseUnknown(dsf.transaction_unsigned); makeSelectedInputsSignable(dsfTxRequest, inputs); let txSigned = await dashTx.hashAndSignAll(dsfTxRequest); @@ -1281,7 +1293,7 @@ App._chaininfo = await rpc('getblockchaininfo'); console.log(App._rawmnlist); App._evonodes = DashJoin.utils._evonodeMapToList(App._rawmnlist); - App._evonode = App._evonodes.at(-1); + App._evonode = App._evonodes.at(-13); console.info('[info] chosen evonode:'); console.log(JSON.stringify(App._evonode, null, 2)); From 19b856b8474e319364d74d40b5135e795a93d67d Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 17 Aug 2024 06:47:39 -0600 Subject: [PATCH 54/55] fix+test: expose raw transaction_unsigned --- tests/dsf.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/dsf.js b/tests/dsf.js index 2779413..4f671bb 100644 --- a/tests/dsf.js +++ b/tests/dsf.js @@ -1,10 +1,11 @@ 'use strict'; -let Assert = require('node:assert/strict'); +// let Assert = require('node:assert/strict'); let Fs = require('node:fs/promises'); let Path = require('node:path'); -let Parser = require('../parser.js'); +let DashP2P = require('../public/dashp2p.js'); +let DashJoin = require('../public/dashjoin.js'); // TODO copy .utils.bytesToHex rather than depend on it let DashKeys = require('dashkeys'); @@ -12,29 +13,29 @@ async function test() { let fixtureDsfBytes = await readFixtureHex('dsf'); // let fixtureDsqJson = require('../fixtures/dsf.json'); - let header = Parser.parseHeader(fixtureDsfBytes); + let header = DashP2P.parsers.header(fixtureDsfBytes); if (header.command !== 'dsf') { throw new Error( `sanity fail: should have loaded 'dsf' fixture, but got '${header.command}'`, ); } - let payload = fixtureDsfBytes.subarray(Parser.HEADER_SIZE); + let payload = fixtureDsfBytes.subarray(DashP2P.sizes.HEADER); // TODO verify // - a chosen subset of our offered inputs (e.g. we offer 9, but 3 are selected) // - that equally many of our offered outputs are selected (e.g. 3 = 3) // - that the satoshi values of our outputs match coin for coin - let dsf = Parser.parseDsf(payload); + let dsf = DashJoin.parsers.dsf(payload); console.log(new Date(), '[debug] dsf obj:', dsf); if ('string' !== typeof dsf.transaction_unsigned) { - throw new Error("'.transactionUnsigned' should exist as a hex string"); + throw new Error("'.transaction_unsigned' should exist as a hex string"); } let txLen = dsf.transaction_unsigned.length / 2; - let expectedLen = header.payloadSize - Parser.SESSION_ID_SIZE; + let expectedLen = header.payloadSize - DashJoin.sizes.SESSION_ID; console.log(new Date(), '[debug] dsf len:', txLen); if (txLen !== expectedLen) { throw new Error( - `expected '.transactionUnsigned' to represent ${header.payloadSize} bytes, but got ${txLen}`, + `expected '.transaction_unsigned' to represent ${header.payloadSize} bytes, but got ${txLen}`, ); } From dc7223c48778ab39b99e6fa129369a94cb217960 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 17 Aug 2024 07:42:55 -0600 Subject: [PATCH 55/55] feat: WOOT! got dsc with the new code. Huzzah! --- public/dashjoin.js | 2 ++ public/dashp2p.js | 12 +++++------ public/wallet-app.js | 49 ++++++++++++++++++++++++++++++-------------- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/public/dashjoin.js b/public/dashjoin.js index f478ccf..90d42d5 100644 --- a/public/dashjoin.js +++ b/public/dashjoin.js @@ -394,6 +394,7 @@ var DashJoin = ('object' === typeof module && exports) || {}; let session_id = DashTx.utils.bytesToHex(sessionId); offset += Sizes.SESSION_ID; + console.log('DEBUG [[dsf]] bytes', DashTx.utils.bytesToHex(bytes)); let transactionUnsigned = bytes.subarray(offset); let transaction_unsigned = DashTx.utils.bytesToHex(transactionUnsigned); console.log('DEBUG [[dsf]] tx', transaction_unsigned); @@ -405,6 +406,7 @@ var DashJoin = ('object' === typeof module && exports) || {}; inputs: txRequest.inputs, outputs: txRequest.outputs, locktime: txRequest.locktime, + transaction_unsigned: transaction_unsigned, }; return dsfTxRequest; }; diff --git a/public/dashp2p.js b/public/dashp2p.js index e087c37..c68e19c 100644 --- a/public/dashp2p.js +++ b/public/dashp2p.js @@ -148,8 +148,8 @@ var DashP2P = ('object' === typeof module && exports) || {}; void (await evstream.once('version')); console.log('%c[[version]] PROCESSED', 'color: red'); - void (await evstream.once('verack')); - console.log('%c[[verack]] PROCESSED', 'color: red'); + // void (await evstream.once('verack')); + // console.log('%c[[verack]] PROCESSED', 'color: red'); (async function () { for (;;) { @@ -551,7 +551,7 @@ var DashP2P = ('object' === typeof module && exports) || {}; addr_recv_port, // required to match addr_trans_services = [], addr_trans_ip = '127.0.0.1', - addr_trans_port = 65535, + addr_trans_port = Math.ceil(65535 * Math.random()), start_height, nonce = null, user_agent = null, @@ -998,9 +998,9 @@ var DashP2P = ('object' === typeof module && exports) || {}; } if (eventname) { - p.events = [eventname]; + p._events = [eventname]; } else if (defaultEvents?.length) { - p.events = defaultEvents; + p._events = defaultEvents; } else { let err = new Error( `call stream.createSubscriber(['*']) or conn.once('*') for default events`, @@ -1008,7 +1008,7 @@ var DashP2P = ('object' === typeof module && exports) || {}; Object.assign(err, { code: 'E_NO_EVENTS' }); throw err; } - console.log('%c[[RESUB]]', 'color: red; font-weight: bold;', p.events); + console.log('%c[[RESUB]]', 'color: red; font-weight: bold;', p._events); return await p._next(); }; diff --git a/public/wallet-app.js b/public/wallet-app.js index a808a6a..f66b164 100644 --- a/public/wallet-app.js +++ b/public/wallet-app.js @@ -43,8 +43,9 @@ // let address; let address = txInput.address; if (!address) { - let pkhBytes = DashKeys.utils.hexToBytes(txInput.pubKeyHash); - address = await DashKeys.pkhToAddr(pkhBytes, { version: network }); + return null; + // let pkhBytes = DashKeys.utils.hexToBytes(txInput.pubKeyHash); + // address = await DashKeys.pkhToAddr(pkhBytes, { version: network }); } let yourKeyData = keysMap[address]; @@ -116,26 +117,30 @@ let spendableAddrs = Object.keys(deltasMap); for (let address of spendableAddrs) { let info = deltasMap[address]; - if (info.balance === 0) { - continue; - } - for (let coin of info.deltas) { - if (coin.reserved > 0) { - continue; - } + info.balance = DashTx.sum(info.deltas); + for (let coin of info.deltas) { let addressInfo = keysMap[coin.address]; Object.assign(coin, { outputIndex: coin.index, denom: DashJoin.getDenom(coin.satoshis), + publicKey: addressInfo.publicKey, pubKeyHash: addressInfo.pubKeyHash, }); + + if (coin.reserved > 0) { + continue; + } + if (opts?.denom === false) { if (coin.denom) { continue; } } + if (info.balance === 0) { + break; + } utxos.push(coin); } } @@ -1084,7 +1089,8 @@ ); }); - function cleanup() { + function cleanup(err) { + console.error('WebSocket Error:', err); delete App.peers[evonode.host]; for (let denom of DashJoin.DENOMS) { delete App.coinjoinQueues[denom][evonode.host]; @@ -1186,8 +1192,9 @@ }); p2p.send(dsiBytes); let msg = await evstream.once('dsf'); - let dsf = DashJoin.parsers.dsf(msg.payload); - console.log('DEBUG dsf', dsf); + console.log('DEBUG dsf %c[[MSG]]', 'color: blue', msg); + let dsfTxRequest = DashJoin.parsers.dsf(msg.payload); + console.log('DEBUG dsf', dsfTxRequest, inputs); makeSelectedInputsSignable(dsfTxRequest, inputs); let txSigned = await dashTx.hashAndSignAll(dsfTxRequest); @@ -1231,6 +1238,8 @@ let sigHashType = DashTx.SIGHASH_ALL | DashTx.SIGHASH_ANYONECANPAY; //jshint ignore:line + console.log(sighashInput); + console.log(input); sighashInput.index = input.index; sighashInput.address = input.address; sighashInput.satoshis = input.satoshis; @@ -1275,6 +1284,8 @@ } } + App.peers = {}; + async function main() { if (network === `testnet`) { let $testnets = $$('[data-network=testnet]'); @@ -1293,8 +1304,17 @@ App._chaininfo = await rpc('getblockchaininfo'); console.log(App._rawmnlist); App._evonodes = DashJoin.utils._evonodeMapToList(App._rawmnlist); - App._evonode = App._evonodes.at(-13); - console.info('[info] chosen evonode:'); + // 35.166.18.166:19999 + let index = 5; + // let index = Math.floor(Math.random() * App._evonodes.length); + // App._evonode = App._evonodes[index]; + App._evonode = App._evonodes.at(index); + // App._evonode = { + // host: '35.166.18.166:19999', + // hostname: '35.166.18.166', + // port: '19999', + // }; + console.info('[info] chosen evonode:', index); console.log(JSON.stringify(App._evonode, null, 2)); App.coinjoinQueues = { @@ -1304,7 +1324,6 @@ 100001000: {}, // 1.00001000 1000010000: {}, // 10.00010000 }; - App.peers = {}; void (await connectToPeer(App._evonode, App._chaininfo.blocks)); }