-
Notifications
You must be signed in to change notification settings - Fork 4
fix: treat UTXO withdrawal amount as total, subtract fees from it #383
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
base: main
Are you sure you want to change the base?
Conversation
🦋 Changeset detectedLatest commit: 11aa333 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
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.
💡 Codex Review
Here are some automated review suggestions for this pull request.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
34616f7 to
11aa333
Compare
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.
Pull request overview
This PR fixes a critical bug in UTXO withdrawal fee handling where fees were previously added on top of the user-specified amount, requiring users to send more than intended. The new implementation treats the user-specified amount as the total, with fees (bridge fee and miner fee) subtracted from it, resulting in more predictable and user-friendly behavior.
Key Changes
- Fee calculation now subtracts fees from the specified amount instead of adding them
- Two-pass UTXO plan building to accurately estimate miner fees before finalizing the withdrawal
- Validation moved from checking the gross amount to validating the net amount (after fees) against the minimum withdrawal requirement
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/clients/near-kit.ts |
Core logic change: calculates bridge fee from total amount, builds withdrawal plan twice to determine miner fee, validates net amount meets minimum requirements |
tests/integration/bitcoin.test.ts |
Updated test amounts from 20,000 to 30,000 sats to account for fees; updated error message assertions to match new "net amount after fees" validation |
tests/clients/near.bitcoin.test.ts |
Similar test updates with amounts increased to 30,000 sats and error message regex patterns updated to handle new fee-aware validation messages |
.changeset/fix-utxo-withdrawal-fees.md |
Changeset documentation clearly describing the fix |
| // Final net amount the recipient will receive | ||
| const netAmount = recipientAmount | ||
|
|
||
| // Validate the net amount meets minimum requirements | ||
| if (netAmount < BigInt(config.min_withdraw_amount)) { | ||
| throw new Error( | ||
| `Net withdrawal amount ${netAmount} (after fees) is below minimum withdrawal amount ${config.min_withdraw_amount}. Total fees: bridge fee ${bridgeFee} + miner fee ${plan.fee}.`, |
Copilot
AI
Dec 3, 2025
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.
[nitpick] The variable netAmount is redundant as it's simply an alias for recipientAmount (line 827). It's only used once in the validation check at line 830. Consider removing this variable and using recipientAmount directly in the validation to improve code clarity:
// Validate the net amount meets minimum requirements
if (recipientAmount < BigInt(config.min_withdraw_amount)) {
throw new Error(
`Net withdrawal amount ${recipientAmount} (after fees) is below minimum withdrawal amount ${config.min_withdraw_amount}. Total fees: bridge fee ${bridgeFee} + miner fee ${plan.fee}.`,
)
}| // Final net amount the recipient will receive | |
| const netAmount = recipientAmount | |
| // Validate the net amount meets minimum requirements | |
| if (netAmount < BigInt(config.min_withdraw_amount)) { | |
| throw new Error( | |
| `Net withdrawal amount ${netAmount} (after fees) is below minimum withdrawal amount ${config.min_withdraw_amount}. Total fees: bridge fee ${bridgeFee} + miner fee ${plan.fee}.`, | |
| // Validate the net amount meets minimum requirements | |
| if (recipientAmount < BigInt(config.min_withdraw_amount)) { | |
| throw new Error( | |
| `Net withdrawal amount ${recipientAmount} (after fees) is below minimum withdrawal amount ${config.min_withdraw_amount}. Total fees: bridge fee ${bridgeFee} + miner fee ${plan.fee}.`, |
| // Now we know the miner fee, calculate the actual recipient amount | ||
| const recipientAmount = availableBudget - plan.fee | ||
|
|
||
| if (recipientAmount <= 0n) { | ||
| throw new Error( | ||
| `Amount ${amount} is too small to cover fees (bridge fee: ${bridgeFee}, miner fee: ${plan.fee})`, | ||
| ) | ||
| } | ||
|
|
||
| // Rebuild plan with the correct recipient amount | ||
| // (The fee should be similar, but UTXO selection might differ slightly) | ||
| plan = this.buildUtxoWithdrawalPlan(chain, utxos, recipientAmount, targetAddress, config) |
Copilot
AI
Dec 3, 2025
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.
The two-pass plan building approach has a potential accounting issue. The recipientAmount is calculated using the first plan's fee (line 807), but the actual transaction uses the second plan's fee (line 817). If these fees differ, the total accounting may not match the user's specified amount.
Example scenario:
- First plan (amount=49500): fee=1500, so recipientAmount=48000
- Second plan (amount=48000): fee=1000 (could be lower with smaller amount)
- Actual transaction total: 48000 + 1000 + bridgeFee(500) = 49500 ✓ matches
- But if second fee=2000 (higher): 48000 + 2000 + 500 = 50500 > 50000 ✗
The check at line 820 catches when the second fee is higher, but if the second fee is lower, the bridge keeps the difference as extra change. Consider using an iterative approach to converge on the exact recipient amount, or document this behavior clearly.
Summary
Fixes fee handling in UTXO withdrawals so that the user-specified amount is treated as the total amount to send, with fees subtracted from it, rather than added on top.
Changes
initUtxoWithdrawalto subtract bridge fee and transaction fee from the specified amountBefore
User specifies 50,000 sats → sends 52,050 sats (50,000 + fees)
After
User specifies 50,000 sats → sends exactly 50,000 sats → recipient receives 47,950 sats (50,000 - fees)