diff --git a/public/dashjoin.js b/public/dashjoin.js index 42d02a3..9146fa3 100644 --- a/public/dashjoin.js +++ b/public/dashjoin.js @@ -9,7 +9,7 @@ var DashJoin = ('object' === typeof module && exports) || {}; const DENOM_LOWEST = 100001; const PREDENOM_MIN = DENOM_LOWEST + 193; - const COLLATERAL = 10000; // DENOM_LOWEST / 10 + const MIN_COLLATERAL = 10000; // DENOM_LOWEST / 10 let STANDARD_DENOMINATIONS_MAP = { // 0.00100001 @@ -44,7 +44,7 @@ var DashJoin = ('object' === typeof module && exports) || {}; // const COINJOIN_ENTRY_MAX_SIZE = 2; // just for testing right now DashJoin.DENOM_LOWEST = DENOM_LOWEST; - DashJoin.COLLATERAL = COLLATERAL; + DashJoin.MIN_COLLATERAL = MIN_COLLATERAL; DashJoin.PREDENOM_MIN = PREDENOM_MIN; DashJoin.DENOMS = [ 100001, // 0.00100001 @@ -81,8 +81,15 @@ var DashJoin = ('object' === typeof module && exports) || {}; Sizes.READY = 1; // 1-byte bool Sizes.SIG = 97; - // Sizes.DSSU = 16; - // Sizes.SESSION_ID = 4; + Sizes.DSSU = 16; + Sizes.SESSION_ID = 4; + Sizes.MESSAGE_ID = 4; + + ///////////////////// + // // + // Packers // + // // + ///////////////////// /** * Turns on or off DSQ messages (necessary for CoinJoin, but off by default) @@ -223,8 +230,8 @@ var DashJoin = ('object' === typeof module && exports) || {}; const command = 'dss'; if (!inputs?.length) { - // TODO make better - throw new Error('you must provide some inputs'); + let msg = `'dss' should receive signed inputs as requested in 'dsi' and accepted in 'dsf', but got 0 inputs`; + throw new Error(msg); } let txInputsHex = DashTx.serializeInputs(inputs); @@ -238,6 +245,12 @@ var DashJoin = ('object' === typeof module && exports) || {}; return bytes; }; + ///////////////////// + // // + // Parsers // + // // + ///////////////////// + /** * @param {Uint8Array} bytes */ @@ -256,15 +269,9 @@ var DashJoin = ('object' === typeof module && exports) || {}; //@ts-ignore - correctness of denomination must be checked higher up let denomination = STANDARD_DENOMINATIONS_MAP[denomination_id]; - /** - * Grab the protxhash - */ let protxhash_bytes = bytes.subarray(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); @@ -272,9 +279,6 @@ var DashJoin = ('object' === typeof module && exports) || {}; let timestampDate = new Date(timestampMs); let timestamp = timestampDate.toISOString(); - /** - * Grab the fReady - */ let ready = bytes[offset] > 0x00; offset += Sizes.READY; @@ -295,8 +299,114 @@ var DashJoin = ('object' === typeof module && exports) || {}; return dsqMessage; }; - // Utils.hexToBytes = DashTx.utils.hexToBytes; - // Utils.bytesToHex = DashTx.utils.bytesToHex; + Parsers._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', + }; + + Parsers._DSSU_STATES = { + 0x00: 'IDLE', + 0x01: 'QUEUE', + 0x02: 'ACCEPTING_ENTRIES', + 0x03: 'SIGNING', + 0x04: 'ERROR', + 0x05: 'SUCCESS', + }; + + Parsers._DSSU_STATUSES = { + 0x00: 'REJECTED', + 0x01: 'ACCEPTED', + }; + + // TODO DSSU type + /** + * 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 + */ + + /** + * @param {Uint8Array} bytes + */ + Parsers.dssu = function (bytes) { + const STATE_SIZE = 4; + const STATUS_UPDATE_SIZE = 4; + + if (bytes.length !== Sizes.DSSU) { + let msg = `developer error: a 'dssu' message is 16 bytes, but got ${bytes.length}`; + throw new Error(msg); + } + let dv = new DataView(bytes.buffer); + let 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 += STATE_SIZE; + + let status_id = dv.getUint32(offset, DV_LITTLE_ENDIAN); + offset += STATUS_UPDATE_SIZE; + + let message_id = dv.getUint32(offset, DV_LITTLE_ENDIAN); + + let dssuMessage = { + session_id: session_id, + state_id: state_id, + state: Parsers._DSSU_STATES[state_id], + status_id: status_id, + status: Parsers._DSSU_STATUSES[status_id], + message_id: message_id, + message: Parsers._DSSU_MESSAGE_IDS[message_id], + }; + return dssuMessage; + }; + + /** + * @param {Uint8Array} bytes + */ + Parsers.dsf = function (bytes) { + let offset = 0; + let sessionId = bytes.subarray(offset, Sizes.SESSION_ID); + let session_id = DashTx.utils.bytesToHex(sessionId); + offset += Sizes.SESSION_ID; + + let transactionUnsigned = bytes.subarray(offset); + let transaction_unsigned = DashTx.utils.bytesToHex(transactionUnsigned); + + let txRequest = DashTx.parseUnknown(transaction_unsigned); + let dsfTxRequest = { + session_id: session_id, + version: txRequest.version, + inputs: txRequest.inputs, + outputs: txRequest.outputs, + locktime: txRequest.locktime, + }; + return dsfTxRequest; + }; Utils._evonodeMapToList = function (evonodesMap) { console.log('[debug] get evonode list...'); diff --git a/public/index.html b/public/index.html index fe8d083..bfe45a0 100644 --- a/public/index.html +++ b/public/index.html @@ -60,7 +60,8 @@ margin: 0; padding: 0; } - fieldset label { + fieldset label, + fieldset button { display: inline-block; } @@ -474,9 +475,14 @@

