Repository navigation
fix(chat): let an exact buy clear a balance-gated group - #1551
Merged
Merged
Conversation
A $5 buy of a launchpad token from a gated group comes back valued at $4.999999 or less, and `GroupAccess` compared raw micro-dollars, so the wallet showed $5.00 while the gate kept blocking until a second buy. Round the held balance half-up to cents (`Fiat.toDouble`) before comparing it with the requirement. $4.995 passes too; the server enforces the rule against its own supply, so a half-cent too generous costs one denied join. This matches iOS's `ConversationGate.unmetBalance`.
After a buy, the reserve-state stream could not correct a balance valued at a stale supply, and it lowered an exact one by a micro-dollar per tick. `TokenState` kept only the USD value, so the handler converted it to token quarks at the new supply and back through `Fiat.tokenBalance`, rounding down on both legs. A $5 buy on the discrete curve at 10k-100k supply read $4.999999, then $4.999998, and so on. `TokenState` now keeps one `Holding(value, tokenQuarks)` per mint, the pair iOS's `StoredBalance` keeps, and a tick values the held quarks at the new supply. `balances` stays as a view derived from the holdings. A speculative add or subtract writes a holding with no quarks, so stale quarks cannot outlive it. The handler also updates from the current state instead of writing back a snapshot, which lost a fetch that landed mid-tick. A fetch writes the supply its metadata carried, as iOS's `VerifiedProtoService.saveReserveStates` does: neither writer is timestamped, the fetch is the only source for a mint the stream has not reached, and the stream's next delivery re-prices the quarks. A balance restored from Room has no quarks and keeps its value until the next fetch.
A minimum-balance rule with an empty `mints` list was checked against the largest single holding, so $3 each of two tokens failed a $5 bar. iOS's `ConversationGate.unmetBalance` compares `totalBalance` for the same rule and admits that holder. Android now sums every holding and rounds the total to cents once, after adding, as iOS does. A rule that names mints is still met by the largest of them.
bmc08gt
force-pushed
the
fix/chat-gate-post-buy-balance
branch
from
September 24, 2026 16:27
6e5d2d5 to
f2539b9
Compare
Buy More stayed disabled for anyone who held none of a gated group's token. The button waits for the rule's currency name, and the chat screen only read it from the token cache. getTokenMetadata writes a network result to that cache only when the account holds the mint, so for a first-time buyer the name never arrived. The chat screen now fetches the rule's mint and then follows the cache, so a copy hydrated after a buy still replaces the fetched one. The gate bar's comment no longer says the button lacks a mint to open; the mint comes from the rule, and only the label waits.
The gate bar disabled its button until the rule's currency name loaded, and a rule that names no mint never loads one, so its button stayed dead. iOS's ConversationGatePanel never disables it; it picks the label and destination from the rule's mint instead. The button now follows the same table. A rule with no mint, or one on USDF, reads "Add Cash" and opens the deposit options the chat screen already presents for an empty balance. A named mint reads "Buy More" until its name loads, then "Buy More <name>", and opens that token either way, since the mint comes from the rule.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Buying exactly the requirement from a balance-gated group ("Buy More" in
GroupGateBar) leaves the gate blocked. The wallet shows $5.00, but the balance behind it is a few micro-dollars under $5, andGroupAccesscompared raw micro-dollars.The purchase itself is right: on the USDF path the fee is added on top, so the curve receives the full amount. The shortfall comes from how the client values the result. On the discrete curve, $5 bought from 10k, 50k and 100k supply measures:
Five commits:
fix(chat): compare at display precision.GroupAccess.evaluaterounds the held balance half-up to cents before comparing, matching what the user sees and iOS'sConversationGate.unmetBalance. $4.995 passes too. The server checks membership against its own supply, so being half a cent generous costs at most a denied join.fix(tokens): re-price held quarks when a supply moves.TokenStatekeeps oneHolding(value, tokenQuarks)per mint, the same pair iOS'sStoredBalancekeeps, and a reserve tick values the held quarks at the new supply withFiat.tokenBalance. The old handler converted the USD value to quarks and back, rounding down on both legs. It also snapshotted state and wrote whole maps back, so a fetch that landed mid-tick was lost; it now updates from the current state.balancesstays as a derived view, andFiatis unchanged.A fetch writes the supply its metadata carried, even if it is older than one the stream delivered. Neither writer is timestamped, and the fetch is the only source for a mint the stream hasn't reached. The stream's next delivery re-prices the quarks, and the coordinator applies every delivery. This matches iOS's
VerifiedProtoService.saveReserveStates(fix(chat): let an exact-amount buy clear a group's balance gate code-ios-app#851). For a first-time mint, whose GetMints supply can predate the buy, adding the mint restarts the stream, and the new stream's opening reserve state re-prices the quarks.fix(chat): sum every holding for a rule that names no mint. An emptymintslist was checked against the largest single holding, so $3 each of two tokens failed a $5 bar that iOS admits. It now compares the sum of all holdings, rounded to cents once after adding, as iOS'stotalBalancecompare does. A rule that names mints is still met by the largest of them.fix(chat): resolve the gate's token for a viewer who holds none. Buy More waits for the rule's currency name, and the chat screen read it only from the token cache.getTokenMetadatacaches a network result only for a mint the account holds, so a first-time buyer's button never enabled. The screen now fetches the rule's mint, then follows the cache.fix(chat): keep the gate's Buy More enabled, as iOS does. The button follows iOS'sConversationGatePanel: a rule with no mint, or on USDF, reads "Add Cash" and opens the deposit options; a named mint reads "Buy More" until its name loads, then "Buy More ". Previously a rule with no mint left the button disabled for good.A balance restored from Room has no quarks and keeps its value until the next fetch.