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);
});