Digital Cash Wallet

- +
+ + +

diff --git a/public/wallet-app.js b/public/wallet-app.js index b49a0e1..96d5fff 100644 --- a/public/wallet-app.js +++ b/public/wallet-app.js @@ -289,10 +289,37 @@ message = memo; memo = null; } - let satoshis = 0; + let burn = 0; msg = memo || message; - let outputs = [{ satoshis, memo, message }]; + let signedTx = await App._signMemo({ burn, memo, message }); + { + let confirmed = window.confirm( + `Really send '${memoEncoding}' memo '${msg}'?`, + ); + if (!confirmed) { + return; + } + } + let txid = await rpc('sendrawtransaction', signedTx.transaction); + $('[data-id=memo-txid]').textContent = txid; + let link = `${rpcExplorer}#?method=getrawtransaction¶ms=["${txid}",1]&submit`; + $('[data-id=memo-link]').textContent = link; + $('[data-id=memo-link]').href = link; + void (await commitWalletTx(signedTx)); + }; + + App._signMemo = async function ({ + burn = 0, + memo = null, + message = null, + collateral = 0, + }) { + let satoshis = burn; + satoshis += collateral; // temporary, for fee calculations only + + let memoOutput = { satoshis, memo, message }; + let outputs = [memoOutput]; let changeOutput = { address: '', pubKeyHash: '', @@ -301,53 +328,40 @@ }; let utxos = getAllUtxos({ denom: false }); - let txInfo = await DashTx.createLegacyTx(utxos, outputs, changeOutput); + let txInfo = DashTx.createLegacyTx(utxos, outputs, changeOutput); if (txInfo.changeIndex >= 0) { let realChange = txInfo.outputs[txInfo.changeIndex]; - // TODO reserve address realChange.address = changeAddrs.shift(); let pkhBytes = await DashKeys.addrToPkh(realChange.address, { version: network, }); realChange.pubKeyHash = DashKeys.utils.bytesToHex(pkhBytes); } + memoOutput.satoshis -= collateral; // adjusting for fee - let signedTx = await dashTx.hashAndSignAll(txInfo); - { - let confirmed = window.confirm( - `Really send '${memoEncoding}' memo '${msg}'?`, - ); - if (!confirmed) { - return; - } + let now = Date.now(); + for (let input of txInfo.inputs) { + input.reserved = now; + } + for (let output of txInfo.outputs) { + output.reserved = now; } - let txid = await rpc('sendrawtransaction', signedTx.transaction); - $('[data-id=memo-txid]').textContent = txid; - let link = `${rpcExplorer}#?method=getrawtransaction¶ms=["${txid}",1]&submit`; - $('[data-id=memo-link]').textContent = link; - $('[data-id=memo-link]').href = link; - void (await commitWalletTx(signedTx)); - }; - App.sendCollateral = async function (event) { - // at least the collateral amount - let dustF = Math.random() * DashJoin.COLLATERAL; - dustF = dustF / 10; - dustF += DashJoin.COLLATERAL; + txInfo.inputs.sort(DashTx.sortInputs); + txInfo.outputs.sort(DashTx.sortOutputs); - let dust = Math.floor(dustF); - let utxos = getAllUtxos(); - let memo = { satoshis: dust, memo: '' }; - let draft = await draftWalletTx(utxos, null, memo); - console.log('draftTx'); - console.log(draft); - draft.tx.outputs[0].satoshis = 0; - draft.tx.feeTarget += dust; - - draft.tx.inputs.sort(DashTx.sortInputs); - draft.tx.outputs.sort(DashTx.sortOutputs); - let signedTx = await dashTx.legacy.finalizePresorted(draft.tx); - console.log(signedTx); + let signedTx = await dashTx.hashAndSignAll(txInfo); + return signedTx; + }; + + App._signCollateral = async function (collateral = DashJoin.MIN_COLLATERAL) { + let signedTx = App._signMemo({ + burn: 0, + memo: '', + message: null, + collateral: DashJoin.MIN_COLLATERAL, + }); + return signedTx; }; async function draftWalletTx(utxos, inputs, output) { @@ -967,8 +981,8 @@ } function siftDenoms() { - if (!denomsMap[DashJoin.COLLATERAL]) { - denomsMap[DashJoin.COLLATERAL] = {}; + if (!denomsMap[DashJoin.MIN_COLLATERAL]) { + denomsMap[DashJoin.MIN_COLLATERAL] = {}; } for (let denom of DashJoin.DENOMS) { if (!denomsMap[denom]) { @@ -986,12 +1000,12 @@ for (let coin of info.deltas) { let denom = DashJoin.getDenom(coin.satoshis); if (!denom) { - let halfCollateral = DashJoin.COLLATERAL / 2; + let halfCollateral = DashJoin.MIN_COLLATERAL / 2; let fitsCollateral = coin.satoshis >= halfCollateral && coin.satoshis < DashJoin.DENOM_LOWEST; if (fitsCollateral) { - denomsMap[DashJoin.COLLATERAL][coin.address] = coin; + denomsMap[DashJoin.MIN_COLLATERAL][coin.address] = coin; } continue; } @@ -1181,6 +1195,7 @@ inputs: signedInputs, }); p2p.send(dssBytes); + void (await evstream.once('dsc')); } return dsfTxRequest; @@ -1280,23 +1295,67 @@ App.peers = {}; void (await connectToPeer(App._evonode, App._chaininfo.blocks)); - - // collateral, denominated - // let collateralTxInfo = await getCollateralTx(); - // // let keys = await getPrivateKeys(collateralTxInfo.inputs); - // // let txInfoSigned = await dashTx.hashAndSignAll(collateralTxInfo, keys); - // let txInfoSigned = await dashTx.hashAndSignAll(collateralTxInfo); - // let collateralTx = DashTx.utils.hexToBytes(txInfoSigned.transaction); - - // await createCoinJoinSession( - // App.evonode, - // // inputs, // [{address, txid, pubKeyHash, ...getPrivateKeyInfo }] - // // outputs, // [{ pubKeyHash, satoshis }] - // // dsaCollateralTx, // any tx with fee >= 0.00010000 - // // dsiCollateralTx, // any tx with fee >= 0.00010000 - // ); } + App.createCoinJoinSession = async function () { + let $coins = $$('[data-name=coin]:checked'); + if (!$coins.length) { + let msg = + 'Use the Coins table to select which coins to include in the CoinJoin session.'; + window.alert(msg); + return; + } + + let inputs = []; + let outputs = []; + let denom; + for (let $coin of $coins) { + let [address, txid, indexStr] = $coin.value.split(','); + let index = parseInt(indexStr, 10); + let coin = selectCoin(address, txid, index); + coin.denom = DashJoin.getDenom(coin.satoshis); + if (!coin.denom) { + let msg = 'CoinJoin requires 10s-Denominated coins, shown in BOLD.'; + window.alert(msg); + return; + } + if (!denom) { + denom = coin.denom; + } + if (coin.denom !== denom) { + let msg = + 'CoinJoin requires all coins to be of the same denomination (ex: three 0.01, or two 1.0, but not a mix of the two).'; + window.alert(msg); + return; + } + Object.assign(coin, { outputIndex: coin.index }); + inputs.push(coin); + + let output = { + address: receiveAddrs.shift(), + satoshis: denom, + pubKeyHash: '', + }; + let pkhBytes = await DashKeys.addrToPkh(output.address, { + version: network, + }); + output.pubKeyHash = DashKeys.utils.bytesToHex(pkhBytes); + outputs.push(output); + } + + let collateralTxes = [ + await App._signCollateral(DashJoin.MIN_COLLATERAL), + await App._signCollateral(DashJoin.MIN_COLLATERAL), + ]; + + await createCoinJoinSession( + App._evonode, + inputs, // [{address, txid, pubKeyHash, ...getPrivateKeyInfo }] + outputs, // [{ pubKeyHash, satoshis }] + collateralTxes, // any tx with fee >= 0.00010000 + ); + }; + main().catch(function (err) { console.error(`Error in main:`, err); });