diff --git a/public/index.html b/public/index.html index 9c46934..8f10e70 100644 --- a/public/index.html +++ b/public/index.html @@ -399,8 +399,88 @@

Digital Cash Wallet

0 + + + Collateral + + + + + 0 + + + + 0 + + +
+ + Share: +
output: txid
+ + +
+ +
output: private key as wif
+
diff --git a/public/wallet-app.js b/public/wallet-app.js index b80e124..f93bed3 100644 --- a/public/wallet-app.js +++ b/public/wallet-app.js @@ -26,6 +26,7 @@ let network = 'testnet'; let rpcBaseUrl = 'https://trpc.digitalcash.dev/'; + let rpcExplorer = 'https://trpc.digitalcash.dev/'; let rpcBasicAuth = btoa(`api:null`); let addresses = []; @@ -127,7 +128,7 @@ localStorage.setItem(key, dataJson); } - function getAllUtxos() { + function getAllUtxos(opts) { let utxos = []; let spendableAddrs = Object.keys(deltasMap); for (let address of spendableAddrs) { @@ -136,13 +137,22 @@ continue; } for (let coin of info.deltas) { - if (coin.reserved) { + if (coin.reserved > 0) { continue; } + + let addressInfo = keysMap[coin.address]; Object.assign(coin, { outputIndex: coin.index, denom: DashJoin.getDenom(coin.satoshis), + pubKeyHash: addressInfo.pubKeyHash, }); + if (opts?.denom === false) { + if (coin.denom) { + continue; + } + } + utxos.push(coin); } } @@ -189,7 +199,7 @@ let dust = totalSats - totalSigSats; dust += fee; - $('[data-id=send-amount]').value = totalAmount.toFixed(4); + $('[data-id=send-amount]').value = toFixed(totalAmount, 4); //$('[data-id=send-dust]').value = dust; $('[data-id=send-dust]').textContent = dust; }; @@ -239,7 +249,7 @@ // but this is quick-n-dirty just to get an alert rather than // checking error types and translating cthe error message let available = balance / SATS; - let availableStr = available.toFixed(4); + let availableStr = toFixed(available, 4); let err = new Error( `requested to send '${amountStr}' when only '${availableStr}' is available`, ); @@ -257,12 +267,12 @@ amount = output.satoshis / SATS; $('[data-id=send-dust]').textContent = draft.tx.feeTarget; - $('[data-id=send-amount]').textContent = amount.toFixed(8); + $('[data-id=send-amount]').textContent = toFixed(amount, 8); let signedTx = await dashTx.legacy.finalizePresorted(draft.tx); console.log('DEBUG signed tx', signedTx); { - let amountStr = amount.toFixed(4); + let amountStr = toFixed(amount, 4); let confirmed = window.confirm(`Really send ${amountStr} to ${address}?`); if (!confirmed) { return; @@ -272,6 +282,84 @@ void (await commitWalletTx(signedTx)); }; + App.exportWif = async function (event) { + event.preventDefault(); + + let address = $('[data-id=export-address]').value; + let privKey = await keyUtils.getPrivateKey({ address }); + let wif = await DashKeys.privKeyToWif(privKey, { version: network }); + + $('[data-id=export-wif]').textContent = wif; + }; + + App.sendMemo = async function (event) { + event.preventDefault(); + + let msg; + + /** @type {String?} */ + let memo = $('[name=memo]').value || ''; + /** @type {String?} */ + let message = null; + let memoEncoding = $('[name=memo-encoding]:checked').value || 'hex'; + if (memoEncoding !== 'hex') { + message = memo; + memo = null; + } + let satoshis = 0; + msg = memo || message; + + let outputs = [{ satoshis, memo, message }]; + let changeOutput = { + address: '', + pubKeyHash: '', + satoshis: 0, + reserved: 0, + }; + + let utxos = getAllUtxos({ denom: false }); + let txInfo = await 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); + } + + let signedTx = await dashTx.hashAndSignAll(txInfo); + { + 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.sendCollateral = async function (event) { + // at least the collateral amount + let dustF = Math.random() * DashJoin.COLLATERAL; + dustF = dustF / 10; + dustF += DashJoin.COLLATERAL; + + let dust = Math.floor(dustF); + let utxos = getAllUtxos(); + let memo = { satoshis: dust, memo: '\0' }; + let draftTx = await draftWalletTx(utxos, null, memo); + draftTx.outputs[0].satoshis = 0; + draftTx.feeTarget += dust; + }; + async function draftWalletTx(utxos, inputs, output) { let draftTx = dashTx.legacy.draftSingleOutput({ utxos, inputs, output }); console.log('DEBUG draftTx', draftTx); @@ -294,7 +382,12 @@ if (output.pubKeyHash) { continue; } - if (!output.address) { + if (output.memo) { + draftTx.feeTarget += output.satoshis; + output.satoshis = 0; + continue; + } + if (!output.address && typeof output.memo !== 'string') { let err = new Error(`output is missing 'address' and 'pubKeyHash'`); window.alert(err.message); throw err; @@ -331,6 +424,10 @@ dbSet(input.address, null); } for (let output of signedTx.outputs) { + let isMemo = !output.address; + if (isMemo) { + continue; + } updatedAddrs.push(output.address); removeElement(addresses, output.address); removeElement(receiveAddrs, output.address); @@ -341,12 +438,13 @@ await updateDeltas(updatedAddrs); let txid = await DashTx.getId(signedTx.transaction); + let now = Date.now(); for (let input of signedTx.inputs) { let coin = selectCoin(input.address, input.txid, input.outputIndex); if (!coin) { continue; } - coin.reserved = true; // mark as spent-ish + coin.reserved = now; // mark as spent-ish } for (let i = 0; i < signedTx.outputs.length; i += 1) { let output = signedTx.outputs[i]; @@ -464,6 +562,9 @@ }); let hdpath = `m/44'/${coinType}'/${accountIndex}'/${usage}`; // accountIndex from step 2 + // TODO put this somewhere safe + // let descriptor = `pkh([${walletId}/${partialPath}/0/${index}])`; + addresses.push(address); if (usage === DashHd.RECEIVE) { receiveAddrs.push(address); @@ -533,6 +634,14 @@ want: 5, need: 0, }, + // { + // denom: 10000, + // priority: 0, + // have: 0, + // want: 100, + // need: 0, + // collateral: true, + // }, ]; function getCashDrawer() { let slots = dbGet('cash-drawer-control', []); @@ -619,11 +728,10 @@ } let cjAmount = cjBalance / SATS; - $('[data-id=cj-balance]').textContent = cjAmount.toFixed(8); + $('[data-id=cj-balance]').textContent = toFixed(cjAmount, 8); } App.denominateCoins = async function (event) { - console.log('DENOMINATE COINS'); event.preventDefault(); { @@ -640,14 +748,11 @@ } let slots = dbGet('cash-drawer-control'); - console.log('slots', slots); let priorityGroups = groupSlotsByPriorityAndAmount(slots); - console.log('priorityGroups', priorityGroups); let priorities = Object.keys(priorityGroups); priorities.sort(sortNumberDesc); - console.log('priorities', priorities); for (let priority of priorities) { let slots = priorityGroups[priority].slice(0); @@ -656,12 +761,10 @@ for (;;) { let slot = slots.shift(); if (!slot) { - console.log('e: no slot'); break; } let isNeeded = slot.need >= 1; if (!isNeeded) { - console.log('s: not needed', slot.denom); continue; } @@ -673,8 +776,9 @@ continue; } + let now = Date.now(); for (let coin of coins) { - coin.reserved = true; + coin.reserved = now; } slot.need -= 1; @@ -711,7 +815,7 @@ { console.log('DEBUG confirming signed tx', signedTx); let amount = output.satoshis / SATS; - let amountStr = amount.toFixed(4); + let amountStr = toFixed(amount, 4); let confirmed = window.confirm( `Really send ${amountStr} to ${output.address}?`, ); @@ -837,7 +941,7 @@ utxo.outputIndex, ].join(','); $('[data-name=address]', clone).textContent = utxo.address; - $('[data-name=amount]', clone).textContent = utxo.amount.toFixed(4); + $('[data-name=amount]', clone).textContent = toFixed(utxo.amount, 4); if (utxo.denom) { $('[data-name=amount]', clone).style.fontStyle = 'italic'; $('[data-name=amount]', clone).style.fontWeight = 'bold'; @@ -853,7 +957,7 @@ let totalBalance = DashTx.sum(utxos); let totalAmount = totalBalance / SATS; - $('[data-id=total-balance]').innerText = totalAmount.toFixed(4); + $('[data-id=total-balance]').innerText = toFixed(totalAmount, 4); let tableBody = $('[data-id=coins-table]'); tableBody.textContent = ''; @@ -870,6 +974,9 @@ } function siftDenoms() { + if (!denomsMap[DashJoin.COLLATERAL]) { + denomsMap[DashJoin.COLLATERAL] = {}; + } for (let denom of DashJoin.DENOMS) { if (!denomsMap[denom]) { denomsMap[denom] = {}; @@ -886,6 +993,13 @@ for (let coin of info.deltas) { let denom = DashJoin.getDenom(coin.satoshis); if (!denom) { + let halfCollateral = DashJoin.COLLATERAL / 2; + let fitsCollateral = + coin.satoshis >= halfCollateral && + coin.satoshis < DashJoin.DENOM_LOWEST; + if (fitsCollateral) { + denomsMap[DashJoin.COLLATERAL][coin.address] = coin; + } continue; } @@ -895,6 +1009,18 @@ } } + /** + * @param {Number} f - the number + * @param {Number} d - how many digits to truncate (round down) at + */ + function toFixed(f, d) { + let order = Math.pow(10, d); + let t = f * order; + t = Math.floor(t); + f = t / order; + return f.toFixed(d); + } + async function main() { if (network === `testnet`) { let $testnets = $$('[data-network=testnet]');