Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
35e51be
add MIN_FEE_USTX constant to match contract min-fee
Mosas2000 Mar 16, 2026
6f030d7
enforce minimum fee of 1 uSTX in feeForTip calculation
Mosas2000 Mar 16, 2026
fd2461d
apply minimum fee logic to recipientReceives
Mosas2000 Mar 16, 2026
a42df36
apply minimum fee to maxTransferForTip post-condition ceiling
Mosas2000 Mar 16, 2026
9c82358
import MIN_FEE_USTX in post-conditions test file
Mosas2000 Mar 16, 2026
0cd5853
add test asserting MIN_FEE_USTX equals 1
Mosas2000 Mar 16, 2026
f79b1b3
add test for minimum fee enforcement on tiny tip amounts
Mosas2000 Mar 16, 2026
94d6f9e
test feeForTip returns 1 for 10 uSTX sub-threshold amount
Mosas2000 Mar 16, 2026
654f0d5
test feeForTip returns minimum fee for 100 uSTX
Mosas2000 Mar 16, 2026
2dd5c15
test feeForTip returns minimum fee for 199 uSTX
Mosas2000 Mar 16, 2026
9e83d58
test feeForTip at the 200 uSTX boundary
Mosas2000 Mar 16, 2026
6002379
test feeForTip returns 2 for 201 uSTX
Mosas2000 Mar 16, 2026
bea126c
test feeForTip never returns 0 when fee basis points are positive
Mosas2000 Mar 16, 2026
334efaf
test feeForTip returns 0 for sub-threshold amounts when bps is zero
Mosas2000 Mar 16, 2026
d664d22
test totalDeduction includes minimum fee for tiny tips
Mosas2000 Mar 16, 2026
02cf45d
test totalDeduction for 1 uSTX tip equals 2
Mosas2000 Mar 16, 2026
68dcf6d
test recipientReceives deducts minimum fee from small tips
Mosas2000 Mar 16, 2026
d479193
test recipientReceives returns 0 for a 1 uSTX tip
Mosas2000 Mar 16, 2026
cb40652
test recipientReceives returns full amount when fees disabled
Mosas2000 Mar 16, 2026
c15269b
test maxTransferForTip ceiling for 1 uSTX tip
Mosas2000 Mar 16, 2026
02cebbb
test maxTransferForTip ceiling for 10 uSTX tip
Mosas2000 Mar 16, 2026
6ba0e80
test string coercion for sub-threshold feeForTip input
Mosas2000 Mar 16, 2026
5d21590
test string coercion for sub-threshold totalDeduction input
Mosas2000 Mar 16, 2026
2581543
add minimum fee cross-function consistency test group
Mosas2000 Mar 16, 2026
b678351
test maxTransferForTip equals totalDeduction plus 1 at min fee boundary
Mosas2000 Mar 16, 2026
12be6fa
test fee plus recipient covers full amount at 200 uSTX boundary
Mosas2000 Mar 16, 2026
25ae1fc
test all fee functions agree when basis points are zero
Mosas2000 Mar 16, 2026
b6fbf6d
re-add string coercion test for sub-threshold feeForTip
Mosas2000 Mar 16, 2026
7d4008b
re-add string coercion test for sub-threshold totalDeduction
Mosas2000 Mar 16, 2026
a1776c6
re-add minimum fee cross-function consistency test group
Mosas2000 Mar 16, 2026
fe2751e
update feeForTip jsdoc to document minimum fee behavior
Mosas2000 Mar 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions frontend/src/lib/post-conditions.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@

import { PostConditionMode, Pc } from '@stacks/transactions';

// Contract fee parameters keep in sync with tipstream.clar
// Contract fee parameters -- keep in sync with tipstream.clar
export const FEE_BASIS_POINTS = 50;
export const BASIS_POINTS_DIVISOR = 10000;
export const MIN_FEE_USTX = 1;

/** Human-readable fee percentage (e.g. 0.5 for 0.5%). */
export const FEE_PERCENT = FEE_BASIS_POINTS / BASIS_POINTS_DIVISOR * 100;
Expand All @@ -37,7 +38,8 @@ export const SAFE_POST_CONDITION_MODE = PostConditionMode.Deny;
*/
export function maxTransferForTip(amountMicroSTX, feeBps = FEE_BASIS_POINTS) {
const amt = Number(amountMicroSTX);
const fee = Math.ceil(amt * feeBps / BASIS_POINTS_DIVISOR);
const rawFee = Math.ceil(amt * feeBps / BASIS_POINTS_DIVISOR);
const fee = feeBps > 0 ? Math.max(rawFee, MIN_FEE_USTX) : 0;
return amt + fee + 1;
}

Expand All @@ -57,14 +59,19 @@ export function tipPostCondition(senderAddress, amountMicroSTX, feeBps = FEE_BAS

/**
* Calculate the platform fee in microSTX for a given tip amount.
* Uses Math.ceil to match the on-chain rounding behavior.
* Uses Math.ceil to match the on-chain rounding behavior and enforces
* a minimum of MIN_FEE_USTX when fee basis points are positive.
*
* @param {number|string} amountMicroSTX Tip amount in microSTX (coerced to Number).
* @param {number} [feeBps=50] Fee in basis points.
* @returns {number}
*/
export function feeForTip(amountMicroSTX, feeBps = FEE_BASIS_POINTS) {
return Math.ceil(Number(amountMicroSTX) * feeBps / BASIS_POINTS_DIVISOR);
const raw = Math.ceil(Number(amountMicroSTX) * feeBps / BASIS_POINTS_DIVISOR);
if (feeBps > 0) {
return Math.max(raw, MIN_FEE_USTX);
}
return 0;
}

/**
Expand All @@ -89,5 +96,7 @@ export function totalDeduction(amountMicroSTX, feeBps = FEE_BASIS_POINTS) {
*/
export function recipientReceives(amountMicroSTX, feeBps = FEE_BASIS_POINTS) {
const amt = Number(amountMicroSTX);
return amt - Math.floor(amt * feeBps / BASIS_POINTS_DIVISOR);
const rawFee = Math.floor(amt * feeBps / BASIS_POINTS_DIVISOR);
const fee = feeBps > 0 ? Math.max(rawFee, MIN_FEE_USTX) : 0;
return amt - fee;
}
70 changes: 70 additions & 0 deletions frontend/src/test/post-conditions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
FEE_BASIS_POINTS,
BASIS_POINTS_DIVISOR,
FEE_PERCENT,
MIN_FEE_USTX,
SAFE_POST_CONDITION_MODE,
maxTransferForTip,
tipPostCondition,
Expand All @@ -26,6 +27,10 @@ describe('post-conditions', () => {
expect(SAFE_POST_CONDITION_MODE).toBeDefined();
});

it('defines MIN_FEE_USTX as 1', () => {
expect(MIN_FEE_USTX).toBe(1);
});

it('exports FEE_PERCENT as a derived percentage', () => {
expect(FEE_PERCENT).toBe(0.5);
expect(FEE_PERCENT).toBe(FEE_BASIS_POINTS / BASIS_POINTS_DIVISOR * 100);
Expand Down Expand Up @@ -101,6 +106,31 @@ describe('post-conditions', () => {
// 1000 * 50 / 10000 = 5
expect(feeForTip(1000)).toBe(5);
});

it('enforces minimum fee of 1 uSTX for tiny amounts', () => {
// 1 * 50 / 10000 = 0.005, ceil = 1, max(1, 1) = 1
expect(feeForTip(1)).toBe(MIN_FEE_USTX);
});

it('returns minimum fee for amounts below the threshold', () => {
// 10 * 50 / 10000 = 0.05, ceil = 1, max(1, 1) = 1
expect(feeForTip(10)).toBe(1);
});

it('returns minimum fee for 100 uSTX', () => {
// 100 * 50 / 10000 = 0.5, ceil = 1, max(1, 1) = 1
expect(feeForTip(100)).toBe(1);
});

it('returns minimum fee for 199 uSTX', () => {
// 199 * 50 / 10000 = 0.995, ceil = 1, max(1, 1) = 1
expect(feeForTip(199)).toBe(1);
});

it('transitions past minimum at 200 uSTX', () => {
// 200 * 50 / 10000 = 1.0, ceil = 1, max(1, 1) = 1
expect(feeForTip(200)).toBe(1);
});
});

describe('totalDeduction', () => {
Expand Down Expand Up @@ -166,5 +196,45 @@ describe('post-conditions', () => {
it('recipientReceives accepts a string micro-STX value', () => {
expect(recipientReceives('1000000')).toBe(recipientReceives(1000000));
});

it('feeForTip coerces string for sub-threshold amounts', () => {
expect(feeForTip('10')).toBe(feeForTip(10));
});

it('totalDeduction coerces string for sub-threshold amounts', () => {
expect(totalDeduction('5')).toBe(totalDeduction(5));
});
});

describe('minimum fee cross-function consistency', () => {
it('totalDeduction equals amount plus feeForTip for sub-threshold tip', () => {
expect(totalDeduction(10)).toBe(10 + feeForTip(10));
});

it('maxTransferForTip equals totalDeduction plus 1 at min fee boundary', () => {
expect(maxTransferForTip(10)).toBe(totalDeduction(10) + 1);
});

it('fee plus recipient covers full amount at 200 uSTX boundary', () => {
const amt = 200;
const net = recipientReceives(amt);
const fee = feeForTip(amt);
expect(net + fee).toBeGreaterThanOrEqual(amt);
});

it('all functions agree when basis points are zero', () => {
expect(feeForTip(10, 0)).toBe(0);
expect(totalDeduction(10, 0)).toBe(10);
expect(recipientReceives(10, 0)).toBe(10);
expect(maxTransferForTip(10, 0)).toBe(11);
});

it('consistency holds across a range of sub-threshold amounts', () => {
for (const amt of [1, 2, 5, 50, 100, 150, 199, 200]) {
const fee = feeForTip(amt);
expect(totalDeduction(amt)).toBe(amt + fee);
expect(maxTransferForTip(amt)).toBe(amt + fee + 1);
}
});
});
});
Loading