diff options
| author | Adam Soltys <[email protected]> | 2026-08-02 17:07:26 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-02 17:07:26 -0700 |
| commit | bff3692ed92dbce4aa1e71f1ba6ec491645eb5db (patch) | |
| tree | 9840735d57148571b8adf3f9e4e4bf4279451d61 /src/features | |
| parent | 404350a32f26c892fa7b3b65f222ebc955be8a36 (diff) | |
| download | coinosv3-bff3692ed92dbce4aa1e71f1ba6ec491645eb5db.tar.gz coinosv3-bff3692ed92dbce4aa1e71f1ba6ec491645eb5db.zip | |
Ark: partial moves to Savings via an exact-amount split
Into Savings now has an amount field with a Max button. Max (or the full
balance) keeps the existing whole-balance cooperative offboard. A
partial amount first mints a vtxo of exactly that amount with a
self-send — the mailbox delivers self-sends, verified on regtest — then
offboards just that vtxo, so the number typed is the number that leaves
Spending (fees still come off what arrives, shown on the success
screen). startOffboard accepts a vtxo subset for this; an exact-match
vtxo skips the split. The unilateral exit page remains reachable from a
link under the field.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/ark.js | 59 |
1 files changed, 52 insertions, 7 deletions
diff --git a/src/features/ark.js b/src/features/ark.js index 4d9242c..09670fa 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -1285,13 +1285,37 @@ export function arkFeature(ctx) { // Offboard the whole ark balance back into this wallet's own on-chain // receive address — the mirror image of boarding. - async function doArkOffboard() { + // Offboard `amountSat` (or everything when omitted). An offboard moves + // whole vtxos — no change — so a partial amount first mints an exact-amount + // vtxo via a self-send (regtest-verified: the mailbox delivers self-sends), + // then exits just that one. The number the user typed is the number that + // leaves Spending; fees come off what arrives in Savings. + async function doArkOffboard(amountSat) { ui.arkBusy = 'offboard'; ui.arkError = ''; render(); try { const mgr = await connectArk(); + const spendables = () => mgr.state.vtxos.filter((v) => v.state === 'spendable'); + const total = spendables().reduce((n, v) => n + v.amountSat, 0); + let ids; // undefined = all spendable vtxos + if (amountSat && amountSat < total) { + let target = spendables().find((v) => v.amountSat === amountSat); + if (!target) { + const before = new Set(spendables().map((v) => v.id)); + await mgr.send(mgr.address(), amountSat); + for (let i = 0; i < 30 && !target; i++) { + await new Promise((r) => setTimeout(r, 2000)); + await mgr.sync().catch(() => {}); + target = spendables().find((v) => v.amountSat === amountSat && !before.has(v.id)); + } + if (!target) throw new Error(t('arkSplitTimeout')); + } + ids = [target.id]; + } const address = wallet.freshReceive().address; const spk = btc.OutScript.encode(btc.Address(wallet.netCfg.net).decode(address)); - const action = await mgr.startOffboard(spk, address); + const action = await mgr.startOffboard(spk, address, ids); + ui.arkOffboardAmt = ''; + ui.arkMoveOpen = false; ui.arkOffboarded = { txid: action.txid, netSat: action.netSat, feeSat: action.feeSat }; wallet.scan().catch(() => {}); // surface the incoming pending tx promptly } catch (e) { @@ -1373,10 +1397,31 @@ export function arkFeature(ctx) { : h('div', { class: 'col', style: 'gap:8px' }, h('p', { class: 'small muted', style: 'margin:0' }, t('moveToSavingDesc')), // no "X sats in Spending" line — the balance sits right above on this card - h('button', { - class: 'btn-block', disabled: !b || !b.spendableSat, - onClick: () => { ui.arkMoveOpen = false; ui.arkExitPage = true; ui.arkError = ''; render(); }, - }, t('moveToSavingBtn'))), + (() => { + const spendable = b ? b.spendableSat : 0; + const sats = parseInt((ui.arkOffboardAmt || '').trim(), 10) || 0; + const valid = sats > 0 && sats <= spendable; + return h('div', { class: 'col', style: 'gap:8px' }, + h('div', { class: 'input-group' }, + h('input', { + type: 'number', inputmode: 'numeric', min: '0', + placeholder: t('arkOffboardAmtPlaceholder'), + value: ui.arkOffboardAmt || '', + onInput: (e) => { ui.arkOffboardAmt = e.target.value; render(); }, + }), + h('button', { class: 'btn-ghost', onClick: () => { ui.arkOffboardAmt = String(spendable); render(); } }, t('max')), + h('button', { + class: 'btn-primary', disabled: !!ui.arkBusy || !valid, + onClick: () => doArkOffboard(sats), + }, ui.arkBusy === 'offboard' ? h('span', { class: 'spinner sm' }) : t('moveToSavingBtn'))), + ui.arkBusy === 'offboard' && sats < spendable + ? h('div', { class: 'small muted', style: 'text-align:center' }, t('arkSplitting')) + : null, + h('button', { + class: 'linklike small', style: 'align-self:center', + onClick: () => { ui.arkMoveOpen = false; ui.arkExitPage = true; ui.arkError = ''; render(); }, + }, t('arkUniTitle'))); + })()), ui.arkError ? h('div', { class: 'notice err' }, ui.arkError) : null); } @@ -1528,7 +1573,7 @@ export function arkFeature(ctx) { h('h4', { style: 'margin:0' }, t('arkCoopTitle')), h('p', { class: 'small muted', style: 'margin:0' }, t('arkCoopDesc')), spendable > 0 - ? h('button', { class: 'btn-primary btn-block', disabled: !!ui.arkBusy, onClick: doArkOffboard }, + ? h('button', { class: 'btn-primary btn-block', disabled: !!ui.arkBusy, onClick: () => doArkOffboard() }, ui.arkBusy === 'offboard' ? h('span', { class: 'spinner sm' }) : t('arkOffboardBtn', { n: fmtAmount(spendable) + ' ' + unitLabel() })) : h('div', { class: 'small faint' }, t('arkExitNoBalance'))), // unilateral |
