Skip to content

fix: route quickstart through flash service#415

Merged
islandbitcoin merged 3 commits into
tmp/bridge-rebase-pr-readyfrom
fix/quickstart-router-public-subgraph
Jun 24, 2026
Merged

fix: route quickstart through flash service#415
islandbitcoin merged 3 commits into
tmp/bridge-rebase-pr-readyfrom
fix/quickstart-router-public-subgraph

Conversation

@forge0x

@forge0x forge0x commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Rename the quickstart app service from galoy to flash and render Apollo Router, Kratos, and Oathkeeper upstreams to http://flash:... instead of legacy Galoy/Bats service names.
  • Pass Flash YAML config into the quickstart flash and trigger containers, including a quickstart-only overlay.
  • Add a guarded quickstart IBEX mock for local bootstrap: it requires both ibex.mock: true in the quickstart overlay and FLASH_ENABLE_IBEX_MOCK=true in Compose, so normal environments still use real IBEX.
  • Narrow quickstart.sh to a Flash public GraphQL smoke test instead of running the old Galoy BTC-wallet funding flow.
  • Fix distroless image source asset permissions so the non-root runtime can read copied config/proto assets.

Note

The previous revision used galoy because that was the inherited Compose service name for the local app container. Dread was right to call that out: Flash quickstart should not encode Galoy service routing, and CI quickstart should not need real IBEX sandbox credentials.

Verification

  • GALOY_QUICKSTART_PATH=./ docker compose -f quickstart/docker-compose.yml config --quiet
  • bash -n quickstart/bin/re-render.sh quickstart/bin/quickstart.sh
  • git diff --check
  • ytt -f ./docker-compose.tmpl.yml -f galoy/docker-compose.yml -f galoy/docker-compose.override.yml | rg -n "/app/quickstart/config|FLASH_ENABLE_IBEX_MOCK|http://(galoy|bats-tests):|firebaseappcheck.googleapis.com/72279297366|projects/72279297366"
  • Isolated local quickstart smoke with local Flash images and alternate host ports: GALOY_ENDPOINT=localhost:14002 COMPOSE_PROJECT_NAME=qs415ci ./bin/quickstart.shregtest, DONE

@forge0x forge0x force-pushed the fix/quickstart-router-public-subgraph branch from e168521 to ea65b42 Compare June 24, 2026 18:35
@forge0x forge0x changed the title fix: point quickstart upstreams at galoy service fix: route quickstart through flash service Jun 24, 2026
@islandbitcoin islandbitcoin merged commit ac1ddce into tmp/bridge-rebase-pr-ready Jun 24, 2026
10 of 12 checks passed
islandbitcoin added a commit that referenced this pull request Jul 2, 2026
* ENG-297 feat(bridge): add core Bridge integration

* ENG-278 ENG-280 ENG-281 ENG-282 ENG-283 ENG-284 ENG-285 ENG-349 ENG-363 ENG-297 fix(bridge): apply audit hardening and hosted KYC refinements

* ENG-276 feat(bridge): add reconciliation and replay tooling

* ENG-394 feat(accounts): create ETH-USDT Cash Wallet for new accounts

* ENG-297 docs: document Bridge rebase PR-readiness plan

* ENG-376 fix(bridge): reuse pending withdrawal idempotency rows

* ENG-297 chore(bridge): satisfy scoped lint on branch changes

* ENG-297 fix(graphql): add isExternal to UsdtWallet

* fix(graphql): preserve latest account upgrade request query

