From 8809422b3d2475a38c4c6614184c3396ed9f9538 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 13 Jul 2024 01:20:25 +0000 Subject: [PATCH] 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; +}