Tabs and reserved funds
Why a running payment reserves the notes it spends, what keeps them reserved, and what frees them.
On Solana, if two transactions need the same lamports, only one succeeds. A private balance has the same rule. A payment that loses that race wastes a proof and your user's time, so the SDK stops it before it starts.
Two tabs, one balance
Every tab of your app shares one copy of the private balance. Two tabs could pick the same notes for two payments at once.
To prevent that, a payment reserves its notes before it builds anything. Another payment, in
this tab or another, chooses among the notes left. readBalances counts reserved notes as
reserved.
A payment reserves every note it spends, change included. Paying 10 USDC from three notes worth 25 USDC reserves all 25.
Each running payment also holds a Web Lock named for it. The browser drops the lock when the tab closes or crashes. That is how the SDK tells a running payment from a stopped one.
A running payment keeps its notes until it finishes. A stopped one keeps them until something frees them, as below.
Where a reserved payment stands
listReservedSpends lists every payment holding notes. Each has a state, and a running flag
that says whether some tab still holds its lock.
state | What it means | What releasing it does |
|---|---|---|
unsent | Nothing has left the device. | Frees its notes at once. |
sent | It may have reached the relayer or Solana. | Stops new attempts. The notes free once it lands or can no longer land. |
releasing | Released while a sent attempt may still land. | Nothing more. It is already waiting. |
A sent payment keeps its notes until it settles. Spending them again meanwhile could race it.
A deposit can reserve notes too. A deposit merges up to three of your user's notes of that token
into its change, and reserves them until it settles. It is listed with kind: "deposit".
What frees reserved funds
- Finishing. A payment that lands spends its notes. Once a sync sees that, they leave the balance.
- The next sync, if the tab closed before sending anything. The payment is
unsentand no tab holds its lock. - Your app's release button.
releaseReservedNotesreleases what is not running. Withforce: trueit releases a running payment too, which then fails at its next step. - A relayer's refusal. A payment that comes back
relay-refusednever reached Solana, so its notes free at once. - Time, for a sent payment that can no longer land. After a relay comes back
failed, its notes stay reserved until its proof's window passes.
What this means for your app
- Show reserved funds apart from the available balance, so the numbers add up.
- Offer a release button for payments that are not running.
- Expect a quick retry to fail after
failed. Its notes may still be reserved. A retry afterrelay-refusedcan spend them at once.
Last updated