Skip to content

Commit 221336c

Browse files
authored
feat(broadcast): add fee adjustment for transaction broadcasting (#17)
1 parent 17eb2cc commit 221336c

File tree

5 files changed

+142
-24
lines changed

5 files changed

+142
-24
lines changed

internal/btcrpc/blockstream/blockstream.go

+19-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"io"
77
"net/http"
8+
"regexp"
89
"strconv"
910
"strings"
1011

@@ -50,7 +51,24 @@ func (c *blockstream) BroadcastTx(txHex string) (string, error) {
5051
}
5152

5253
if resp.StatusCode != 200 {
53-
return "", fmt.Errorf("status code: %v, failed to broadcast transaction: %s", resp.StatusCode, body)
54+
// Check for minimum relay fee error
55+
bodyStr := string(body)
56+
57+
// Regex to extract minimum fee from error message
58+
minFeeRegex := regexp.MustCompile(`min relay fee not met, (\d+) < (\d+)`)
59+
matches := minFeeRegex.FindStringSubmatch(bodyStr)
60+
61+
if len(matches) == 3 {
62+
minFee, _ := strconv.ParseInt(matches[2], 10, 64)
63+
64+
return "", &BroadcastTxError{
65+
Message: fmt.Sprintf("status code: %v, failed to broadcast transaction: %s", resp.StatusCode, bodyStr),
66+
StatusCode: resp.StatusCode,
67+
MinFee: minFee,
68+
}
69+
}
70+
71+
return "", fmt.Errorf("status code: %v, failed to broadcast transaction: %s", resp.StatusCode, bodyStr)
5472
}
5573

5674
return string(body), nil

internal/btcrpc/blockstream/interface.go

+12
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@ package blockstream
22

33
import "github.com/dwarvesf/icy-backend/internal/model"
44

5+
// BroadcastTxError represents a detailed error when broadcasting a transaction
6+
type BroadcastTxError struct {
7+
Message string
8+
StatusCode int
9+
MinFee int64 // Minimum fee required in satoshis
10+
}
11+
12+
// Error implements the error interface
13+
func (e *BroadcastTxError) Error() string {
14+
return e.Message
15+
}
16+
517
type IBlockStream interface {
618
BroadcastTx(txHex string) (hash string, err error)
719
EstimateFees() (fees map[string]float64, err error)

internal/btcrpc/btcrpc.go

+102-2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ package btcrpc
33
import (
44
"fmt"
55
"strconv"
6+
"strings"
67

78
"github.com/btcsuite/btcd/btcutil"
89
"github.com/btcsuite/btcd/chaincfg"
10+
"github.com/btcsuite/btcd/wire"
911

1012
"github.com/dwarvesf/icy-backend/internal/btcrpc/blockstream"
1113
"github.com/dwarvesf/icy-backend/internal/model"
@@ -86,8 +88,8 @@ func (b *BtcRpc) Send(receiverAddressStr string, amount *model.Web3BigInt) (stri
8688
return "", err
8789
}
8890

89-
// Serialize & broadcast tx
90-
txID, err := b.broadcast(tx)
91+
// Serialize & broadcast tx with potential fee adjustment
92+
txID, err := b.broadcastWithFeeAdjustment(tx, selectedUTXOs, receiverAddress, senderAddress, amountToSend, changeAmount)
9193
if err != nil {
9294
b.logger.Error("[btcrpc.Send][broadcast]", map[string]string{
9395
"error": err.Error(),
@@ -98,6 +100,104 @@ func (b *BtcRpc) Send(receiverAddressStr string, amount *model.Web3BigInt) (stri
98100
return txID, nil
99101
}
100102

103+
// broadcastWithFeeAdjustment attempts to broadcast the transaction,
104+
// and if it fails due to minimum relay fee, attempts to increase the fee by 5%
105+
func (b *BtcRpc) broadcastWithFeeAdjustment(
106+
tx *wire.MsgTx,
107+
selectedUTXOs []blockstream.UTXO,
108+
receiverAddress btcutil.Address,
109+
senderAddress *btcutil.AddressWitnessPubKeyHash,
110+
amountToSend, changeAmount int64,
111+
) (string, error) {
112+
// First attempt to broadcast
113+
txID, err := b.broadcast(tx)
114+
if err == nil {
115+
return txID, nil
116+
}
117+
118+
// Check if the error is specifically about minimum relay fee
119+
broadcastErr, ok := err.(*blockstream.BroadcastTxError)
120+
if ok && strings.Contains(broadcastErr.Error(), "min relay fee not met") {
121+
b.logger.Info("[btcrpc.Send][FeeAdjustment]", map[string]string{
122+
"message": "Attempting to adjust transaction fee",
123+
})
124+
125+
// Use the minimum fee from the error if available
126+
var adjustedFee, currentFee int64
127+
if broadcastErr.MinFee > 0 {
128+
// Use the minimum fee from the error
129+
adjustedFee = broadcastErr.MinFee
130+
131+
// Fallback to calculating current fee if no minimum fee in error
132+
feeRates, err := b.blockstream.EstimateFees()
133+
if err != nil {
134+
return "", fmt.Errorf("failed to get fee rates for adjustment: %v", err)
135+
}
136+
137+
currentFee, err = b.calculateTxFee(feeRates, len(selectedUTXOs), 2, 6)
138+
if err != nil {
139+
return "", fmt.Errorf("failed to calculate current fee: %v", err)
140+
}
141+
142+
if adjustedFee > int64(float64(currentFee)*1.05) {
143+
return "", fmt.Errorf("fee too high to adjust, adjusted fee: %d, current fee: %d", adjustedFee, currentFee)
144+
}
145+
} else {
146+
// Fallback to calculating fee if no minimum fee in error
147+
feeRates, err := b.blockstream.EstimateFees()
148+
if err != nil {
149+
return "", fmt.Errorf("failed to get fee rates for adjustment: %v", err)
150+
}
151+
152+
currentFee, err = b.calculateTxFee(feeRates, len(selectedUTXOs), 2, 6)
153+
if err != nil {
154+
return "", fmt.Errorf("failed to calculate current fee: %v", err)
155+
}
156+
157+
// Adjust fee to be 5% higher
158+
adjustedFee = int64(float64(currentFee) * 1.05)
159+
}
160+
161+
b.logger.Info("[btcrpc.Send][FeeAdjustment]", map[string]string{
162+
"currentFee": strconv.FormatInt(currentFee, 10),
163+
"adjustedFee": strconv.FormatInt(adjustedFee, 10),
164+
"changeAmount": strconv.FormatInt(changeAmount, 10),
165+
"amountToSend": strconv.FormatInt(amountToSend, 10),
166+
})
167+
168+
// Calculate adjusted change amount
169+
adjustedChangeAmount := changeAmount - (adjustedFee - currentFee)
170+
171+
// If adjusted change amount becomes negative, we can't proceed
172+
if adjustedChangeAmount < 0 {
173+
return "", fmt.Errorf("insufficient funds to adjust transaction fee")
174+
}
175+
176+
// Recreate transaction with adjusted fee
177+
adjustedTx, err := b.prepareTx(selectedUTXOs, receiverAddress, senderAddress, amountToSend, adjustedChangeAmount)
178+
if err != nil {
179+
return "", fmt.Errorf("failed to prepare adjusted transaction: %v", err)
180+
}
181+
182+
// Re-sign the transaction
183+
privKey, _, err := b.getSelfPrivKeyAndAddress(b.appConfig.Bitcoin.WalletWIF)
184+
if err != nil {
185+
return "", fmt.Errorf("failed to get private key for re-signing: %v", err)
186+
}
187+
188+
err = b.sign(adjustedTx, privKey, senderAddress, selectedUTXOs)
189+
if err != nil {
190+
return "", fmt.Errorf("failed to sign adjusted transaction: %v", err)
191+
}
192+
193+
// Attempt to broadcast adjusted transaction
194+
return b.broadcast(adjustedTx)
195+
}
196+
197+
// If it's a different error, return the original error
198+
return "", err
199+
}
200+
101201
func (b *BtcRpc) CurrentBalance() (*model.Web3BigInt, error) {
102202
balance, err := b.blockstream.GetBTCBalance(b.appConfig.Blockchain.BTCTreasuryAddress)
103203
if err != nil {

internal/btcrpc/helper.go

+8-20
Original file line numberDiff line numberDiff line change
@@ -276,26 +276,6 @@ func (b *BtcRpc) selectUTXOs(address string, amountToSend int64) (selected []blo
276276
return nil, 0, err
277277
}
278278

279-
// blk := "6"
280-
// minFee, ok := feeRates[blk]
281-
// if !ok {
282-
// return nil, 0, fmt.Errorf("no fee rate available for target %s blocks", blk)
283-
// }
284-
// for block, fee := range feeRates {
285-
// if fee <= maxTxFee {
286-
// minFee = fee
287-
// blk = block
288-
// break
289-
// }
290-
// }
291-
// if minFee > maxTxFee {
292-
// return nil, 0, fmt.Errorf("fee rate %f exceeds maximum threshold: %d USD", minFee, maxTxFee)
293-
// }
294-
// targetBlocks, err := strconv.Atoi(blk)
295-
// if err != nil {
296-
// return nil, 0, fmt.Errorf("failed to convert block string to int: %v", err)
297-
// }
298-
299279
// Iteratively select UTXOs until we have enough to cover amount + fee
300280
var totalSelected int64
301281
var fee int64
@@ -313,8 +293,16 @@ func (b *BtcRpc) selectUTXOs(address string, amountToSend int64) (selected []blo
313293
return nil, 0, err
314294
}
315295

296+
if fee > amountToSend {
297+
return nil, 0, fmt.Errorf("fee exceeds amount to send: fee %d, amountToSend %d", fee, amountToSend)
298+
}
299+
316300
// if we have enough to cover amount + current fee => return selected UTXOs and change amount
317301
if totalSelected >= amountToSend+fee {
302+
b.logger.Info("[selectUTXOs] calculateTxFee", map[string]string{
303+
"amountToSend": fmt.Sprintf("%d", amountToSend),
304+
"fee": fmt.Sprintf("%d", fee),
305+
})
318306
changeAmount = totalSelected - amountToSend - fee
319307
return selected, changeAmount, nil
320308
}

internal/handler/swap/swap.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func (h *handler) CreateSwapRequest(c *gin.Context) {
9191
return
9292
}
9393
if icyAmountFloat < h.appConfig.MinIcySwapAmount {
94-
c.JSON(http.StatusBadRequest, view.CreateResponse[any](nil, fmt.Errorf("minimum ICY amount is %s", h.appConfig.MinIcySwapAmount), nil, "invalid ICY amount"))
94+
c.JSON(http.StatusBadRequest, view.CreateResponse[any](nil, fmt.Errorf("minimum ICY amount is %v", h.appConfig.MinIcySwapAmount), nil, "invalid ICY amount"))
9595
return
9696
}
9797

0 commit comments

Comments
 (0)