diff options
| author | Adam Soltys <[email protected]> | 2026-08-04 08:31:21 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-04 08:31:21 -0700 |
| commit | 9fd8887dcdd5e0fc1067a91032bfd18eb25f0202 (patch) | |
| tree | 0ab3da2a80bb69a71139697852da646576f4f23f /src | |
| parent | 40c26324c556dc47be7e392391420183a36a6c3d (diff) | |
| download | coinosv3-9fd8887dcdd5e0fc1067a91032bfd18eb25f0202.tar.gz coinosv3-9fd8887dcdd5e0fc1067a91032bfd18eb25f0202.zip | |
Ark gifts go down to 1 sat; negative amounts can't OOM the encoder
Regtest-proven (tools/ark/dust-gift-test.js): the ASP cosigns all-dust
arkoor sends, so a sub-dust bearer vtxo sweeps to its claimer fine, and
dust folds into rounds alongside bigger siblings. The 330-sat gift
floor guarded a failure that can't happen — dropped to 1 sat.
The experiment also caught a live bug: refresh() of a balance smaller
than the round fee computed a negative output amount, and pbWriter's
varint sign-extends negatives into an infinite loop — auto-refresh
would OOM the tab of any wallet too small to renew. varint now throws
on negatives and refresh() refuses cleanly, before marking coins
pending, when balance minus fee is under the server's 330-sat output
minimum.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015QwAtBSpKvECqp1rHLHjJk
Diffstat (limited to 'src')
| -rw-r--r-- | src/ark/manager.js | 5 | ||||
| -rw-r--r-- | src/ark/proto.js | 3 | ||||
| -rw-r--r-- | src/features/gifts.js | 7 |
3 files changed, 14 insertions, 1 deletions
diff --git a/src/ark/manager.js b/src/ark/manager.js index 01de06f..f9819a0 100644 --- a/src/ark/manager.js +++ b/src/ark/manager.js @@ -1270,6 +1270,11 @@ export class ArkManager { const tip = await this.chain.tipHeight(); const totalSat = inputs.reduce((n, v) => n + v.amountSat, 0); const feeSat = this.refreshFee(inputs, tip); + // The server refuses round outputs under P2TR dust (330), so a wallet + // this small can't renew — fail before the action marks coins pending. + if (totalSat - feeSat < 330) { + throw new Error(`balance too small to refresh: ${totalSat} sat minus ${feeSat} sat fee is under the 330 sat minimum`); + } const action = { id: `refresh-${Date.now()}`, type: 'refresh', step: 'created', inputIds: inputs.map((v) => v.id), diff --git a/src/ark/proto.js b/src/ark/proto.js index 6729465..81651ee 100644 --- a/src/ark/proto.js +++ b/src/ark/proto.js @@ -29,6 +29,9 @@ export function pbWriter() { const chunks = []; const varint = (n) => { n = BigInt(n); + // a negative sign-extends under >>= forever — the do/while would push + // bytes until the process dies of memory, so refuse it loudly instead + if (n < 0n) throw new Error(`varint: negative value ${n}`); const bs = []; do { let b = Number(n & 0x7fn); n >>= 7n; if (n) b |= 0x80; bs.push(b); } while (n); chunks.push(Uint8Array.from(bs)); diff --git a/src/features/gifts.js b/src/features/gifts.js index bc16619..0543030 100644 --- a/src/features/gifts.js +++ b/src/features/gifts.js @@ -402,11 +402,16 @@ export function giftsFeature(ctx) { // An ark gift: send the amount to a fresh bearer identity, off-chain and // instant — no coin locking, no split logic, no claim fee. + // Gifts go down to 1 sat: the ASP cosigns all-dust arkoor sends, so a + // sub-dust bearer vtxo sweeps to the claimer fine (regtest-proven — + // tools/ark/dust-gift-test.js). The trade a sub-330 coin carries — no + // unilateral exit on its own, renewable in rounds only alongside bigger + // siblings — is the same one dust zaps already make. async function doCreateArkGift() { const info = hook('arkGiftInfo'); const avail = info ? info.spendableSat : 0; const amt = ui.giftMax ? avail : parseAmount(ui.giftAmount, getUnit()); - if (!amt || amt < 330) { ui.giftError = t('giftAmountInvalid', { n: fmtAmount(330) + ' ' + unitLabel() }); render(); return; } + if (!amt || amt < 1) { ui.giftError = t('giftAmountInvalid', { n: fmtAmount(1) + ' ' + unitLabel() }); render(); return; } if (amt > avail) { ui.giftError = t('giftExceedsBalance'); render(); return; } ui.busy = true; ui.giftError = ''; render(); try { |
