Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 26 additions & 3 deletions dev/apollo-federation/supergraph.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,28 @@ type BridgeCancelWithdrawalRequestPayload
withdrawal: BridgeWithdrawal
}

input BridgeCreateExternalAccountInput
@join__type(graph: PUBLIC)
{
accountNumber: String!
accountOwnerName: String!
bankName: String!
checkingOrSavings: String = "checking"
city: String!
country: String!
postalCode: String!
routingNumber: String!
state: String!
streetLine1: String!
}

type BridgeCreateExternalAccountPayload
@join__type(graph: PUBLIC)
{
errors: [Error!]!
externalAccount: BridgeExternalAccount
}

type BridgeCreateVirtualAccountPayload
@join__type(graph: PUBLIC)
{
Expand Down Expand Up @@ -379,13 +401,13 @@ type BridgeWithdrawal
bridgeTransferId: String
createdAt: String!
currency: String!
externalAccountId: String
failureReason: String
finalAmount: String
estimatedBridgeFee: String
estimatedBridgeFeePercent: String
estimatedCustomerFee: String
estimatedGasBuffer: String
externalAccountId: String
failureReason: String
finalAmount: String
flashFee: String
flashFeeIsEstimate: Boolean!
flashFeeNotice: String
Expand Down Expand Up @@ -1273,6 +1295,7 @@ type Mutation
accountUpdateDisplayCurrency(input: AccountUpdateDisplayCurrencyInput!): AccountUpdateDisplayCurrencyPayload!
bridgeAddExternalAccount: BridgeAddExternalAccountPayload!
bridgeCancelWithdrawalRequest(input: BridgeCancelWithdrawalRequestInput!): BridgeCancelWithdrawalRequestPayload!
bridgeCreateExternalAccount(input: BridgeCreateExternalAccountInput!): BridgeCreateExternalAccountPayload!
bridgeCreateVirtualAccount: BridgeCreateVirtualAccountPayload!
bridgeInitiateKyc(input: BridgeInitiateKycInput!): BridgeInitiateKycPayload!
bridgeInitiateWithdrawal(input: BridgeInitiateWithdrawalInput!): BridgeInitiateWithdrawalPayload!
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"graphql-ws": "^5.13.1",
"gt3-server-node-express-sdk": "https://github.com/GaloyMoney/gt3-server-node-express-bypass#master",
"i18n": "^0.15.1",
"ibex-client": "^3.0.0",
"ibex-client": "github:lnflash/ibex-client#28f4a784cb59e033f49257f22a437b68c95fd94b",
"invoices": "^3.0.0",
"ioredis": "^5.3.2",
"ioredis-cache": "^2.0.0",
Expand Down
2 changes: 2 additions & 0 deletions src/graphql/admin/root/query/bridge-reconciliation-orphans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const BridgeReconciliationOrphansQuery = GT.Field({
orphanType: orphanType as
| "bridge_without_ibex"
| "ibex_without_bridge"
| "bridge_transfer_without_ibex_send"
| "ibex_send_without_bridge_settlement"
| undefined,
limit: limit ?? 50,
})
Expand Down
16 changes: 16 additions & 0 deletions src/graphql/error-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,15 @@ export const mapError = (error: ApplicationError): CustomApolloError => {
message,
})

case "BridgeDepositInstructionsMissingError":
message =
error.message ||
"Bridge did not return crypto deposit instructions for this withdrawal"
return bridgeGqlError({
code: "BRIDGE_DEPOSIT_INSTRUCTIONS_MISSING",
message,
})

case "BridgeWebhookValidationError":
message = "Invalid webhook signature"
return bridgeGqlError({
Expand All @@ -620,6 +629,13 @@ export const mapError = (error: ApplicationError): CustomApolloError => {
message,
})

case "BridgePlaidNotAvailableError":
message = error.message || "Plaid bank account linking is not available"
return bridgeGqlError({
code: "BRIDGE_PLAID_NOT_AVAILABLE",
message,
})

case "BridgeError":
message = error.message || "Bridge API error"
return bridgeGqlError({ code: "BRIDGE_ERROR", message })
Expand Down
2 changes: 2 additions & 0 deletions src/graphql/public/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import UpdateExternalWalletMutation from "./root/mutation/update-external-wallet
import BridgeInitiateKycMutation from "./root/mutation/bridge-initiate-kyc"
import BridgeCreateVirtualAccountMutation from "./root/mutation/bridge-create-virtual-account"
import BridgeAddExternalAccountMutation from "./root/mutation/bridge-add-external-account"
import BridgeCreateExternalAccountMutation from "./root/mutation/bridge-create-external-account"
import BridgeRequestWithdrawalMutation from "./root/mutation/bridge-request-withdrawal"
import BridgeInitiateWithdrawalMutation from "./root/mutation/bridge-initiate-withdrawal"
import BridgeCancelWithdrawalRequestMutation from "./root/mutation/bridge-cancel-withdrawal-request"
Expand Down Expand Up @@ -125,6 +126,7 @@ export const mutationFields = {
bridgeInitiateKyc: BridgeInitiateKycMutation,
bridgeCreateVirtualAccount: BridgeCreateVirtualAccountMutation,
bridgeAddExternalAccount: BridgeAddExternalAccountMutation,
bridgeCreateExternalAccount: BridgeCreateExternalAccountMutation,
bridgeRequestWithdrawal: BridgeRequestWithdrawalMutation,
bridgeInitiateWithdrawal: BridgeInitiateWithdrawalMutation,
bridgeCancelWithdrawalRequest: BridgeCancelWithdrawalRequestMutation,
Expand Down
79 changes: 79 additions & 0 deletions src/graphql/public/root/mutation/bridge-create-external-account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { GT } from "@graphql/index"
import { mapAndParseErrorForGqlResponse } from "@graphql/error-map"
import IError from "@graphql/shared/types/abstract/error"
import BridgeExternalAccount from "@graphql/public/types/object/bridge-external-account"
import BridgeCreateExternalAccountInput from "@graphql/public/types/input/bridge-create-external-account-input"
import { BridgeConfig } from "@config"
import BridgeService from "@services/bridge"
import { BridgeDisabledError, BridgeAccountLevelError } from "@services/bridge/errors"

const BridgeCreateExternalAccountPayload = GT.Object({
name: "BridgeCreateExternalAccountPayload",
fields: () => ({
errors: { type: GT.NonNullList(IError) },
externalAccount: { type: BridgeExternalAccount },
}),
})

const bridgeCreateExternalAccount = GT.Field({
type: GT.NonNull(BridgeCreateExternalAccountPayload),
args: {
input: { type: GT.NonNull(BridgeCreateExternalAccountInput) },
},
resolve: async (
_,
{
input,
}: {
input: {
bankName: string
accountNumber: string
routingNumber: string
accountOwnerName: string
checkingOrSavings?: string
streetLine1: string
city: string
state: string
postalCode: string
country: string
}
},
{ domainAccount }: GraphQLPublicContextAuth,
) => {
if (!BridgeConfig.enabled) {
return { errors: [mapAndParseErrorForGqlResponse(new BridgeDisabledError())] }
}

if (!domainAccount || domainAccount.level <= 0) {
return { errors: [mapAndParseErrorForGqlResponse(new BridgeAccountLevelError())] }
}

const result = await BridgeService.createExternalAccount(domainAccount.id, {
account_owner_name: input.accountOwnerName,
bank_name: input.bankName,
currency: "usd",
account_type: "us",
account: {
account_number: input.accountNumber,
routing_number: input.routingNumber,
checking_or_savings:
(input.checkingOrSavings as "checking" | "savings") ?? "checking",
},
address: {
street_line_1: input.streetLine1,
city: input.city,
state: input.state,
postal_code: input.postalCode,
country: input.country,
},
})

if (result instanceof Error) {
return { errors: [mapAndParseErrorForGqlResponse(result)] }
}

return { externalAccount: result, errors: [] }
},
})

export default bridgeCreateExternalAccount
25 changes: 22 additions & 3 deletions src/graphql/public/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,24 @@
withdrawal: BridgeWithdrawal
}

input BridgeCreateExternalAccountInput {

Check notice on line 258 in src/graphql/public/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Type 'BridgeCreateExternalAccountInput' was added

Type 'BridgeCreateExternalAccountInput' was added
accountNumber: String!

Check notice on line 259 in src/graphql/public/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Input field 'accountNumber' of type 'String!' was added to input object type 'BridgeCreateExternalAccountInput'

The field is being added to a new type.
accountOwnerName: String!

Check notice on line 260 in src/graphql/public/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Input field 'accountOwnerName' of type 'String!' was added to input object type 'BridgeCreateExternalAccountInput'

The field is being added to a new type.
bankName: String!

Check notice on line 261 in src/graphql/public/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Input field 'bankName' of type 'String!' was added to input object type 'BridgeCreateExternalAccountInput'

The field is being added to a new type.
checkingOrSavings: String = "checking"

Check notice on line 262 in src/graphql/public/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Input field 'checkingOrSavings' of type 'String' with default value '"checking"' was added to input object type 'BridgeCreateExternalAccountInput'

The field is being added to a new type.
city: String!

Check notice on line 263 in src/graphql/public/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Input field 'city' of type 'String!' was added to input object type 'BridgeCreateExternalAccountInput'

The field is being added to a new type.
country: String!

Check notice on line 264 in src/graphql/public/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Input field 'country' of type 'String!' was added to input object type 'BridgeCreateExternalAccountInput'

The field is being added to a new type.
postalCode: String!

Check notice on line 265 in src/graphql/public/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Input field 'postalCode' of type 'String!' was added to input object type 'BridgeCreateExternalAccountInput'

The field is being added to a new type.
routingNumber: String!

Check notice on line 266 in src/graphql/public/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Input field 'routingNumber' of type 'String!' was added to input object type 'BridgeCreateExternalAccountInput'

The field is being added to a new type.
state: String!

Check notice on line 267 in src/graphql/public/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Input field 'state' of type 'String!' was added to input object type 'BridgeCreateExternalAccountInput'

The field is being added to a new type.
streetLine1: String!

Check notice on line 268 in src/graphql/public/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Input field 'streetLine1' of type 'String!' was added to input object type 'BridgeCreateExternalAccountInput'

The field is being added to a new type.
}

type BridgeCreateExternalAccountPayload {

Check notice on line 271 in src/graphql/public/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Type 'BridgeCreateExternalAccountPayload' was added

Type 'BridgeCreateExternalAccountPayload' was added
errors: [Error!]!

Check notice on line 272 in src/graphql/public/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Field 'errors' was added to object type 'BridgeCreateExternalAccountPayload'

Field 'errors' was added to object type 'BridgeCreateExternalAccountPayload'
externalAccount: BridgeExternalAccount

Check notice on line 273 in src/graphql/public/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Field 'externalAccount' was added to object type 'BridgeCreateExternalAccountPayload'

Field 'externalAccount' was added to object type 'BridgeCreateExternalAccountPayload'
}

type BridgeCreateVirtualAccountPayload {
errors: [Error!]!
virtualAccount: BridgeVirtualAccount
Expand Down Expand Up @@ -326,13 +344,13 @@
bridgeTransferId: String
createdAt: String!
currency: String!
externalAccountId: String
failureReason: String
finalAmount: String
estimatedBridgeFee: String
estimatedBridgeFeePercent: String
estimatedCustomerFee: String
estimatedGasBuffer: String
externalAccountId: String
failureReason: String
finalAmount: String
flashFee: String
flashFeeIsEstimate: Boolean!
flashFeeNotice: String
Expand Down Expand Up @@ -989,6 +1007,7 @@
accountUpdateDisplayCurrency(input: AccountUpdateDisplayCurrencyInput!): AccountUpdateDisplayCurrencyPayload!
bridgeAddExternalAccount: BridgeAddExternalAccountPayload!
bridgeCancelWithdrawalRequest(input: BridgeCancelWithdrawalRequestInput!): BridgeCancelWithdrawalRequestPayload!
bridgeCreateExternalAccount(input: BridgeCreateExternalAccountInput!): BridgeCreateExternalAccountPayload!

Check notice on line 1010 in src/graphql/public/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Field 'bridgeCreateExternalAccount' was added to object type 'Mutation'

Field 'bridgeCreateExternalAccount' was added to object type 'Mutation'

Check notice on line 1010 in src/graphql/public/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Argument 'input: BridgeCreateExternalAccountInput!' added to field 'Mutation.bridgeCreateExternalAccount'

Argument 'input: BridgeCreateExternalAccountInput!' added to field 'Mutation.bridgeCreateExternalAccount'
bridgeCreateVirtualAccount: BridgeCreateVirtualAccountPayload!
bridgeInitiateKyc(input: BridgeInitiateKycInput!): BridgeInitiateKycPayload!
bridgeInitiateWithdrawal(input: BridgeInitiateWithdrawalInput!): BridgeInitiateWithdrawalPayload!
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { GT } from "@graphql/index"

const BridgeCreateExternalAccountInput = GT.Input({
name: "BridgeCreateExternalAccountInput",
fields: () => ({
bankName: { type: GT.NonNull(GT.String) },
accountNumber: { type: GT.NonNull(GT.String) },
routingNumber: { type: GT.NonNull(GT.String) },
accountOwnerName: { type: GT.NonNull(GT.String) },
checkingOrSavings: { type: GT.String, defaultValue: "checking" },
streetLine1: { type: GT.NonNull(GT.String) },
city: { type: GT.NonNull(GT.String) },
state: { type: GT.NonNull(GT.String) },
postalCode: { type: GT.NonNull(GT.String) },
country: { type: GT.NonNull(GT.String) },
}),
})

export default BridgeCreateExternalAccountInput
15 changes: 14 additions & 1 deletion src/servers/cron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import {
import { baseLogger } from "@services/logger"
import { setupMongoConnection } from "@services/mongodb"
import { activateLndHealthCheck, checkAllLndHealth } from "@services/lnd/health"
import { reconcileBridgeAndIbexDeposits } from "@services/bridge/reconciliation"
import {
reconcileBridgeAndIbexDeposits,
reconcileBridgeAndIbexWithdrawals,
} from "@services/bridge/reconciliation"

import { elapsedSinceTimestamp, sleep } from "@utils"
import { rebalancingInternalChannels } from "@services/lnd/rebalancing"
Expand Down Expand Up @@ -76,6 +79,15 @@ const reconcileBridgeDepositsJob = async () => {
if (result instanceof Error) throw result
}

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

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

const main = async () => {
console.log("cronjob started")
const start = new Date()
Expand All @@ -97,6 +109,7 @@ const main = async () => {
...(cronConfig.rebalanceEnabled ? [rebalance] : []),
...(cronConfig.swapEnabled ? [swapOutJob] : []),
reconcileBridgeDepositsJob,
reconcileBridgeWithdrawalsJob,
deleteExpiredPaymentFlows,
deleteExpiredInvoices,
deleteLndPaymentsBefore2Months,
Expand Down
12 changes: 10 additions & 2 deletions src/services/alerts/ibex-bridge-movement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ export const alertIbexReconciliationOrphan = ({
reason,
context,
}: {
orphanType: "bridge_without_ibex" | "ibex_without_bridge"
orphanType:
| "bridge_without_ibex"
| "ibex_without_bridge"
| "bridge_transfer_without_ibex_send"
| "ibex_send_without_bridge_settlement"
txHash?: string
transferId?: string
reason: string
Expand All @@ -60,7 +64,11 @@ export const alertIbexReconciliationOrphan = ({
const title =
orphanType === "ibex_without_bridge"
? "IBEX crypto receive without matching Bridge deposit"
: "Bridge deposit without matching IBEX crypto receive"
: orphanType === "bridge_without_ibex"
? "Bridge deposit without matching IBEX crypto receive"
: orphanType === "bridge_transfer_without_ibex_send"
? "Bridge withdrawal transfer without matching IBEX send"
: "IBEX withdrawal send without Bridge settlement"

alertIbexMovement(dedupKey, {
title,
Expand Down
17 changes: 11 additions & 6 deletions src/services/bridge/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ export interface CreateExternalAccountRequest {
street_line_1: string
city: string
country: string
state?: string
postal_code?: string
}
account_type: string | "us" | "iban" | "unknown" | "clabe" | "pix" | "gb"
currency: "usd" | "gbp" | "brl" | "eur" | string
Expand Down Expand Up @@ -203,8 +205,10 @@ export interface ExternalAccount {
currency: string
bank_name?: string
account_number_last_4?: string
last_4?: string
routing_number?: string
iban?: string
active?: boolean
created_at: string
}

Expand Down Expand Up @@ -373,8 +377,8 @@ export class BridgeClient {
"Content-Type": "application/json",
}

// Bridge rejects Idempotency-Key on some GET endpoints (e.g. /webhook_events).
if (method.toUpperCase() !== "GET") {
// Bridge rejects Idempotency-Key on GET and DELETE endpoints.
if (!["GET", "DELETE"].includes(method.toUpperCase())) {
if (idempotencyKey) {
headers["Idempotency-Key"] = idempotencyKey
} else {
Expand Down Expand Up @@ -556,14 +560,15 @@ export class BridgeClient {
return this.request<Transfer>("POST", "/transfers", bodyWithCustomer, idempotencyKey)
}

async getTransfer(
customerId: BridgeCustomerId,
transferId: BridgeTransferId,
): Promise<Transfer> {
async getTransfer(transferId: BridgeTransferId): Promise<Transfer> {
// Note: Bridge API uses /transfers/{id} not /customers/{id}/transfers/{id}
return this.request<Transfer>("GET", `/transfers/${transferId}`)
}

async deleteTransfer(transferId: BridgeTransferId): Promise<Transfer> {
return this.request<Transfer>("DELETE", `/transfers/${transferId}`)
}

// ============ List Events ============

async listEvents(params?: ListEventsParams): Promise<ListResponse<BridgeWebhookEvent>> {
Expand Down
Loading
Loading