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
12 changes: 12 additions & 0 deletions dev/apollo-federation/supergraph.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -374,13 +374,25 @@ type BridgeWithdrawal
@join__type(graph: PUBLIC)
{
amount: String!
bridgeDeveloperFee: String
bridgeExchangeFee: String
bridgeTransferId: String
createdAt: String!
currency: String!
externalAccountId: String
failureReason: String
finalAmount: String
estimatedBridgeFee: String
estimatedBridgeFeePercent: String
estimatedCustomerFee: String
estimatedGasBuffer: String
flashFee: String
flashFeeIsEstimate: Boolean!
flashFeeNotice: String
flashFeePercent: String
id: ID!
status: String!
subtotalAmount: String
}

"""
Expand Down
14 changes: 14 additions & 0 deletions dev/config/base-config.yaml

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should never check in an API key, not even a sandbox one. Remove this and purge it from commit history

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ bridge:
apiKey: "" # real key injected via config overrides; never commit a real key
baseUrl: "https://api.sandbox.bridge.xyz/v0"
minWithdrawalAmount: 2
developerFeePercent: 2.0
withdrawalFeeEstimate:
bridgeFixedFeePercent: 0.6
usdtTransferGasLimit: 65000
gasPriceBufferMultiplier: 1.5
ethereumGasRpcUrls:
- "https://ethereum-rpc.publicnode.com"
- "https://eth.llamarpc.com"
- "https://cloudflare-eth.com"
ethUsdPriceUrl: "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd"
timeoutMs: 3000
cacheTtlMs: 60000
fallbackGasPriceGwei: 30
ethUsdFallback: 3000
timeoutMs: 15000
webhook:
port: 4009
Expand Down
12 changes: 12 additions & 0 deletions src/app/bridge/get-withdrawal-flash-fee-notice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { getI18nInstance } from "@config"
import { getLanguageOrDefault } from "@domain/locale"

export const BRIDGE_WITHDRAWAL_FLASH_FEE_NOTICE_PHRASE =
"notification.bridgeWithdrawal.flashFeeNotice"

export const getBridgeWithdrawalFlashFeeNotice = (locale: UserLanguage): string =>
getI18nInstance().__({ phrase: BRIDGE_WITHDRAWAL_FLASH_FEE_NOTICE_PHRASE, locale })

export const getBridgeWithdrawalFlashFeeNoticeForUser = (
user?: Pick<User, "language">,
): string => getBridgeWithdrawalFlashFeeNotice(getLanguageOrDefault(user?.language ?? ""))
1 change: 1 addition & 0 deletions src/config/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"title": "Deposit received"
},
"bridgeWithdrawal": {
"flashFeeNotice": "Shown fees and amounts are estimates. Final fees may differ.",
"cancelled": {
"body": "Your withdrawal of {{amount}} has been cancelled.",
"title": "Withdrawal cancelled"
Expand Down
3 changes: 2 additions & 1 deletion src/config/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"title": "Depósito recibido"
},
"bridgeWithdrawal": {
"flashFeeNotice": "Las comisiones y montos mostrados son estimados. Las comisiones finales pueden variar.",
"cancelled": {
"body": "Su retiro de {{amount}} ha sido cancelado.",
"title": "Retiro cancelado"
Expand All @@ -57,4 +58,4 @@
}
}
}
}
}
29 changes: 28 additions & 1 deletion src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,33 @@ export const configSchema = {
apiKey: { type: "string" },
baseUrl: { type: "string" },
minWithdrawalAmount: { type: "number" },
developerFeePercent: { type: "number" },
withdrawalFeeEstimate: {
type: "object",
properties: {
bridgeFixedFeePercent: { type: "number", default: 0.6 },
usdtTransferGasLimit: { type: "integer", default: 65000 },
gasPriceBufferMultiplier: { type: "number", default: 1.5 },
ethereumGasRpcUrls: {
type: "array",
items: { type: "string" },
default: [
"https://ethereum-rpc.publicnode.com",
"https://eth.llamarpc.com",
"https://cloudflare-eth.com",
],
},
ethUsdPriceUrl: {
type: "string",
default:
"https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd",
},
timeoutMs: { type: "integer", default: 3000 },
cacheTtlMs: { type: "integer", default: 60000 },
fallbackGasPriceGwei: { type: "number", default: 30 },
ethUsdFallback: { type: "number", default: 3000 },
},
},
timeoutMs: { type: "integer", default: 10000 },
webhook: {
type: "object",
Expand All @@ -670,7 +697,7 @@ export const configSchema = {
required: ["port", "publicKeys", "timestampSkewMs"],
},
},
required: ["enabled", "apiKey", "baseUrl", "webhook"],
required: ["enabled", "apiKey", "baseUrl", "developerFeePercent", "webhook"],
},
exchangeRates: {
type: "object",
Expand Down
14 changes: 14 additions & 0 deletions src/config/schema.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,25 @@ type BridgeWebhook = {
replaySecret?: string
}

type BridgeWithdrawalFeeEstimateConfig = {
bridgeFixedFeePercent?: number
usdtTransferGasLimit?: number
gasPriceBufferMultiplier?: number
ethereumGasRpcUrls?: string[]
ethUsdPriceUrl?: string
timeoutMs?: number
cacheTtlMs?: number
fallbackGasPriceGwei?: number
ethUsdFallback?: number
}

type BridgeConfig = {
enabled: boolean
apiKey: string
baseUrl: string
minWithdrawalAmount: number
developerFeePercent: number
withdrawalFeeEstimate?: BridgeWithdrawalFeeEstimateConfig
timeoutMs?: number
webhook: BridgeWebhook
}
Expand Down
7 changes: 7 additions & 0 deletions src/graphql/error-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,13 @@ export const mapError = (error: ApplicationError): CustomApolloError => {
message,
})

case "BridgeWithdrawalNetAmountTooLowError":
message = error.message || "Withdrawal amount must exceed estimated customer fees"
return bridgeGqlError({
code: "BRIDGE_WITHDRAWAL_NET_AMOUNT_TOO_LOW",
message,
})

case "BridgeWithdrawalNotFoundError":
message = error.message || "Withdrawal request not found"
return bridgeGqlError({
Expand Down
12 changes: 2 additions & 10 deletions src/graphql/public/root/query/bridge-withdrawal-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import BridgeWithdrawal from "@graphql/public/types/object/bridge-withdrawal"
import { BridgeConfig } from "@config"
import { BridgeDisabledError } from "@services/bridge/errors"
import * as BridgeAccountsRepo from "@services/mongoose/bridge-accounts"
import { presentBridgeWithdrawal } from "@services/bridge/withdrawal-fees"
import { RepositoryError } from "@domain/errors"

const bridgeWithdrawalRequest = GT.Field({
Expand All @@ -24,16 +25,7 @@ const bridgeWithdrawalRequest = GT.Field({
// Ownership check — never expose another account's withdrawal
if (withdrawal.accountId !== (domainAccount.id as string)) return null

return {
id: withdrawal.id,
amount: withdrawal.amount,
currency: withdrawal.currency,
externalAccountId: withdrawal.externalAccountId,
status: withdrawal.status,
bridgeTransferId: withdrawal.bridgeTransferId,
failureReason: withdrawal.failureReason,
createdAt: withdrawal.createdAt.toISOString(),
}
return presentBridgeWithdrawal(withdrawal)
},
})

Expand Down
12 changes: 12 additions & 0 deletions src/graphql/public/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -321,13 +321,25 @@ type BridgeVirtualAccount {

type BridgeWithdrawal {
amount: String!
bridgeDeveloperFee: String
bridgeExchangeFee: String
bridgeTransferId: String
createdAt: String!
currency: String!
externalAccountId: String
failureReason: String
finalAmount: String
estimatedBridgeFee: String
estimatedBridgeFeePercent: String
estimatedCustomerFee: String
estimatedGasBuffer: String
flashFee: String
flashFeeIsEstimate: Boolean!
flashFeeNotice: String
flashFeePercent: String
id: ID!
status: String!
subtotalAmount: String
}

type BuildInformation {
Expand Down
22 changes: 22 additions & 0 deletions src/graphql/public/types/object/bridge-withdrawal.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { GT } from "@graphql/index"
import { getBridgeWithdrawalFlashFeeNoticeForUser } from "@app/bridge/get-withdrawal-flash-fee-notice"
import { isFlashFeeEstimate } from "@services/bridge/withdrawal-fees"

const BridgeWithdrawal = GT.Object({
name: "BridgeWithdrawal",
Expand All @@ -8,6 +10,26 @@ const BridgeWithdrawal = GT.Object({
currency: { type: GT.NonNull(GT.String) },
externalAccountId: { type: GT.String },
status: { type: GT.NonNull(GT.String) },
estimatedBridgeFeePercent: { type: GT.String },
estimatedBridgeFee: { type: GT.String },
estimatedGasBuffer: { type: GT.String },
estimatedCustomerFee: { type: GT.String },
flashFeePercent: { type: GT.String },
flashFee: { type: GT.String },
flashFeeIsEstimate: { type: GT.NonNull(GT.Boolean) },
flashFeeNotice: {
type: GT.String,
resolve: (parent, _, { user }: GraphQLPublicContext) => {
const isEstimate =
parent.flashFeeIsEstimate === true || isFlashFeeEstimate(parent)
if (!isEstimate) return null
return getBridgeWithdrawalFlashFeeNoticeForUser(user)
},
},
bridgeDeveloperFee: { type: GT.String },
bridgeExchangeFee: { type: GT.String },
subtotalAmount: { type: GT.String },
finalAmount: { type: GT.String },
bridgeTransferId: { type: GT.String },
failureReason: { type: GT.String },
createdAt: { type: GT.NonNull(GT.String) },
Expand Down
14 changes: 13 additions & 1 deletion src/services/bridge/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,19 @@ export interface Transfer {
bank_routing_number?: string
to_address?: string
}
receipt?: Record<string, unknown>
receipt?: {
initial_amount: string
developer_fee: string
exchange_fee: string
subtotal_amount: string
gas_fee?: string
final_amount?: string
exchange_rate?: string
source_tx_hash?: string
destination_tx_hash?: string
remaining_prefunded_balance?: string
url?: string
}
created_at: string
updated_at: string
}
Expand Down
8 changes: 8 additions & 0 deletions src/services/bridge/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ export class BridgeInsufficientFundsError extends BridgeError {
}
}

export class BridgeWithdrawalNetAmountTooLowError extends BridgeError {
constructor(
message: string = "Withdrawal amount must exceed estimated customer fees",
) {
super(message)
}
}

export class BridgeAccountLevelError extends BridgeError {
constructor(
message: string = "Bridge requires at least a Personal account (Level 1+)",
Expand Down
Loading