Skip to content

feat(payments): scaffold Balance service for GetBalance - #658

Merged
bmc08gt merged 2 commits into
mainfrom
feat/ocp-balance-rpc
Aug 26, 2026
Merged

feat(payments): scaffold Balance service for GetBalance#658
bmc08gt merged 2 commits into
mainfrom
feat/ocp-balance-rpc

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Scaffolds the OpenCode Balance service added upstream in ocp-protobuf-api@ea6418c5, and bumps the ocp-client-protocol pin to 0.2.0 to pick it up.

What the contract adds

One unary RPC, ocp.balance.v1.Balance/GetBalance. The request takes an owner account; the response carries a result enum (OK/DENIED/NOT_FOUND) and core_mint_value, a uint64 in quarks. Nothing pre-existing changed, so no existing result enum was renumbered and no existing Error*(rawValue:) mapping shifts.

What this adds

BalanceServiceErrorGetBalanceClient.getBalance, registered on Client, plus the TransportClassificationTests registry line every new TransportClassifiableError conformer needs.

Nothing consumes it yet. No screen, view model, or Session integration, because there is no balance surface asking for this today.

Two calls worth review:

  • The request is not signed. GetBalanceRequest has no auth field, unlike every other Payments API request, so getBalance takes a bare PublicKey rather than a signing KeyPair. That asymmetry is in the contract, not an oversight in this scaffold.
  • core_mint_value maps to TokenAmount(quarks:mint:.usdf). The mint is not on the wire: the field is core-mint quarks by definition, and the contract carries a core mint address nowhere, so USDF is a client-side constant here exactly as it is in SwapInstructionBuilder+NewCurrency.swift, which takes MintMetadata.usdf as coreMint. If the core mint ever stops being USDF, this call site is one of the places that has to change.

Android keeps the same field as a raw Long rather than materializing the implied mint. Both platforms hold the same information; only the domain typing differs.

ErrorGetBalance orders ok/denied/notFound at rawValues 0/1/2 to match the proto, with the transport cases at negative rawValues as this codebase does elsewhere.

Blocked on

0.2.0 does not exist yet. exact: "0.2.0" has no tag to resolve until code-payments/ocp-client-protocol#4 merges and is published, so this stays a draft until then.

Adds the app-side service/client layering for the new ocp.balance.v1.Balance
RPC: BalanceService wraps the generated client, ErrorGetBalance maps the
proto result enum and conforms to TransportClassifiableError, and
Client+Balance exposes an async getBalance(owner:) wrapper.

GetBalanceRequest carries no auth field, so this takes a bare PublicKey
rather than a signing KeyPair, unlike every other Payments API request.
Picks up the Balance service added upstream in ea6418c5.
@bmc08gt bmc08gt self-assigned this Aug 26, 2026
@bmc08gt
bmc08gt marked this pull request as ready for review August 26, 2026 15:56
@bmc08gt
bmc08gt merged commit 08af5b5 into main Aug 26, 2026
@bmc08gt
bmc08gt deleted the feat/ocp-balance-rpc branch August 26, 2026 16:07
bmc08gt added a commit that referenced this pull request Aug 26, 2026
`FlipcashAPI/Package.swift` moved to `exact: "0.2.0"` in #658, but the
workspace `Package.resolved` still held 0.1.0, so every build re-resolved
it and left the tree dirty.
bmc08gt added a commit that referenced this pull request Aug 26, 2026
…, chat and discover (#659)

* fix(tests): rewrite the buy/sell regressions as convert flows

The tab-bar UI has no Buy or Sell tile on a currency the account holds —
`CurrencyInfoContentV2` offers Give / Convert / Withdraw, and Convert is
what replaced both. It moves value between balances you already hold, so
each of the three skipped tests is a convert in one direction: token →
Dollars was Sell, Dollars → token was buy-paying-with-reserves, and
token → token was buy-with-currency. The tests are renamed for the
direction they exercise, and `SellConfirmationScreen` goes with the v1
sell sheet that was its only subject.

Two of the three need no picker interaction: `ConvertAmountViewModel`
defaults a non-Dollars source to Dollars and a Dollars source to the
largest other holding. `ConvertBetweenTokensRegressionTests` is the one
that opens the picker, so `CurrencyPickerSheet` rows get
`currency-picker-row` with Dollars distinguished as
`currency-picker-row-usdf` — otherwise picking "the first token" could
land on Dollars depending on how balances sort.

Two v1 assertions are dropped rather than ported. The nested-sheet
swipe-down regression can't recur: convert is pushed, so there is no
sheet behind the processing screen. And a finished convert now pops to
the Wallet root instead of returning to the currency page, per
`ConvertFlowDestinationView`.

`CurrencyInfoUIScreen` splits `assertReached` into held and unheld
variants, since which tiles exist depends on the balance.

* fix(tests): route the settings, wallet-tile and give tests through the tab bar

Nine more `skipPendingTabBarRewrite` call sites, in three groups.

Settings rows: the You tab's list has only My Account and Advanced, so each
test opens the You tab and scrolls to its row. Access Key and Application Logs
are on Advanced, not My Account — `SettingsMyAccountScreen` keeps the
account-level actions off itself and says so in its header doc. Blocked stays
on My Account.

Money rows: Add Money and Withdraw Money are Wallet tiles now, so
`SettingsUIScreen` loses both properties and the four tests enter through
`wallet-tile-add-money` / `wallet-tile-withdraw-money`.

Give: `navigateToGiveAmount()` goes Wallet → first currency card → its Give
tile. The keypad pops itself as the bill appears — `GiveScreen.onBillPresented`
when `isPushed` — so both callers end on `CurrencyInfoScreen` rather than a tab
root, and the cash link reaches its history from there without a second trip
through the wallet. That history is the "Recent" section header in the
tab-bar UI; the v1 "Transaction History" button went with the old footer.

`GiveDiscoverGateRegressionTests` is deleted rather than rewritten: USDF is
giveable now, so `GiveCashGate.discoverCurrencies` is unreachable. The
"No Balance Yet" sibling keeps its skip with a corrected reason — the gate has
no fresh-account entry either, since the only caller that gated a give was
`ScanBottomBar`, which the embedded Scan tab does not render.

Also drops the balance-retry loop in `navigateToGiveAmount()`: the Give tile is
only drawn for a currency the account holds, so that path raises no gate
dialog.

* fix(tests): route the chat-group UI tests through the tab bar

`BlockUnblockSmokeTests` opened the Tips list as a sheet off the scanner's
bottom bar and closed it through `navigationBars["Tips"]`. Embedded as the
Chat tab, that list has no toolbar and no leading tip-card cell, so the page
object now opens `app.buttons["Chat"]`, waits on the "Chats" title, and treats
every cell as a conversation. Its `tearDown` unblock also still tapped
`app.buttons["Settings"]`, which the tab bar removed — so a run that failed
after the block left the shared account with a user blocked. It goes through
You › My Account › Blocked now, with a non-asserting scroll of its own:
`scrollUpToAndTap` would fail the teardown and mask the real failure.

`ProfileCreationSmokeTests` walked Tips intro → name → photo → tipcard, and
none of those four steps has an entry left. `OnboardingNameScreen` is
mandatory after the access key, so no account reaches the app without a name
and both name-less prompts stay gated off; `ProfileNameScreen` skips the photo
step for every caller because the card omits the photo. Replaced by
`DisplayNameSmokeTests`, covering what survived: a freshly registered account
lands on the You tab with a card it can share, and the name behind it changes
through My Account. `selectFirstPhotoFromLibrary` went with its last caller.

Both pass on iPhone 17.

* test(give): delete the no-balance gate regression

`GiveCashGate`'s "No Balance Yet" branch has no entry a test can reach. The
only caller that gated a give from a tab root was `ScanBottomBar`, which
renders under `if !isEmbedded`, and the Scan tab embeds `ScanScreen`. The gate
still fires from a chat's Send Cash, `TipFlow`, and the give deeplink, but the
test's premise was a fresh empty account, which reaches none of those — and
give now starts from a held currency's Give tile, which an empty account has
no card for.

Same call as `GiveDiscoverGateRegressionTests`, which went for the same reason.

* test(discover): route the discover-group UI tests through the wallet tiles

Discover moved from a scanner sheet to a Wallet tile, and currency creation
moved from Discover's promo card to a sibling tile, so the two entries are now
checked side by side instead of one through the other.

Both tiles are gated on funding: WalletScreen draws walletTiles only for
session.hasEverAddedMoney(), and gives an unfunded account the new-user tutorial
in their place. So DiscoverCurrenciesSmokeTests takes the standing account
rather than creating a fresh one, and the currency-creation gate moves out of
AddMoneyGateRegressionTests into its own class on the same account. That gate
still fires there because shouldAddMoneyBeforeLaunch is a shortfall check, not a
$0 check — the account holds money but not the launch cost — and the test skips
if that ever stops being true.

The buy gate has no fixture left and stays skipped, with the reason recorded on
the class: flipcash://discover reaches the same destination, but app.open
relaunches the app and a freshly created account comes back on "Create a New
Account", while both standing accounts hold displayable USDF, so
BuyAmountViewModel.paymentOptions is non-empty and the button reads Next
instead of Add Money.

* test(uitests): drop skipPendingTabBarRewrite with its last call site

Every test that entered through the v1 scanner chrome has been rewritten or
deleted, so the helper has no callers. The rewrite plan keeps the map of where
each flow moved.

* test(buy): delete the no-assets add-money gate regression

The test needs an account with nothing spendable, and the tab-bar UI gives such
an account no door to a currency's Get button. The Wallet's Discover tile is
drawn only for session.hasEverAddedMoney(); flipcash://discover reaches the same
destination, but app.open relaunches the app and a freshly created account comes
back on "Create a New Account"; and both standing accounts hold displayable
USDF, so BuyAmountViewModel.paymentOptions is non-empty and the button reads
Next instead of Add Money.

The gate is still live app code — a funded account spent down to nothing hits
it. Restoring the test needs a spent-down fixture, which the rewrite plan
records.

* chore(api): record ocp-client-protocol 0.2.0 in the workspace lockfile

`FlipcashAPI/Package.swift` moved to `exact: "0.2.0"` in #658, but the
workspace `Package.resolved` still held 0.1.0, so every build re-resolved
it and left the tree dirty.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant