Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
07a7e8b
feat(bridge): provision ETH-USDT cash wallet
forge0x May 9, 2026
92adf91
ENG-394 create USDT wallet for new accounts
forge0x May 12, 2026
afb41ba
fix(wallets): return empty list when account has no wallets
heyolaniran May 12, 2026
4fc1d6e
fix(graphql): pass wallet currency into Ibex balance reads
heyolaniran May 12, 2026
a095d2b
fix(ibex): authenticate via API base /auth/signin
heyolaniran May 12, 2026
8fb99e8
fix(bridge): VA idempotency before USDT wallet; checking USDT for wit…
heyolaniran May 12, 2026
f3e3929
feat(bridge): unique partial index on pending withdrawals; handle E11000
heyolaniran May 12, 2026
3c4d83a
test(bridge): ENG-296 VA idempotency and bridgeEthereumAddress assert…
heyolaniran May 12, 2026
96dab87
chore(bruno): update local environment variables
heyolaniran May 12, 2026
e63e1b4
fix(bridge): align crypto receive webhook to ethereum usdt
forge0x May 12, 2026
861d401
fix: Ibex USDT address
heyolaniran May 13, 2026
d83f331
fix: remove logs from the process
heyolaniran May 13, 2026
c4cbce9
fix: Ibex Client operations
heyolaniran May 13, 2026
b02cdd1
fix: Ibex types
heyolaniran May 13, 2026
7df4302
fix: Ibex account creation
heyolaniran May 13, 2026
e8397a7
Add orphan status and resolution fields to Bridge reconciliation Mong…
heyolaniran May 11, 2026
92b2191
Add PubSub trigger for Bridge↔IBEX reconciliation updates
heyolaniran May 11, 2026
08b870f
Tighten batch reconciliation and add reconcileByTxHash
heyolaniran May 11, 2026
55bc082
Trigger real-time Bridge↔IBEX reconciliation from webhooks
heyolaniran May 11, 2026
4534aee
Expose Bridge reconciliation orphans and live events over GraphQL
heyolaniran May 11, 2026
d77fee8
Align scheduled and CLI reconciliation with the 15-minute safety net
heyolaniran May 11, 2026
3dfd6d6
Add unit tests for Bridge↔IBEX reconciliation helpers
heyolaniran May 11, 2026
7d36fc9
Document Bridge↔IBEX deposit reconciliation design
heyolaniran May 11, 2026
cd8a3e3
fix(ibex): validate Ethereum USDT on crypto-receive webhook
heyolaniran May 12, 2026
bdfe9e4
fix: return existing link if the account has incomplete hosted kyc st…
heyolaniran May 14, 2026
d9db77a
fix(graphql): register UsdtWallet in admin schema interface types
heyolaniran May 15, 2026
f1da0c0
fix(bridge): address PR review feedback
heyolaniran May 15, 2026
8080684
chore(graphql): remove unused bridge reconciliation public types
heyolaniran May 15, 2026
7073a9d
chore: codegen SDL cleanup and drop stale reconciliation doc
heyolaniran May 15, 2026
6b95936
fix(ibex): return Promise from reconcileByTxHash mock in crypto-recei…
heyolaniran May 15, 2026
04d53a5
chore: regenerate GraphQL SDL and supergraph from codegen
heyolaniran May 16, 2026
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
2 changes: 1 addition & 1 deletion dev/apollo-federation/supergraph.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1646,7 +1646,6 @@ type Query
@join__type(graph: PUBLIC)
{
accountDefaultWallet(username: Username!, walletCurrency: WalletCurrency): PublicWallet!
latestAccountUpgradeRequest: AccountUpgradeRequestPayload!
bridgeExternalAccounts: [BridgeExternalAccount]
bridgeKycStatus: String
bridgeVirtualAccount: BridgeVirtualAccount
Expand All @@ -1657,6 +1656,7 @@ type Query
currencyList: [Currency!]!
globals: Globals
isFlashNpub(input: IsFlashNpubInput!): IsFlashNpubPayload
latestAccountUpgradeRequest: AccountUpgradeRequestPayload!
lnInvoicePaymentStatus(input: LnInvoicePaymentStatusInput!): LnInvoicePaymentStatusPayload!
me: User
mobileVersions: [MobileVersions]
Expand Down
4 changes: 2 additions & 2 deletions dev/bruno/Flash GraphQL API/environments/local.bru
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ vars {
token:
walletId:
walletIdUsd: c593736e-5a58-42e4-93fa-dc895856c1f1
userEmail: maurienteso@gmail.com
userFullName: maurienteso
userEmail: mauriente@gmail.com
userFullName: maurientes
}
5 changes: 4 additions & 1 deletion src/app/authentication/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,10 @@ export const loginDeviceUpgradeWithPhone = async ({
if (deviceWallets instanceof Error) return deviceWallets
let deviceAccountHasBalance = false
for (const wallet of deviceWallets) {
const balance = await getBalanceForWallet({ walletId: wallet.id })
const balance = await getBalanceForWallet({
walletId: wallet.id,
currency: wallet.currency,
})
if (balance instanceof Error) return balance
if (!balance.isZero()) {
deviceAccountHasBalance = true
Expand Down
1 change: 1 addition & 0 deletions src/domain/pubsub/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const PubSubDefaultTriggers = {
UserPriceUpdate: "USER_PRICE_UPDATE",
AccountUpdate: "ACCOUNT_UPDATE",
LnPaymentStatus: "LN_PAYMENT_STATUS",
BridgeReconciliationUpdate: "BRIDGE_RECONCILIATION_UPDATE",
} as const

export const customPubSubTrigger = ({
Expand Down
2 changes: 2 additions & 0 deletions src/graphql/admin/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import AccountDetailsByAccountId from "./root/query/account-details-by-account-i
import MerchantsPendingApprovalQuery from "./root/query/merchants-pending-approval-listing"
import IdDocumentReadUrlQuery from "./root/query/id-document-read-url"
import NotificationTopicsQuery from "./root/query/notification-topics"
import BridgeReconciliationOrphansQuery from "./root/query/bridge-reconciliation-orphans"

export const queryFields = {
unauthed: {},
Expand All @@ -34,6 +35,7 @@ export const queryFields = {
merchantsPendingApproval: MerchantsPendingApprovalQuery,
idDocumentReadUrl: IdDocumentReadUrlQuery,
notificationTopics: NotificationTopicsQuery,
bridgeReconciliationOrphans: BridgeReconciliationOrphansQuery,
},
}

Expand Down
40 changes: 40 additions & 0 deletions src/graphql/admin/root/query/bridge-reconciliation-orphans.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { GT } from "@graphql/index"
import BridgeReconciliationOrphanObject from "@graphql/admin/types/object/bridge-reconciliation-orphan"
import { findOrphans } from "@services/mongoose/bridge-reconciliation-orphan"

const BridgeReconciliationOrphansQuery = GT.Field({
type: GT.NonNullList(BridgeReconciliationOrphanObject),
args: {
status: { type: GT.String, defaultValue: null },
orphanType: { type: GT.String, defaultValue: null },
limit: { type: GT.Int, defaultValue: 50 },
},
resolve: async (
_: unknown,
{
status,
orphanType,
limit,
}: { status?: string; orphanType?: string; limit?: number },
) => {
const result = await findOrphans({
status: status as "unmatched" | "resolved" | undefined,
orphanType: orphanType as
| "bridge_without_ibex"
| "ibex_without_bridge"
| undefined,
limit: limit ?? 50,
})

if (result instanceof Error) throw result

return result.map((o) => ({
...o,
detectedAt: o.detectedAt.toISOString(),
resolvedAt: o.resolvedAt?.toISOString() ?? null,
triageContext: JSON.stringify(o.triageContext),
}))
},
})

export default BridgeReconciliationOrphansQuery
60 changes: 60 additions & 0 deletions src/graphql/admin/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,21 @@ type BTCWallet implements Wallet {
walletCurrency: WalletCurrency!
}

type BridgeReconciliationOrphan {
amount: String
currency: String
customerId: String
detectedAt: String!
id: ID!
orphanKey: String!
orphanType: String!
resolvedAt: String
status: String!
transferId: String
triageContext: String!
txHash: String
}

input BusinessDeleteMapInfoInput {
username: Username!
}
Expand Down Expand Up @@ -319,6 +334,7 @@ type Query {
accountDetailsByUserPhone(phone: Phone!): AuditedAccount!
accountDetailsByUsername(username: Username!): AuditedAccount!
allLevels: [AccountLevel!]!
bridgeReconciliationOrphans(limit: Int = 50, orphanType: String = null, status: String = null): [BridgeReconciliationOrphan!]!
idDocumentReadUrl(
"""Storage key of the ID document file"""
fileKey: String!
Expand Down Expand Up @@ -551,6 +567,50 @@ type UsdWallet implements Wallet {
walletCurrency: WalletCurrency!
}

"""
A wallet belonging to an account which contains a USDT balance and a list of transactions.
"""
type UsdtWallet implements Wallet {
accountId: ID!
balance: FractionalCentAmount!
id: ID!
isExternal: Boolean!
lnurlp: Lnurl

"""An unconfirmed incoming onchain balance."""
pendingIncomingBalance: SignedAmount!
transactions(
"""Returns the items in the list that come after the specified cursor."""
after: String

"""Returns the items in the list that come before the specified cursor."""
before: String

"""Returns the first n items from the list."""
first: Int

"""Returns the last n items from the list."""
last: Int
): TransactionConnection
transactionsByAddress(
"""Returns the items that include this address."""
address: OnChainAddress!

"""Returns the items in the list that come after the specified cursor."""
after: String

"""Returns the items in the list that come before the specified cursor."""
before: String

"""Returns the first n items from the list."""
first: Int

"""Returns the last n items from the list."""
last: Int
): TransactionConnection
walletCurrency: WalletCurrency!
}

input UserUpdatePhoneInput {
accountUuid: ID!
phone: Phone!
Expand Down
3 changes: 2 additions & 1 deletion src/graphql/admin/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import BtcWallet from "@graphql/shared/types/object/btc-wallet"
import GraphQLApplicationError from "@graphql/shared/types/object/graphql-application-error"
import UsdWallet from "@graphql/shared/types/object/usd-wallet"
import UsdtWallet from "@graphql/shared/types/object/usdt-wallet"

export const ALL_INTERFACE_TYPES = [GraphQLApplicationError, BtcWallet, UsdWallet]
export const ALL_INTERFACE_TYPES = [GraphQLApplicationError, BtcWallet, UsdWallet, UsdtWallet]
21 changes: 21 additions & 0 deletions src/graphql/admin/types/object/bridge-reconciliation-orphan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { GT } from "@graphql/index"

const BridgeReconciliationOrphanObject = GT.Object({
name: "BridgeReconciliationOrphan",
fields: () => ({
id: { type: GT.NonNullID },
orphanKey: { type: GT.NonNull(GT.String) },
orphanType: { type: GT.NonNull(GT.String) },
status: { type: GT.NonNull(GT.String) },
txHash: { type: GT.String },
transferId: { type: GT.String },
customerId: { type: GT.String },
amount: { type: GT.String },
currency: { type: GT.String },
detectedAt: { type: GT.NonNull(GT.String) },
resolvedAt: { type: GT.String },
triageContext: { type: GT.NonNull(GT.String) },
}),
})

export default BridgeReconciliationOrphanObject
7 changes: 5 additions & 2 deletions src/graphql/public/root/mutation/onchain-payment-send-all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import WalletId from "@graphql/shared/types/scalar/wallet-id"

import { Wallets } from "@app"
import { getBalanceForWallet } from "@app/wallets"
import { USDAmount } from "@domain/shared"
import { USDAmount, WalletCurrency } from "@domain/shared"

const OnChainPaymentSendAllInput = GT.Input({
name: "OnChainPaymentSendAllInput",
Expand Down Expand Up @@ -63,7 +63,10 @@ const OnChainPaymentSendAllMutation = GT.Field<
return { errors: [{ message: speed.message }] }
}

const amount = await getBalanceForWallet({ walletId })
const amount = await getBalanceForWallet({
walletId,
currency: WalletCurrency.Usd,
})
if (amount instanceof Error) return amount
if (!(amount instanceof USDAmount)) {
return { errors: [{ message: "Onchain payments require a USD wallet" }] }
Expand Down
2 changes: 1 addition & 1 deletion src/graphql/public/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1290,7 +1290,6 @@ type PublicWallet {

type Query {
accountDefaultWallet(username: Username!, walletCurrency: WalletCurrency): PublicWallet!
latestAccountUpgradeRequest: AccountUpgradeRequestPayload!
bridgeExternalAccounts: [BridgeExternalAccount]
bridgeKycStatus: String
bridgeVirtualAccount: BridgeVirtualAccount
Expand All @@ -1301,6 +1300,7 @@ type Query {
currencyList: [Currency!]!
globals: Globals
isFlashNpub(input: IsFlashNpubInput!): IsFlashNpubPayload
latestAccountUpgradeRequest: AccountUpgradeRequestPayload!
lnInvoicePaymentStatus(input: LnInvoicePaymentStatusInput!): LnInvoicePaymentStatusPayload!
me: User
mobileVersions: [MobileVersions]
Expand Down
5 changes: 4 additions & 1 deletion src/graphql/shared/types/object/btc-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ const BtcWallet = GT.Object<Wallet>({
description: "A balance stored in BTC.",
resolve: async (source) => {
if (source.type === WalletType.External) return null
const balanceSats = await Wallets.getBalanceForWallet({ walletId: source.id })
const balanceSats = await Wallets.getBalanceForWallet({
walletId: source.id,
currency: source.currency,
})
if (balanceSats instanceof Error) {
throw mapError(balanceSats)
}
Expand Down
6 changes: 3 additions & 3 deletions src/scripts/reconcile-bridge-ibex-deposits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import { reconcileBridgeAndIbexDeposits } from "@services/bridge/reconciliation"
const args = yargs(hideBin(process.argv))
.option("window-hours", {
type: "number",
default: 24,
describe: "Reconciliation window in hours",
default: 0.25,
describe: "Reconciliation window in hours (default: 15 minutes)",
})
.option("configPath", { type: "string", demandOption: true })
.parseSync()

const main = async () => {
const windowMs = Math.max(1, Math.floor(args["window-hours"])) * 60 * 60 * 1000
const windowMs = args["window-hours"] * 60 * 60 * 1000
const result = await reconcileBridgeAndIbexDeposits({ windowMs })
if (result instanceof Error) throw result
baseLogger.info(result, "Bridge↔IBEX reconciliation finished")
Expand Down
6 changes: 5 additions & 1 deletion src/servers/cron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,14 @@ const swapOutJob = async () => {
if (swapResult instanceof Error) throw swapResult
}

// Window covers 15 min of events — real-time webhook reconciliation handles everything
// else immediately. This batch pass is only a safety net for missed/delayed webhooks.
const RECONCILE_WINDOW_MS = 15 * 60 * 1000

const reconcileBridgeDepositsJob = async () => {
if (!BridgeConfig.enabled) return

const result = await reconcileBridgeAndIbexDeposits()
const result = await reconcileBridgeAndIbexDeposits({ windowMs: RECONCILE_WINDOW_MS })
if (result instanceof Error) throw result
}

Expand Down
Loading