-
Notifications
You must be signed in to change notification settings - Fork 19
feat(bridge): withdrawal fee estimates — request/confirm/cancel flow #403
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
heyolaniran
wants to merge
8
commits into
lnflash:tmp/bridge-rebase-pr-ready
Choose a base branch
from
heyolaniran:tmp/bridge-rebase-pr-ready
base: tmp/bridge-rebase-pr-ready
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
e4b0e43
feat(bridge): add withdrawal fee estimate config
heyolaniran caf4e4c
feat(bridge): compute withdrawal customer fee estimates
heyolaniran 1fd686c
feat(bridge): persist withdrawal fee breakdown in MongoDB
heyolaniran 85da7e4
feat(bridge): surface fee estimates in withdrawal service flow
heyolaniran 0c1113b
feat(bridge): add localized flash fee notice for withdrawals
heyolaniran f49ee0c
feat(bridge): expose withdrawal fee fields in GraphQL API
heyolaniran b625a48
test(bridge): fix unit test failures from fee field additions
heyolaniran 3036655
fix: remove the hardcoded bridge developer fees percentage
heyolaniran File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 ?? "")) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -649,6 +649,18 @@ export const configSchema = { | |
| apiKey: { type: "string" }, | ||
| baseUrl: { type: "string" }, | ||
| minWithdrawalAmount: { type: "number" }, | ||
| developerFeePercent: { type: "number", default: 2.0 }, | ||
| withdrawalFeeEstimate: { | ||
| type: "object", | ||
| properties: { | ||
| bridgeFixedFeePercent: { type: "number", default: 0.6 }, | ||
| usdtTransferGasLimit: { type: "integer", default: 65000 }, | ||
| gasPriceBufferMultiplier: { type: "number", default: 1.5 }, | ||
| ethereumGasRpcUrl: { type: "string", default: "https://cloudflare-eth.com" }, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. depending on only one URL for gas fees is a bottleneck. At a minimum we should be getting multiple sources and using an average, or getting the gas fee estimate from Bridge directly |
||
| fallbackGasPriceGwei: { type: "number", default: 30 }, | ||
| ethUsdFallback: { type: "number", default: 3000 }, | ||
| }, | ||
| }, | ||
| timeoutMs: { type: "integer", default: 10000 }, | ||
| webhook: { | ||
| type: "object", | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| import { baseLogger } from "@services/logger" | ||
|
|
||
| export type EthereumGasMarketSnapshot = { | ||
| gasPriceGwei: number | ||
| ethUsd: number | ||
| } | ||
|
|
||
| export const computeEstimatedGasBufferUsd = ({ | ||
| gasLimit, | ||
| gasPriceGwei, | ||
| ethUsd, | ||
| bufferMultiplier, | ||
| }: { | ||
| gasLimit: number | ||
| gasPriceGwei: number | ||
| ethUsd: number | ||
| bufferMultiplier: number | ||
| }): string => { | ||
| const gasUsd = ((gasLimit * gasPriceGwei * ethUsd) / 1e9) * bufferMultiplier | ||
| return gasUsd.toFixed(2) | ||
| } | ||
|
|
||
| const parseHexWeiToGwei = (hexWei: string): number | Error => { | ||
| const normalized = hexWei.startsWith("0x") ? hexWei.slice(2) : hexWei | ||
| if (!/^[0-9a-fA-F]+$/.test(normalized)) { | ||
| return new Error(`Invalid gas price response: ${hexWei}`) | ||
| } | ||
| const wei = BigInt(`0x${normalized}`) | ||
| return Number(wei) / 1e9 | ||
| } | ||
|
|
||
| export const fetchEthereumGasPriceGwei = async ({ | ||
| rpcUrl, | ||
| timeoutMs, | ||
| }: { | ||
| rpcUrl: string | ||
| timeoutMs: number | ||
| }): Promise<number | Error> => { | ||
| const controller = new AbortController() | ||
| const timeout = setTimeout(() => controller.abort(), timeoutMs) | ||
|
|
||
| try { | ||
| const response = await fetch(rpcUrl, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| jsonrpc: "2.0", | ||
| id: 1, | ||
| method: "eth_gasPrice", | ||
| params: [], | ||
| }), | ||
| signal: controller.signal, | ||
| }) | ||
|
|
||
| if (!response.ok) { | ||
| return new Error(`Ethereum RPC gas price request failed: HTTP ${response.status}`) | ||
| } | ||
|
|
||
| const payload = (await response.json()) as { | ||
| result?: string | ||
| error?: { message?: string } | ||
| } | ||
|
|
||
| if (payload.error?.message) { | ||
| return new Error(`Ethereum RPC gas price error: ${payload.error.message}`) | ||
| } | ||
| if (!payload.result) { | ||
| return new Error("Ethereum RPC gas price response missing result") | ||
| } | ||
|
|
||
| const gasPriceGwei = parseHexWeiToGwei(payload.result) | ||
| if (gasPriceGwei instanceof Error) return gasPriceGwei | ||
| if (!Number.isFinite(gasPriceGwei) || gasPriceGwei <= 0) { | ||
| return new Error(`Invalid gas price gwei value: ${gasPriceGwei}`) | ||
| } | ||
|
|
||
| return gasPriceGwei | ||
| } catch (error) { | ||
| baseLogger.warn({ error, rpcUrl }, "Failed to fetch Ethereum gas price") | ||
| return error instanceof Error ? error : new Error(String(error)) | ||
| } finally { | ||
| clearTimeout(timeout) | ||
| } | ||
| } | ||
|
|
||
| export const fetchEthUsdPrice = async ({ | ||
| timeoutMs, | ||
| }: { | ||
| timeoutMs: number | ||
| }): Promise<number | Error> => { | ||
| const controller = new AbortController() | ||
| const timeout = setTimeout(() => controller.abort(), timeoutMs) | ||
|
|
||
| try { | ||
| const url = | ||
| "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd" | ||
| const response = await fetch(url, { signal: controller.signal }) | ||
| if (!response.ok) { | ||
| return new Error(`ETH/USD price request failed: HTTP ${response.status}`) | ||
| } | ||
|
|
||
| const payload = (await response.json()) as { ethereum?: { usd?: number } } | ||
| const ethUsd = payload.ethereum?.usd | ||
| if (ethUsd == null || !Number.isFinite(ethUsd) || ethUsd <= 0) { | ||
| return new Error("ETH/USD price response missing ethereum.usd") | ||
| } | ||
|
|
||
| return ethUsd | ||
| } catch (error) { | ||
| baseLogger.warn({ error }, "Failed to fetch ETH/USD price") | ||
| return error instanceof Error ? error : new Error(String(error)) | ||
| } finally { | ||
| clearTimeout(timeout) | ||
| } | ||
| } | ||
|
|
||
| export const fetchEthereumGasMarketSnapshot = async ({ | ||
| rpcUrl, | ||
| timeoutMs, | ||
| fallbackGasPriceGwei, | ||
| ethUsdFallback, | ||
| }: { | ||
| rpcUrl: string | ||
| timeoutMs: number | ||
| fallbackGasPriceGwei: number | ||
| ethUsdFallback: number | ||
| }): Promise<EthereumGasMarketSnapshot> => { | ||
| const [gasPriceResult, ethUsdResult] = await Promise.all([ | ||
| fetchEthereumGasPriceGwei({ rpcUrl, timeoutMs }), | ||
| fetchEthUsdPrice({ timeoutMs }), | ||
| ]) | ||
|
|
||
| const gasPriceGwei = | ||
| gasPriceResult instanceof Error ? fallbackGasPriceGwei : gasPriceResult | ||
| const ethUsd = ethUsdResult instanceof Error ? ethUsdFallback : ethUsdResult | ||
|
|
||
| if (gasPriceResult instanceof Error) { | ||
| baseLogger.warn( | ||
| { fallbackGasPriceGwei, error: gasPriceResult.message }, | ||
| "Using fallback Ethereum gas price for withdrawal fee estimate", | ||
| ) | ||
| } | ||
| if (ethUsdResult instanceof Error) { | ||
| baseLogger.warn( | ||
| { ethUsdFallback, error: ethUsdResult.message }, | ||
| "Using fallback ETH/USD price for withdrawal fee estimate", | ||
| ) | ||
| } | ||
|
|
||
| return { gasPriceGwei, ethUsd } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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