Skip to content

Conversation

@r-near
Copy link
Collaborator

@r-near r-near commented Dec 1, 2025

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

  • Modified initUtxoWithdrawal to subtract bridge fee and transaction fee from the specified amount
  • Updated minimum withdrawal validation to check net amount after fees
  • Updated tests to reflect new behavior where amounts must account for fees

Before

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)

@changeset-bot
Copy link

changeset-bot bot commented Dec 1, 2025

🦋 Changeset detected

Latest commit: 11aa333

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
omni-bridge-sdk Patch

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

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a 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".

@r-near r-near force-pushed the fix/utxo-withdrawal-fees branch from 34616f7 to 11aa333 Compare December 3, 2025 17:28
Copilot AI review requested due to automatic review settings December 3, 2025 17:28
Copy link
Contributor

Copilot AI left a 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

Comment on lines +826 to +832
// 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}.`,
Copy link

Copilot AI Dec 3, 2025

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}.`,
  )
}
Suggested change
// 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}.`,

Copilot uses AI. Check for mistakes.
Comment on lines +806 to +817
// 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)
Copy link

Copilot AI Dec 3, 2025

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants