Skip to content
Merged
Changes from all commits
Commits
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
31 changes: 22 additions & 9 deletions staking-dashboard/src/utils/claimCart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,22 @@ export interface DelegationClaimInputs {
tokenAddress: Address
decimals: number
symbol: string
/** Current ERC20 balance sitting on the split contract — i.e. tokens already
* claimed from a rollup but not yet distributed. When this is non-zero and
* no per-rollup balances need claiming, the helper emits a distribute-only
* plan so a previously stranded balance can still be swept to the user. */
/** Current ERC20 balance sitting on the split contract. When non-zero with
* no per-rollup balance to claim, the helper emits a distribute-only entry
* to sweep stranded tokens — but only if the balance is above
* `RECOVERY_DUST_THRESHOLD_NUMERATOR / 10` of one whole token, to avoid
* queuing a useless distribute for the rounding-dust most splits carry
* after any partial distribute. */
splitContractBalance?: bigint
}

/**
* Dust threshold (numerator / 10) used to gate the distribute-only recovery
* flow. `5` here means "0.5 of a whole token". Below this we assume the
* leftover is rounding-dust and not worth queuing a transaction for.
*/
const RECOVERY_DUST_THRESHOLD_NUMERATOR = 5n

export interface DelegationClaimResult {
entries: ClaimCartEntry[]
/** `stepGroupIdentifier` of the delegation's distribute step. Pass to
Expand Down Expand Up @@ -90,11 +99,15 @@ export function buildDelegationClaimEntries(inputs: DelegationClaimInputs): Dele
} = inputs

const claimables = rollupRewardsByRollup.filter((r) => r.rewards > 0n)
// Distribute-only recovery: no rollup balance to claim, but the split
// contract still holds tokens from a prior partially-executed claim. Emit
// just the distribute (no claim deps) so the user can sweep the stranded
// balance. Without this branch the button/modal becomes a silent no-op.
if (claimables.length === 0 && splitContractBalance === 0n) {
// Dust threshold scaled by the asset's decimals (0.5 of one whole token).
// Splits almost always carry a tiny non-zero balance after a partial
// distribute, so a bare `splitContractBalance > 0n` check made the bulk
// path queue a useless distribute every click. Only treat balances above
// the threshold as a real stranded amount worth a distribute-only recovery.
const dustThreshold = decimals >= 1
? RECOVERY_DUST_THRESHOLD_NUMERATOR * 10n ** BigInt(decimals - 1)
: 0n
if (claimables.length === 0 && splitContractBalance < dustThreshold) {
return { entries: [], distributeGroup: null }
}

Expand Down
Loading