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.
This commit is contained in:
Chris Troutner
2026-08-26 07:10:06 -07:00
parent e7e758d370
commit 962b2eb627
5 changed files with 28 additions and 12 deletions
+2 -1
View File
@@ -39,6 +39,7 @@ Feature: Like / Tip a Memo
Examples: Examples:
| tip | | tip |
| 600 |
| 3000 | | 3000 |
| 25000 | | 25000 |
@@ -64,7 +65,7 @@ Feature: Like / Tip a Memo
Examples: Examples:
| tip | | tip |
| 1 | | 1 |
| 2999 | | 599 |
Scenario: Like / Tip a Memo - 6 a tip above the maximum is rejected Scenario: Like / Tip a Memo - 6 a tip above the maximum is rejected
Given the wallet has a spendable balance of 150000000 sats Given the wallet has a spendable balance of 150000000 sats
+7 -3
View File
@@ -123,8 +123,12 @@ function LikeTipModal ({ show, post, wallet, profiles = {}, onHide, onSuccess })
step='1' step='1'
placeholder='0' placeholder='0'
value={tip} value={tip}
onChange={(e) => setTip(e.target.value)} onChange={(e) => {
disabled={submitting || !!error} setTip(e.target.value)
// Clear a previous validation error so the user can retry.
setError('')
}}
disabled={submitting}
/> />
</Form.Group> </Form.Group>
</Form> </Form>
@@ -141,7 +145,7 @@ function LikeTipModal ({ show, post, wallet, profiles = {}, onHide, onSuccess })
<Button <Button
variant='primary' variant='primary'
onClick={handleSubmit} onClick={handleSubmit}
disabled={submitting || !!error || !post || !wallet} disabled={submitting || !post || !wallet}
> >
{submitting ? 'Liking...' : 'Like'} {submitting ? 'Liking...' : 'Like'}
</Button> </Button>
+7 -3
View File
@@ -16,7 +16,8 @@
Constants Constants
MEMO_LIKE_PREFIX : hex prefix for the Memo "like" action (0x6d04) 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) MAX_TIP_SATS : sanity maximum for a single tip (100000000 sats = 1 BCH)
PARENT_TXID_BYTES : liked post txid size in bytes (32) PARENT_TXID_BYTES : liked post txid size in bytes (32)
*/ */
@@ -26,6 +27,7 @@ const { hexToBytes } = require('./hex')
const MEMO_LIKE_PREFIX = '6d04' const MEMO_LIKE_PREFIX = '6d04'
const DUST_LIMIT_SATS = 3000 const DUST_LIMIT_SATS = 3000
const DUST_TIP_SATS = 600
const MAX_TIP_SATS = 100000000 const MAX_TIP_SATS = 100000000
const PARENT_TXID_BYTES = 32 const PARENT_TXID_BYTES = 32
@@ -43,6 +45,7 @@ class MemoLike extends MemoAction {
super(deps) super(deps)
this.feed = deps.feed this.feed = deps.feed
this.dustLimit = deps.dustLimit || DUST_LIMIT_SATS this.dustLimit = deps.dustLimit || DUST_LIMIT_SATS
this.dustTipSats = deps.dustTipSats || DUST_TIP_SATS
this.maxTip = deps.maxTip || MAX_TIP_SATS this.maxTip = deps.maxTip || MAX_TIP_SATS
} }
@@ -81,8 +84,8 @@ class MemoLike extends MemoAction {
throw err throw err
} }
if (tipSats > 0 && tipSats < this.dustLimit) { if (tipSats > 0 && tipSats < this.dustTipSats) {
const err = new Error(`Tip is below the dust limit of ${this.dustLimit} sats.`) const err = new Error(`Tip is below the dust limit of ${this.dustTipSats} sats.`)
err.code = 'like_dust' err.code = 'like_dust'
throw err throw err
} }
@@ -189,6 +192,7 @@ class MemoLike extends MemoAction {
MemoLike.MEMO_LIKE_PREFIX = MEMO_LIKE_PREFIX MemoLike.MEMO_LIKE_PREFIX = MEMO_LIKE_PREFIX
MemoLike.DUST_LIMIT_SATS = DUST_LIMIT_SATS MemoLike.DUST_LIMIT_SATS = DUST_LIMIT_SATS
MemoLike.DUST_TIP_SATS = DUST_TIP_SATS
MemoLike.MAX_TIP_SATS = MAX_TIP_SATS MemoLike.MAX_TIP_SATS = MAX_TIP_SATS
module.exports = MemoLike module.exports = MemoLike
+1 -1
View File
@@ -154,7 +154,7 @@ test('submitting with a non-numeric tip string is rejected', async () => {
test('submitting with a dust tip is rejected', async () => { test('submitting with a dust tip is rejected', async () => {
const { wallet, page } = build() const { wallet, page } = build()
page.open(POST_TXID, AUTHOR_ADDRESS) page.open(POST_TXID, AUTHOR_ADDRESS)
page.setTip('2999') page.setTip('599')
const result = await page.submit() const result = await page.submit()
+11 -4
View File
@@ -59,6 +59,10 @@ test('DUST_LIMIT_SATS is 3000', () => {
assert.equal(MemoLike.DUST_LIMIT_SATS, 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', () => { test('MAX_TIP_SATS is 100000000', () => {
assert.equal(MemoLike.MAX_TIP_SATS, 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') 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 }] }) const wallet = fakeWallet({ utxos: [{ txid: 'u1', value: 100000 }] })
await assertLikeRejected(wallet, 1, 'like_dust') 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 wallet = fakeWallet({ utxos: [{ txid: 'u1', value: 100000 }] })
const memoLike = new MemoLike({ wallet }) 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(txid, 'fake-txid')
assert.equal(wallet.broadcasts.length, 1) 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 () => { test('a tip above the hard maximum is rejected with a maximum error', async () => {