Skip to content
Closed
Show file tree
Hide file tree
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
13 changes: 2 additions & 11 deletions src/hooks/wallet/__tests__/useSpendBundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* The hook itself orchestrates kernel clients, Rain API calls, and the
* session-key grant flow — those paths are covered by integration + manual
* testing on sandbox. These tests lock down the deterministic pieces:
* - `computeSpendStrategy` routing (smartcollateral → mixed → insufficient)
* - `computeSpendStrategy` routing (collateralsmart → mixed → insufficient)
* - `usdcUnitsToRainCents` amount conversion at the Rain API boundary
*/

Expand Down Expand Up @@ -45,17 +45,8 @@ import { computeSpendStrategy } from '../useSpendBundle'
describe('computeSpendStrategy', () => {
const amount = 1000n

it('prefers smart-only when smart covers the amount, even if collateral-only is allowed', () => {
// Smart account is spent first so the payment never touches the Rain
// collateral (and its per-account withdrawal-signature cooldown) when
// smart-account USDC already covers it.
it('prefers collateral-only when allowed and rain covers the amount', () => {
expect(computeSpendStrategy({ smart: 5000n, rain: 10_000n, amount, collateralOnlyAllowed: true })).toBe(
'smart-only'
)
})

it('uses collateral-only when smart cannot cover but collateral can (allowed)', () => {
expect(computeSpendStrategy({ smart: 100n, rain: 10_000n, amount, collateralOnlyAllowed: true })).toBe(
'collateral-only'
)
})
Expand Down
19 changes: 11 additions & 8 deletions src/hooks/wallet/useSpendBundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,22 +104,25 @@ export class SessionKeyGrantRequiredError extends Error {

/**
* Pure routing helper — decides which bucket(s) a spend will pull from.
* Priority: smart → collateral → mixed. The smart account is spent first
* whenever it can cover the whole amount, so a payment never touches the
* Rain collateral — and Rain's per-account withdrawal-signature cooldown —
* if the user's smart-account USDC already covers it. Collateral is the
* fallback (single recipient AND no subsequent kernel calls, since Rain's
* coordinator transfers tokens directly with nothing following), and `mixed`
* tops up the shortfall from collateral when smart alone can't cover it.
* Priority: collateral → smart → mixed. Collateral-only requires a single
* recipient AND no subsequent kernel calls (Rain's coordinator transfers
* tokens directly; nothing follows).
*
* NOTE: a smart-first variant (#2230) was reverted — it routed `smart-only`
* whenever the cached `smartBalance` covered the amount, but card funds are
* swept smart→collateral, so a stale/pre-sweep balance made smart-only spends
* revert on-chain ("ERC20: transfer amount exceeds balance") for collateral-
* funded users. Re-introducing smart-first needs a live (uncached) balance read
* AND a fallback to collateral when the smart-only path can't be funded.
*/
export function computeSpendStrategy(input: {
smart: bigint
rain: bigint
amount: bigint
collateralOnlyAllowed: boolean
}): SpendStrategy {
if (input.smart >= input.amount) return 'smart-only'
if (input.collateralOnlyAllowed && input.rain >= input.amount) return 'collateral-only'
if (input.smart >= input.amount) return 'smart-only'
if (input.smart + input.rain >= input.amount) return 'mixed'
return 'insufficient'
}
Expand Down
Loading