From 962b2eb627957eb252b9779b53c532889378e595 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Wed, 26 Aug 2026 07:10:06 -0700 Subject: [PATCH] Lower tip dust limit to 600 sats and keep the like/tip modal editable after errors Allow tips as low as 600 sats (tip output dust) while keeping the wallet balance gate at 3000 sats to cover transaction fees. Keep the tip input and Like button enabled after a validation error so the user can edit and retry. By specifier. --- specs/like-tip-memo.feature | 3 ++- src/components/post-feed/like-tip-modal.js | 10 +++++++--- src/services/memo-like.js | 10 +++++++--- test/unit/like-tip-page.test.js | 2 +- test/unit/memo-like.test.js | 15 +++++++++++---- 5 files changed, 28 insertions(+), 12 deletions(-) diff --git a/specs/like-tip-memo.feature b/specs/like-tip-memo.feature index 466ef65..40031f9 100644 --- a/specs/like-tip-memo.feature +++ b/specs/like-tip-memo.feature @@ -39,6 +39,7 @@ Feature: Like / Tip a Memo Examples: | tip | + | 600 | | 3000 | | 25000 | @@ -64,7 +65,7 @@ Feature: Like / Tip a Memo Examples: | tip | | 1 | - | 2999 | + | 599 | Scenario: Like / Tip a Memo - 6 a tip above the maximum is rejected Given the wallet has a spendable balance of 150000000 sats diff --git a/src/components/post-feed/like-tip-modal.js b/src/components/post-feed/like-tip-modal.js index d6e4c5f..073463d 100644 --- a/src/components/post-feed/like-tip-modal.js +++ b/src/components/post-feed/like-tip-modal.js @@ -123,8 +123,12 @@ function LikeTipModal ({ show, post, wallet, profiles = {}, onHide, onSuccess }) step='1' placeholder='0' value={tip} - onChange={(e) => setTip(e.target.value)} - disabled={submitting || !!error} + onChange={(e) => { + setTip(e.target.value) + // Clear a previous validation error so the user can retry. + setError('') + }} + disabled={submitting} /> @@ -141,7 +145,7 @@ function LikeTipModal ({ show, post, wallet, profiles = {}, onHide, onSuccess }) diff --git a/src/services/memo-like.js b/src/services/memo-like.js index 038ccf2..cff2127 100644 --- a/src/services/memo-like.js +++ b/src/services/memo-like.js @@ -16,7 +16,8 @@ Constants MEMO_LIKE_PREFIX : hex prefix for the Memo "like" action (0x6d04) - DUST_LIMIT_SATS : smallest non-dust BCH output (3000 sats) + DUST_LIMIT_SATS : minimum wallet balance required to broadcast a like (3000 sats) + DUST_TIP_SATS : smallest non-dust tip output (600 sats) MAX_TIP_SATS : sanity maximum for a single tip (100000000 sats = 1 BCH) PARENT_TXID_BYTES : liked post txid size in bytes (32) */ @@ -26,6 +27,7 @@ const { hexToBytes } = require('./hex') const MEMO_LIKE_PREFIX = '6d04' const DUST_LIMIT_SATS = 3000 +const DUST_TIP_SATS = 600 const MAX_TIP_SATS = 100000000 const PARENT_TXID_BYTES = 32 @@ -43,6 +45,7 @@ class MemoLike extends MemoAction { super(deps) this.feed = deps.feed this.dustLimit = deps.dustLimit || DUST_LIMIT_SATS + this.dustTipSats = deps.dustTipSats || DUST_TIP_SATS this.maxTip = deps.maxTip || MAX_TIP_SATS } @@ -81,8 +84,8 @@ class MemoLike extends MemoAction { throw err } - if (tipSats > 0 && tipSats < this.dustLimit) { - const err = new Error(`Tip is below the dust limit of ${this.dustLimit} sats.`) + if (tipSats > 0 && tipSats < this.dustTipSats) { + const err = new Error(`Tip is below the dust limit of ${this.dustTipSats} sats.`) err.code = 'like_dust' throw err } @@ -189,6 +192,7 @@ class MemoLike extends MemoAction { MemoLike.MEMO_LIKE_PREFIX = MEMO_LIKE_PREFIX MemoLike.DUST_LIMIT_SATS = DUST_LIMIT_SATS +MemoLike.DUST_TIP_SATS = DUST_TIP_SATS MemoLike.MAX_TIP_SATS = MAX_TIP_SATS module.exports = MemoLike diff --git a/test/unit/like-tip-page.test.js b/test/unit/like-tip-page.test.js index 39a033a..865ea7c 100644 --- a/test/unit/like-tip-page.test.js +++ b/test/unit/like-tip-page.test.js @@ -154,7 +154,7 @@ test('submitting with a non-numeric tip string is rejected', async () => { test('submitting with a dust tip is rejected', async () => { const { wallet, page } = build() page.open(POST_TXID, AUTHOR_ADDRESS) - page.setTip('2999') + page.setTip('599') const result = await page.submit() diff --git a/test/unit/memo-like.test.js b/test/unit/memo-like.test.js index 8030a0f..4667318 100644 --- a/test/unit/memo-like.test.js +++ b/test/unit/memo-like.test.js @@ -59,6 +59,10 @@ test('DUST_LIMIT_SATS is 3000', () => { assert.equal(MemoLike.DUST_LIMIT_SATS, 3000) }) +test('DUST_TIP_SATS is 600', () => { + assert.equal(MemoLike.DUST_TIP_SATS, 600) +}) + test('MAX_TIP_SATS is 100000000', () => { assert.equal(MemoLike.MAX_TIP_SATS, 100000000) }) @@ -135,20 +139,23 @@ test('a negative tip is rejected with a validation error', async () => { await assertLikeRejected(fakeWallet(), -1, 'like_validation') }) -test('a tip below the dust limit is rejected with a dust error', async () => { +test('a tip below the tip dust limit is rejected with a dust error', async () => { const wallet = fakeWallet({ utxos: [{ txid: 'u1', value: 100000 }] }) await assertLikeRejected(wallet, 1, 'like_dust') - await assertLikeRejected(wallet, 2999, 'like_dust') + await assertLikeRejected(wallet, 599, 'like_dust') }) -test('a tip at the dust limit is accepted', async () => { +test('a tip at the tip dust limit is accepted', async () => { const wallet = fakeWallet({ utxos: [{ txid: 'u1', value: 100000 }] }) const memoLike = new MemoLike({ wallet }) - const txid = await memoLike.like(POST_TXID, 3000, AUTHOR_ADDRESS) + const txid = await memoLike.like(POST_TXID, 600, AUTHOR_ADDRESS) assert.equal(txid, 'fake-txid') assert.equal(wallet.broadcasts.length, 1) + assert.deepEqual(wallet.broadcasts[0].bchOutput, [ + { address: AUTHOR_ADDRESS, amountSat: 600 } + ]) }) test('a tip above the hard maximum is rejected with a maximum error', async () => {