* feat: bridge reconciliation + USDT provisioning additions (#354)

Ready to merge.

Verified locally:
- `yarn check:sdl` passes
- targeted bridge/IBEX unit tests pass
- `git diff --check` passes

**squash & merge** used to keep `tmp/bridge-rebase-pr-ready` clean.

* ENG-297 - IBEX USD -> USDT Parity  (#355)

* ENG-297 fix(wallets): support USDT cash wallet parity

* fix(wallets): support USDT intraledger sends

* fix(graphql): serialize USDT fee probe amounts (#358)

* Feat/bridge reconciliation (#360)

* feat: add timeout for Bridge request
* feat(bridge): ENG-350 — reset withdrawal state on transfer.failed and surface failure reason
* feat(bridge): ENG-350 harden transfer webhook edge cases and notify users
* feat(bridge): ENG-350 introduce full KYC status lifecycle with offboarded support
* refactor(bridge): rename BridgeDepositLog model to BridgeDeposits
* refactor(ibex): rename IbexCryptoReceiveLog model to IbexCryptoReceive
* refactor(bridge): rename BridgeReplayLog model to BridgeReplay
* fix(bridge) : ENG-289 timeout increase to 15s
* feat: adding full kyc status to reflect them stored as bridgeKycStatus in mongoDB instead of just pending state
* feat: Kyc status
* fix: Duplicated webhook event message
* fix: Duplicated webhook deposit event message
* fix: Virtual Account Edge case

* fix: expose usdt amounts as usd cents

* fix: keep usdt cent boundaries explicit

* feat(cutover): prepare cash wallet migration for bridge (#377)

* fix(graphql): make usdt wallet balance nullable (#369)

* feat(cutover): add cash wallet migration state primitives

* feat(cutover): persist cash wallet cutover state

* feat(cutover): add cash wallet write guard

* feat(cutover): classify cash wallet migration candidates

* feat(cutover): add cash wallet preflight summary

* feat(cutover): collect cash wallet discovery results

* feat(cutover): plan primary cash wallet migrations

* feat(cutover): upsert planned migration records

* feat(cutover): prepare primary migration batch

* feat(cutover): start migration worker checkpoint

* feat(cutover): record migration source balance

* feat(cutover): create balance move invoice checkpoint

* feat(cutover): send balance move payment checkpoint

* feat(cutover): verify balance move checkpoint

* feat(cutover): create fee reimbursement invoice checkpoint

* feat(cutover): complete fee reimbursement checkpoint

* feat(cutover): flip default wallet checkpoint

* feat(cutover): complete migration worker checkpoints

* feat(cutover): provision destination checkpoint

* feat(cutover): dispatch migration worker steps

* feat(cutover): run locked migration batches

* feat(cutover): build migration step handlers

* feat(cutover): wire migration runtime services

* feat(cutover): orchestrate primary migration batches

* feat(cutover): add migration lifecycle controls

* fix(cutover): align migration indexes with run ids

* feat(cutover): add operator command script

* chore(cutover): format migration state helpers

* feat(cutover): preview dry-run migration plan

* fix(cutover): satisfy production build types

* feat(cutover): add operator controls and verification

* feat(cutover): add client-aware cash wallet presentation

* fix(cutover): harden operator cutover run (#376)

* feat(cutover): add operator dashboard and provisioning tools

* fix(cutover): use ibex oauth credentials in local setup

* fix(cutover): retry wallet provisioning and refresh stale invoices

* fix(cutover): retry ibex rate limits during migration payments

* fix(cutover): clarify dashboard run anomalies

* chore(cutover): remove local run artifacts

* chore(cutover): trim bridge PR noise

* chore(cutover): move unit tests to separate PR

* Potential fix for pull request finding

* Potential fix for pull request finding

* Potential fix for pull request finding

Co-authored-by: Island Bitcoin <34528298+islandbitcoin@users.noreply.github.com>

* fix(bridge): restore cutover repository build

* fix(cutover): preserve cash wallet history after cutover (#380)

* fix(wallets): preserve USDT transaction history precision (#382)

* fix(wallets): preserve USDT transaction history precision

* chore(wallets): restore base relay import

* fix(wallets): handle missing IBEX USDT history amounts

* feat(bridge): external account webhook + ENG-350 transfer failure reset (#379)

* feat(externalAccounts): Webhook endpoint firing for external accounts

feat: external accounts event triggering

this commit initialize the webhook endpoint to register on Bridge to trigger external account link after plaid completion

* fix(ENG-350) : reset pending withdraw state on transfer failure.

this commit mark the transfer as failed in our bridge withdrawals collection when something went wrong during the trasfer flow, the user is notified with a popup and on return state, as well for transfer successfully completed

* fix(bridge): normalize currency display in withdrawal notification

Map "usdt" to "USD" and uppercase other currency codes so the push
notification shows a human-readable label instead of a raw enum value.

* test(bridge): verify webhook signature contract (#383)

* fix(cash-wallet): guard cutover start

Require a prepared runnable migration row before Cash Wallet cutover start can move config to in_progress.

* feat: add BridgeTransferRequest ERPNext audit model and webhook wiring (#386)

* feat: add BridgeTransferRequest ERPNext audit model and webhook wiring

Adds a full Bridge Transfer Request audit record system that writes
ERPNext DocType rows from Bridge deposit, IBEX crypto receive, and
Bridge transfer webhook events.

- BridgeTransferRequest model with toErpnext() serialization
- BridgeTransferRequestWriter with 4 event handlers
- ErpNext.upsertBridgeTransferRequest with idempotent upsert
- Webhook wiring in deposit, transfer, and crypto-receive routes
- Bridge deposit log made idempotent by eventId

* fix: normalize IBEX network casing for ERPNext consistency

The IBEX crypto receive route lowercases the network field for Mongo
storage, but was passing the lowercased value to the BridgeTransferRequest
model. This created inconsistent casing in ERPNext records: deposits used
'Ethereum' (model default) while IBEX receives used 'ethereum'.

Fix: capitalize the normalized network before passing to the writer.
The lowercase value is still stored in the IBEX log and raw payload.

* fix(bridge): lower account level requirement from level 2 to level > 0 (#385)

* fix(bridge): lower account level requirement from level 2 to level > 0

Bridge operations now allow any non-zero account level instead of requiring Pro (level 2+).

* fix: account denomination for non zero level

---------

Co-authored-by: Island Bitcoin <34528298+islandbitcoin@users.noreply.github.com>

* fix(bridge): correct replay tooling for Bridge webhook_events API (#381)

- Rename createBridgeReplayLog → createBridgeReplay in replay.spec.ts to
  match the production export; spec was failing on every run before this.
- Remove start_date/end_date from ListEventsParams — Bridge /webhook_events
  only supports starting_after, ending_before, limit and category; date
  range filtering is now applied locally inside listAllEvents.
- Update replay-bridge-events.ts to pass start/end (local filter) instead
  of the silently-dropped start_date/end_date.
- Update client.spec.ts to assert local filtering behaviour rather than
  asserting those params are forwarded to the Bridge API.

* fix(bridge): add external account webhook config (#390)

Co-authored-by: Vandana <forge@getflash.io>

* fix(bridge): align GraphQL contract and error codes (#393)

Co-authored-by: Vandana <forge@getflash.io>

* fix(ibex): wire USDT LNURL-pay msat conversion (#389)

* fix(ibex): convert payToLnurl amount to explicit millisatoshis

The IBEX LNURL-pay API (POST /v2/lnurl/pay/send) expects the amount
in millisatoshis, but PayLnurlArgs.send.amount passed the wallet
currency's base unit directly (USDT micros, USD cents, or BTC sats).

Changed PayLnurlArgs to accept amountMsat: number instead of
send: IbexCurrency. This makes the expected unit explicit and forces
callers to perform the msat conversion at the app layer where the
DealerPriceService is available.

ENG-406

* feat(lnurl): add USD wallet LNURL payment mutation

* feat(bridge): push notification on deposit settlement [ENG-275] (#392)

Fires a best-effort push when a Bridge USDT deposit settles via the IBEX
crypto.received webhook (the must-have launch gate). Mirrors the existing
sendBridgeWithdrawalNotification pattern:
- new src/app/bridge/send-deposit-notification.ts
- wired at the crypto-receive settlement success (idempotent — inside the
  per-txHash lock; fires once)
- notification.bridgeDeposit i18n phrases (en + es)

Withdrawal-completion push already exists (transfer.ts) — this completes the
deposit side. IBEX→USD currency display mirrors the withdrawal notif.

Co-authored-by: Dread <bobodread@bobodread.com>

* feat(bridge): split withdrawal into request, confirm, and cancel steps (#387)

* feat(bridge): split withdrawal into request, confirm, and cancel steps

Introduce a two-step off-ramp flow so users review a pending withdrawal
before Bridge is called, with deduplication, cancellation, and push
notifications for the cancelled outcome.

* docs(bridge): document two-step withdrawal flow in FLOWS and API

Update bridge-integration docs to describe bridgeRequestWithdrawal,
bridgeInitiateWithdrawal(withdrawalId), and bridgeCancelWithdrawalRequest.

* chore(bruno): add GraphQL requests for bridge withdrawal flow

Add Bruno mutations and query for request, initiate, cancel, and fetch
pending withdrawal, with local env vars for bridgeWithdrawalId.

* fix(config): add external_account webhook public key to dev config

Required by the Bridge webhook schema so local write-sdl and config
validation succeed.

* chore(graphql): regenerate supergraph for bridge withdrawal mutations

Add bridgeRequestWithdrawal, bridgeCancelWithdrawalRequest, and
bridgeWithdrawalRequest to the federated supergraph schema.

* fix(bridge): map withdrawal request errors to Bridge codes

BridgeWithdrawalNotFoundError and BridgeWithdrawalAlreadyInitiatedError
now map to dedicated BRIDGE_WITHDRAWAL_NOT_FOUND and
BRIDGE_WITHDRAWAL_ALREADY_INITIATED codes instead of the generic
INVALID_INPUT. Tests updated to assert the correct Bridge-specific codes.

* test(bridge): add resolver coverage for request/initiate/cancel withdrawal

Eight resolver-layer tests across the three bridge withdrawal mutations:
happy-path delegation to BridgeService and the two new Bridge-specific
error codes (BRIDGE_WITHDRAWAL_NOT_FOUND, BRIDGE_WITHDRAWAL_ALREADY_INITIATED)
flowing through the full error-map pipeline.

* fix(bridge): resolve duplicate const declarations in deposit webhook handler

Auto-merge conflict left two `const lockKey`/`const lockResult` pairs in
the same function scope. Rename the audit-idempotency pair to
`auditLockKey`/`auditLockResult` to fix TS2451.

* fix(graphql): align BridgeWithdrawal type with new id/status contract

The type had transferId: NonNull and state: NonNull left over from the
old single-step flow. All new resolvers (request/initiate/cancel
mutations plus bridgeWithdrawalRequest query) return id + status, so
those NonNull fields were always null at runtime.

Changes:
- bridge-withdrawal.ts: transferId → id (NonNull), state removed,
  bridgeTransferId added (nullable)
- index.ts: WithdrawalResult and InitiateWithdrawalResult updated to
  match; getWithdrawals map rewritten (id/status/bridgeTransferId/
  externalAccountId) and the always-true filter removed
- bridge-withdrawal-request.ts: add bridgeTransferId to return object
- bridge-contract.spec.ts: assertions updated to new field set

* test(bridge): cover getWithdrawals mapping to id/status/bridgeTransferId

Five tests asserting that getWithdrawals emits the new GQL-facing shape
(id, status, bridgeTransferId) and not the old transferId/state fields.
Also adds findWithdrawalsByAccountId to the bridge-accounts mock so the
describe block can exercise the mapping path.

* feat(bridge): add submitted status for initiated withdrawals

Introduces an intermediary "submitted" status so the UI can distinguish
between a pending withdrawal (awaiting user confirmation) and one that has
already been submitted to Bridge (awaiting Bridge processing).

- schema.ts: adds "submitted" to IBridgeWithdrawalRecord.status union and
  Mongoose enum
- bridge-accounts.ts: updateWithdrawalTransferId now sets status →
  "submitted" atomically with the bridgeTransferId write;
  updateWithdrawalStatus query filter changed from "pending" to "submitted"
  so webhook-driven transitions (completed/failed) match the correct row
- index.spec.ts: mock fixture updated; initiate test now asserts
  result.status === "submitted" and result.bridgeTransferId is set

* fix(bridge): restore getWithdrawals filter with correct && operator

The original filter `w.bridgeTransferId !== null || w.bridgeTransferId !==
undefined` was a tautology (always true), so pending rows with no
bridgeTransferId leaked through — surfacing a null id on a NonNull field
when transferId was the source.

The filter is now `!== null && !== undefined`, preserving the intended
invariant: getWithdrawals (bridgeWithdrawals query) returns only rows
that have been submitted to Bridge. Clients use bridgeWithdrawalRequest(id)
to inspect individual pending/cancelled rows.

Tests updated to assert the exclusion of pending/cancelled rows and the
inclusion of submitted/completed/failed rows.

* fix(graphql): map bridge withdrawal errors through bridgeGqlError

Use BRIDGE_WITHDRAWAL_NOT_FOUND and BRIDGE_WITHDRAWAL_ALREADY_INITIATED
via the shared Bridge error helper so these codes match the documented
contract and other Bridge GraphQL mappings.

* test(bridge): cover withdrawal request confirm and cancel flow

Add chained service tests for request, dedupe, initiate, cancel, and
error guards, and tighten resolver assertions for pending/submitted
status and cancelled delegation.

* chore(graphql): regenerate schema for withdrawal request flow

Align BridgeWithdrawal SDL with id/status/bridgeTransferId and
recompose the federated supergraph.

* test(bridge): guard getWithdrawals id/status GraphQL contract

Add return-shape regression tests so getWithdrawals and initiateWithdrawal
emit id/status (not legacy transferId/state) and exclude rows without a
bridgeTransferId. Add bridgeWithdrawals resolver coverage for passthrough.

* chore(bridge): fix surgical lint issues in withdrawal flow

* chore(bridge): finish withdrawal lint and docs cleanup

* feat(bridge): add KYC tier ceiling error code (ENG-354) (#394)

* feat(alerts): Bridge alerting to PagerDuty / Slack / Discord [ENG-361] (#391)

* feat(alerts): add Bridge AlertService (PagerDuty/Slack/Discord) [ENG-361]

Severity-routed best-effort alert fan-out: critical pages PagerDuty + informs
Slack/Mattermost + Discord; warning informs only. Each sender no-ops when its
env credential is unset. Config: ALERT_PAGERDUTY_ROUTING_KEY / ALERT_SLACK_WEBHOOK_URL
/ ALERT_DISCORD_WEBHOOK_URL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(alerts): wire Bridge alert sources to AlertService [ENG-361]

Fire-and-forget alertBridge() at the Bridge failure points (alongside existing
logging), all critical/page:
- ERPNext audit-write failures (deposit + transfer completed/failed)
- Bridge webhook processing exceptions (deposit + transfer catch)
- Bridge API outage in client.request(): 5xx / timeout / network (4xx not alerted)

IBEX-error source deferred: on-receive.ts is general LN/onchain receive handling,
not the Bridge<->IBEX movement path; needs the exact call site (warning sev).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(alerts): add Bridge alerting setup guide [ENG-361]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(alerts): clean bridge alert lint and deposit test

* feat(alerts): dedupe Bridge alerts and wire IBEX movement warnings [ENG-361]

Group PagerDuty triggers with dedup_key, suppress duplicate Slack/Discord informs, and alert IBEX crypto-receive and reconciliation failures as warnings.

* chore(alerts): format bridge alert updates

* chore(alerts): trim dedup cleanup noise

---------

Co-authored-by: Dread <bobodread@bobodread.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Vandana <forge@getflash.io>
Co-authored-by: heyolaniran <olaniran.abd@gmail.com>

* feat(cashout): route Cashout V1 wallets via cutover guard (ENG-357) (#395)

* feat(cashout): route Cashout V1 wallets via cutover guard (ENG-357)

Cashout V1 always debited the legacy USD wallet and credited the bank-owner
USD wallet. Post-cutover the user's funds live in an ETH-USDT cash wallet, so
the offer must debit USDT and credit the bank-owner's USDT wallet. The Flash
bank-owner account holds both a USD and a USDT wallet, so the route simply
selects the matching pair on both sides — no cross-currency swap.

- Add resolveCashoutWalletSelection: reads the cutover config + per-account
  migration and runs evaluateCashWalletCutoverGuard to pick the route. Source
  and destination wallets are resolved server-side from the guard, NOT from the
  client-supplied walletId (trusted only for wallet-level auth). The guard
  blocks the cashout while a migration is in-flight or has failed.
- CashoutManager.createOffer builds a USD or USDT invoice per route; the
  USD/JMD payout math is unchanged (1 USDT = 1 USD).
- executeCashout authorizes by account, since an old client may still present
  the zeroed legacy USD walletId while the offer settles in USDT, instead of
  an exact wallet-id match.
- CashoutValidator and CashoutDetails.payment.amount are currency-aware; the
  USD path stays byte-identical. ErpNext.draftCashout records the USDT amount.

Adds unit coverage for the routing decision tree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(cashout): add ENG-357 Bruno smoke coverage

* fix(cashout): harden offer redis deserialization

* chore(bridge): add sandbox e2e test suite (#388)

* feat(bridge): add opt-in sandbox e2e test suite

ENG-274. Covers KYC, virtual account, external account, deposit,
withdrawal, post-cutover state, ETH-USDT LN parity, and ERPNext
audit-row verification. Guarded by RUN_BRIDGE_SANDBOX_E2E=true.
Includes preflight check for Level 1 service guard and
documentation drift cleanup (Level 2->Level 1, Tron->ETH-USDT).

* chore(bridge): automate sandbox webhook setup

* chore(bridge): add ERPNext audit snapshot

* test(bridge): gate hosted sandbox success paths

* test(bridge): quiet sandbox e2e service warnings

* chore(bridge): polish sandbox e2e PR cleanup

* chore(bridge): refresh sandbox e2e backup

* test(bridge): address sandbox e2e review feedback

* chore(bridge): add refreshed ERPNext dev backup (#400)

* improve backup and restore scripts, updated to latest frappe backup

* fix(dev): make local Frappe startup repair frontend

* fix(bridge): resolve type errors in bridge-sandbox-e2e suite (#409)

The Bridge sandbox e2e suite never passed through tsc (CI was disabled),
so type errors accumulated and now fail Check Code. All test-only:

- helpers.ts: route mock req/res through 'unknown' before casting to the
  Express handler param types (TS2352).
- execQuery: make generic (default Record<string, unknown>, backward
  compatible) so callers can type the GraphQL payload (TS2339).
- HandlerResponse.body: type as Record<string, unknown> instead of
  unknown, fixing .body access in the deposit/external-account specs
  without per-site casts (TS18046).
- cutover-state.spec: type the execQuery result and narrow the error
  union before asserting.

No production code touched.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(bridge): remove leaked sandbox API key from base config (#412)

The Bridge sandbox API key was committed in dev/config/base-config.yaml.
The key has been rotated/revoked at Bridge (so the value is now inert), and
this removes it from the live tree, replacing it with an empty placeholder —
the real key is injected via config overrides, consistent with the ibex
credentials in the same file.

Note: the rotated key still exists in commit history; since it is revoked
this is no longer a live secret, so a history rewrite of the shared branch
is optional and should be coordinated separately.

Co-authored-by: Dread <bobodread@bobodread.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Normalize Bridge webhook payload envelopes (#406)

* fix(bridge): normalize webhook payload envelopes

* fix: key bridge deposit audit rows by stable id

* fix: handle uncreditable bridge deposit activity

* test(bridge): update transfer webhook spec to Bridge envelope + fix kyc status type

- transfer.spec.ts now posts Bridge's real webhook envelope (event_type/event_object,
  nested source.failure_reason) instead of the legacy { event, data } shape the
  handler no longer reads; restores coverage for failed-cashout audit and
  already_terminal paths.
- kyc.ts: cast bridgeKycStatus to Account["bridgeKycStatus"] (BridgeKycStatus was
  referenced but never defined/imported).

---------

Co-authored-by: Vandana <forge@getflash.io>

* Map IBEX crypto sends as USDT on-chain transactions (#405)

* fix(ibex): map crypto send transactions as USDT sends

* fix: sign IBEX crypto send display amounts

---------

Co-authored-by: Vandana <forge@getflash.io>

* feat(bridge): withdrawal fee estimates — request/confirm/cancel flow (#403)

* feat(bridge): add withdrawal fee estimate config

Introduce developerFeePercent and withdrawalFeeEstimate settings
(bridge fixed fee, gas limit, RPC URL, and fallbacks) for customer
fee breakdown on Bridge withdrawals.

* feat(bridge): compute withdrawal customer fee estimates

Add flash fee, Bridge rail fee (0.6%), and buffered Ethereum gas
estimates, plus presenter helpers for pending vs receipt amounts.

* feat(bridge): persist withdrawal fee breakdown in MongoDB

Store flash, Bridge, gas, and total customer fee estimates on pending
withdrawals and refresh them when reusing an existing pending request.

* feat(bridge): surface fee estimates in withdrawal service flow

Resolve and persist fee estimates on request, map Bridge transfer
receipt fees on initiate, improve Ibex balance error mapping, and pass
developer_fee_percent when creating virtual accounts.

* feat(bridge): add localized flash fee notice for withdrawals

Expose flashFeeNotice copy explaining Flash, Bridge, and gas buffer
components while totals remain estimates until Bridge settles.

* feat(bridge): expose withdrawal fee fields in GraphQL API

Add estimated fee breakdown, subtotal/final amounts, receipt fees,
and flashFeeNotice on BridgeWithdrawal for request and query paths.

* test(bridge): fix unit test failures from fee field additions

- replay.spec.ts: spread jest.requireActual('@config') so getFeesConfig
  survives the partial @config mock
- index.spec.ts: mock updateWithdrawalFeeEstimates in the dedup test so
  stale clearAllMocks() impl doesn't bleed in from a prior test
- return-shapes.spec.ts: toEqual → toMatchObject for getWithdrawals shape
  check, now that presentBridgeWithdrawal emits fee fields
- client-usd-wallet.spec.ts: spread jest.requireActual('ibex-client') so
  IbexUrls survives the partial ibex-client mock

* fix: remove the hardcoded bridge developer fees percentage

* fix: address bridge withdrawal fee review

* fix: address bridge withdrawal fee review

---------

Co-authored-by: Patoo <262265744+patoo0x@users.noreply.github.com>
Co-authored-by: Vandana <forge@getflash.io>

* Send Bridge cashout USDT to transfer deposit addresses (#407)

* feat(bridge): send cashout USDT to Bridge deposits

* fix: show pending bridge cashouts in erp

* fix: omit idempotency key when deleting bridge transfers

* fix: preserve accepted bridge cashout sends

* chore(graphql): regenerate SDL + supergraph for bridgeCreateExternalAccount

The bridgeCreateExternalAccount resolver was registered in
src/graphql/public/mutations.ts but the generated schema.graphql and the
Apollo supergraph were never regenerated, so the field was absent from the
SDL/supergraph (and would fail check:sdl in CI). Regenerate both so the
mutation is exposed on the public API.

---------

Co-authored-by: Vandana <forge@getflash.io>

* feat: Add notification on Kyc system and extend notification for deposit and withdrawal processes. (#414)

* feat: support deposit notification outcoumes (funds received, processing, completed)
Add an outcome parameter to deposit push notifications so each lifecycle stage can use its own title and body.

* feat: send withdrawal push notifications on submit and USDT send
Notify users when a withdrawal is submitted to Bridge and when USDT is sent, and centralize send-failure handling with push alerts.

* feat: add kyc push notification module with imcomplete deep-link support
send localized kyc status notifications, skip pushes before initiation, use a dedicated incomplete message, and attach latest KYC links for incomplete notifications.

* feat: wire kyc webhook status changes to push notifications
Notify users on KYC status transitions from bridge webhook handler, with guards for unchanged status and pre-initiation states.

* feat(bridge): wire withdrawal submit and USDT-sent push notifications

Notify users when a withdrawal is submitted to Bridge and when USDT is sent.
Centralize send-failure handling with failed push alerts and add unit tests
for the new notification outcomes.

* fix: send developer_fee on Bridge withdrawal transfers
bridge only applies developer_fee_percent when flexible_amount is enabled, fixed amount offramps (implemented in flash) must use developer_fee (USD value), so pass the stored flashFee from the pending withdrawal instead

* fix: harden bridge webhook remediation

* fix: restore public ibex client install

* fix: point quickstart vendir at flash repo

* fix: clean up bridge remediation ci noise

* fix: address bridge ci security findings

* fix: repair bridge ci followups

* fix: provide config for integration tests

* fix: repair integration ibex mock

* fix: complete integration ibex mock

* fix: avoid duplicate integration wallet ids

* ci: run checks on the bridge-rebase integration branch (#408)

Test/check workflows were filtered to `pull_request: branches: [main]`,
so PRs targeting the long-lived `tmp/bridge-rebase-pr-ready` integration
branch never triggered CI. Add the integration branch to the
pull_request filter on all gating workflows, and add a push trigger on
the core build/test/lint workflows (check-code, unit-test,
integration-test) so the branch tip is checked as PRs stack onto it.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: route quickstart through flash service (#415)

* fix: route quickstart through flash service

* fix: make quickstart use flash config

* docs: fix bridge sandbox mongodb example

---------

Co-authored-by: Patoo <262265744+patoo0x@users.noreply.github.com>

* test: clean up bridge CI checks

* ci: retry supergraph composition

* fix: harden ibex webhook routes

* fix(bridge): use shared BridgeApiError class so instanceof checks work

client.ts defined its own BridgeApiError (extends Error) while the rest
of the codebase imports BridgeApiError from errors.ts (extends
BridgeError → DomainError). The catch block in addExternalAccount uses
the errors.ts version for its instanceof check, so it never matched the
error thrown by the client — the 401 from the Plaid link endpoint was
surfaced as a generic BRIDGE_API_ERROR instead of BRIDGE_PLAID_NOT_AVAILABLE,
preventing the mobile app from falling back to manual bank entry.

Remove the duplicate class from client.ts and import from ./errors.

* fix: delegate account-scoped crypto receive info to ibex-client 3.1.x (#419)

* fix: delegate account-scoped crypto receive info to ibex-client 3.1.x
ibex-client
3.1.x exposes the same contract, so replace the raw ibexPost wrapper with the
SDK and align CryptoReceiveInfo types with the library.

* chore: update yarn.lock for ibex-client 3.1.x

* style(ibex): fix prettier formatting in findEthereumUsdtReceiveOption

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cash-wallet-cutover): preserve precise USD migration amounts (#420)

* fix(wallets): return USDT transaction amounts in cents instead of micros

* fix(notifications): format USDT pushes as USD (#422)

Co-authored-by: Vandana <forge@getflash.io>

* feat(topup): add topup.enabled flag exposed via Globals (#421)

Adds an instance-wide `topup.enabled` config flag (default OFF) and surfaces
it to clients on the Globals query as `topupEnabled`, so the mobile app can
gate the two top-up entry points it owns — the credit-card webview and the
bank-transfer-to-support handoff — via YAML + rolling restart, consistent
with how cashout/bridge are flagged (and with the KILL_MONEY SOP).

International bank top-up is intentionally NOT covered here: it flows through
the bridge and is already gated by `bridge.enabled`.

- config: schema.ts + schema.types.d.ts + yaml.ts (Topup.Enabled); default
  false and optional, so it's non-breaking for existing deploys
- graphql: Globals.topupEnabled field + resolver wiring
- regenerated src/graphql/public/schema.graphql
- dev base-config.yaml: explicit topup.enabled: false for parity with cashout

Co-authored-by: Dread <bobodread@bobodread.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(bridge): harden webhook and withdrawal guards (#424)

* fix(bridge): harden webhook and withdrawal guards

* fix(bridge): close review gaps in webhook/withdrawal hardening

Follow-ups from review of this PR:

- return-shapes.spec.ts: mock BridgeClient.getCustomer (status "active")
  — the new live-KYC check made initiateWithdrawal call getCustomer, which
  this suite never stubbed, so the contract test got a TypeError (CI red).

- replay ingress: grant the loopback exemption from the socket address
  only. request-ip prefers X-Forwarded-For, which any external caller can
  set to "127.0.0.1" and walk through the gate without knowing the
  allowlist. Loopback now reads req.socket.remoteAddress; the allowlist
  path still uses request-ip (meaningful only behind trusted-XFF ingress).
  Added a spoofed-XFF regression test.

- rate limiters (crypto-receive, bridge webhook, replay): disable
  express-rate-limit v7's xForwardedForHeader validation. Without Express
  `trust proxy`, any request carrying X-Forwarded-For (i.e. anything
  behind an LB) throws ERR_ERL_UNEXPECTED_X_FORWARDED_FOR and 500s —
  which would have broken every IBEX crypto-receive webhook in prod.
  Unset trust proxy now degrades to one shared bucket keyed on the LB
  socket address instead of an outage; set `trust proxy` for per-sender
  buckets.

- authenticate.spec.ts: pin the fail-closed behavior for unconfigured
  secrets (the old `!==` compare passed when both sides were undefined,
  silently disabling auth).

Full unit suite green locally (104 suites / 783 passed), tsc-check and
eslint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Dread <bobodread@bobodread.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(bridge): manage external account defaults (#423)

* feat(bridge): manage external account defaults

* test(bridge): mock default external account helper

---------

Co-authored-by: Patoo <262265744+patoo0x@users.noreply.github.com>

* fix(bridge): harden mongoose query filters

---------

Co-authored-by: Vandana <forge@getflash.io>
Co-authored-by: Olaniran ⚡ <93789719+heyolaniran@users.noreply.github.com>
Co-authored-by: Patoo <patoo@getflash.io>
Co-authored-by: Dread <bobodread@bobodread.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: heyolaniran <olaniran.abd@gmail.com>
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.

3 participants