Skip to content

Commit f981aad

Browse files
committed
feat(btcrpc): add network fee to transaction processing
1 parent 4da8f44 commit f981aad

10 files changed

+82
-46
lines changed

internal/btcrpc/btcrpc.go

+10-10
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@ func New(appConfig *config.AppConfig, logger *logger.Logger) IBtcRpc {
3737
}
3838
}
3939

40-
func (b *BtcRpc) Send(receiverAddressStr string, amount *model.Web3BigInt) (string, error) {
40+
func (b *BtcRpc) Send(receiverAddressStr string, amount *model.Web3BigInt) (string, int64, error) {
4141
// Get sender's priv key and address
4242
privKey, senderAddress, err := b.getSelfPrivKeyAndAddress(b.appConfig.Bitcoin.WalletWIF)
4343
if err != nil {
4444
b.logger.Error("[btcrpc.Send][getSelfPrivKeyAndAddress]", map[string]string{
4545
"error": err.Error(),
4646
})
47-
return "", fmt.Errorf("failed to get self private key: %v", err)
47+
return "", 0, fmt.Errorf("failed to get self private key: %v", err)
4848
}
4949

5050
// Get receiver's address
@@ -53,24 +53,24 @@ func (b *BtcRpc) Send(receiverAddressStr string, amount *model.Web3BigInt) (stri
5353
b.logger.Error("[btcrpc.Send][DecodeAddress]", map[string]string{
5454
"error": err.Error(),
5555
})
56-
return "", err
56+
return "", 0, err
5757
}
5858

5959
amountToSend, ok := amount.Int64()
6060
if !ok {
6161
b.logger.Error("[btcrpc.Send][Int64]", map[string]string{
6262
"value": amount.Value,
6363
})
64-
return "", fmt.Errorf("failed to convert amount to int64")
64+
return "", 0, fmt.Errorf("failed to convert amount to int64")
6565
}
6666

6767
// Select required UTXOs and calculate change amount
68-
selectedUTXOs, changeAmount, err := b.selectUTXOs(senderAddress.EncodeAddress(), amountToSend)
68+
selectedUTXOs, changeAmount, fee, err := b.selectUTXOs(senderAddress.EncodeAddress(), amountToSend)
6969
if err != nil {
7070
b.logger.Error("[btcrpc.Send][selectUTXOs]", map[string]string{
7171
"error": err.Error(),
7272
})
73-
return "", err
73+
return "", 0, err
7474
}
7575

7676
// Create new tx and prepare inputs/outputs
@@ -79,7 +79,7 @@ func (b *BtcRpc) Send(receiverAddressStr string, amount *model.Web3BigInt) (stri
7979
b.logger.Error("[btcrpc.Send][prepareTx]", map[string]string{
8080
"error": err.Error(),
8181
})
82-
return "", err
82+
return "", 0, err
8383
}
8484

8585
// Sign tx
@@ -88,7 +88,7 @@ func (b *BtcRpc) Send(receiverAddressStr string, amount *model.Web3BigInt) (stri
8888
b.logger.Error("[btcrpc.Send][sign]", map[string]string{
8989
"error": err.Error(),
9090
})
91-
return "", err
91+
return "", 0, err
9292
}
9393

9494
// Serialize & broadcast tx with potential fee adjustment
@@ -97,10 +97,10 @@ func (b *BtcRpc) Send(receiverAddressStr string, amount *model.Web3BigInt) (stri
9797
b.logger.Error("[btcrpc.Send][broadcast]", map[string]string{
9898
"error": err.Error(),
9999
})
100-
return "", err
100+
return "", 0, err
101101
}
102102

103-
return txID, nil
103+
return txID, fee, nil
104104
}
105105

106106
// broadcastWithFeeAdjustment attempts to broadcast the transaction,

internal/btcrpc/helper.go

+9-10
Original file line numberDiff line numberDiff line change
@@ -269,21 +269,20 @@ func (b *BtcRpc) getConfirmedUTXOs(address string) ([]blockstream.UTXO, error) {
269269
// returns selected UTXOs and change amount
270270
// change amount is the amount sent back to sender after sending total amount of selected UTXOs to recipient
271271
// changeAmount = total amount of selected UTXOs - amountToSend - fee
272-
func (b *BtcRpc) selectUTXOs(address string, amountToSend int64) (selected []blockstream.UTXO, changeAmount int64, err error) {
272+
func (b *BtcRpc) selectUTXOs(address string, amountToSend int64) (selected []blockstream.UTXO, changeAmount int64, fee int64, err error) {
273273
confirmedUTXOs, err := b.getConfirmedUTXOs(address)
274274
if err != nil {
275-
return nil, 0, err
275+
return nil, 0, 0, err
276276
}
277277

278278
// Get current fee rate from mempool
279279
feeRates, err := b.blockstream.EstimateFees()
280280
if err != nil {
281-
return nil, 0, err
281+
return nil, 0, 0, err
282282
}
283283

284284
// Iteratively select UTXOs until we have enough to cover amount + fee
285285
var totalSelected int64
286-
var fee int64
287286

288287
for _, utxo := range confirmedUTXOs {
289288
selected = append(selected, utxo)
@@ -295,23 +294,23 @@ func (b *BtcRpc) selectUTXOs(address string, amountToSend int64) (selected []blo
295294
// targetBlocks confirmations: widely accepted standard for bitcoin transactions
296295
fee, err = b.calculateTxFee(feeRates, len(selected), 2, 6)
297296
if err != nil {
298-
return nil, 0, err
297+
return nil, 0, 0, err
299298
}
300299

301300
if fee > amountToSend {
302-
return nil, 0, fmt.Errorf("fee exceeds amount to send: fee %d, amountToSend %d", fee, amountToSend)
301+
return nil, 0, 0, fmt.Errorf("fee exceeds amount to send: fee %d, amountToSend %d", fee, amountToSend)
303302
}
304303

305304
satoshiRate, err := b.GetSatoshiUSDPrice()
306305
if err != nil {
307-
return nil, 0, err
306+
return nil, 0, 0, err
308307
}
309308

310309
// calculate and round up to 1 decimal places
311310
usdFee := math.Ceil(float64(fee)/satoshiRate*10) / 10
312311

313312
if usdFee > b.appConfig.Bitcoin.MaxTxFeeUSD {
314-
return nil, 0, fmt.Errorf("fee exceeds maximum threshold: usdFee %0.1f, MaxTxFeeUSD %0.1f", usdFee, b.appConfig.Bitcoin.MaxTxFeeUSD)
313+
return nil, 0, 0, fmt.Errorf("fee exceeds maximum threshold: usdFee %0.1f, MaxTxFeeUSD %0.1f", usdFee, b.appConfig.Bitcoin.MaxTxFeeUSD)
315314
}
316315

317316
// if we have enough to cover amount + current fee => return selected UTXOs and change amount
@@ -322,11 +321,11 @@ func (b *BtcRpc) selectUTXOs(address string, amountToSend int64) (selected []blo
322321
"usdFee": fmt.Sprintf("%0.1f", usdFee),
323322
})
324323
changeAmount = totalSelected - amountToSend - fee
325-
return selected, changeAmount, nil
324+
return selected, changeAmount, fee, nil
326325
}
327326
}
328327

329-
return nil, 0, fmt.Errorf(
328+
return nil, 0, 0, fmt.Errorf(
330329
"insufficient funds: have %d satoshis, need %d satoshis",
331330
totalSelected,
332331
amountToSend+fee,

internal/btcrpc/interface.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package btcrpc
33
import "github.com/dwarvesf/icy-backend/internal/model"
44

55
type IBtcRpc interface {
6-
Send(receiverAddress string, amount *model.Web3BigInt) (string, error)
6+
Send(receiverAddress string, amount *model.Web3BigInt) (string, int64, error)
77
CurrentBalance() (*model.Web3BigInt, error)
88
GetTransactionsByAddress(address string, fromTxId string) ([]model.OnchainBtcTransaction, error)
99
EstimateFees() (map[string]float64, error)

internal/model/onchain_btc_processed_transaction.go

+13-12
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,17 @@ const (
1313
)
1414

1515
type OnchainBtcProcessedTransaction struct {
16-
ID int `json:"id"`
17-
IcyTransactionHash *string `json:"icy_transaction_hash"`
18-
BtcTransactionHash string `json:"btc_transaction_hash"`
19-
SwapTransactionHash string `json:"swap_transaction_hash"`
20-
BTCAddress string `json:"btc_address"`
21-
ProcessedAt *time.Time `json:"processed_at"`
22-
Amount string `json:"amount"`
23-
Status BtcProcessingStatus `json:"status"`
24-
ICYSwapTx OnchainIcySwapTransaction `json:"icy_swap_tx"`
25-
CreatedAt time.Time `json:"created_at"`
26-
UpdatedAt time.Time `json:"updated_at"`
27-
NetworkFee string `gorm:"column:network_fee" json:"network_fee"`
16+
ID int `json:"id"`
17+
IcyTransactionHash *string `json:"icy_transaction_hash"`
18+
BtcTransactionHash string `json:"btc_transaction_hash"`
19+
SwapTransactionHash string `json:"swap_transaction_hash"`
20+
BTCAddress string `json:"btc_address"`
21+
ProcessedAt *time.Time `json:"processed_at"`
22+
Amount string `json:"amount"`
23+
Status BtcProcessingStatus `json:"status"`
24+
OnchainIcySwapTransaction OnchainIcySwapTransaction `gorm:"foreignKey:TransactionHash;references:SwapTransactionHash" json:"icy_swap_tx"`
25+
CreatedAt time.Time `json:"created_at"`
26+
UpdatedAt time.Time `json:"updated_at"`
27+
NetworkFee string `gorm:"column:network_fee" json:"network_fee"`
28+
TotalAmount string `gorm:"-" json:"total_amount"`
2829
}

internal/model/onchain_icy_swap_transaction.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package model
33
import "time"
44

55
type OnchainIcySwapTransaction struct {
6-
ID int `json:"id"`
6+
ID int `json:"-"`
77
TransactionHash string `json:"transaction_hash"`
88
BlockNumber uint64 `json:"block_number"`
99
IcyAmount string `json:"icy_amount"`

internal/store/onchainbtcprocessedtransaction/interface.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ type IStore interface {
2525
UpdateStatus(tx *gorm.DB, id int, status model.BtcProcessingStatus) error
2626

2727
// UpdateToCompleted updates the status of a BTC processed transaction to processed
28-
UpdateToCompleted(tx *gorm.DB, id int, btcTxHash string) error
28+
UpdateToCompleted(tx *gorm.DB, id int, btcTxHash string, networkFee int64) error
2929

3030
// Get all pending BTC processed transactions
3131
GetPendingTransactions(tx *gorm.DB) ([]model.OnchainBtcProcessedTransaction, error)

internal/store/onchainbtcprocessedtransaction/onchain_btc_processed_transaction.go

+38-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package onchainbtcprocessedtransaction
22

33
import (
4+
"strconv"
45
"strings"
56
"time"
67

@@ -38,10 +39,11 @@ func (s *store) UpdateStatus(tx *gorm.DB, id int, status model.BtcProcessingStat
3839
}).Error
3940
}
4041

41-
func (s *store) UpdateToCompleted(tx *gorm.DB, id int, btcTxHash string) error {
42+
func (s *store) UpdateToCompleted(tx *gorm.DB, id int, btcTxHash string, networkFee int64) error {
4243
return tx.Model(&model.OnchainBtcProcessedTransaction{}).Where("id = ?", id).Updates(map[string]interface{}{
4344
"status": model.BtcProcessingStatusCompleted,
4445
"btc_transaction_hash": btcTxHash,
46+
"network_fee": networkFee,
4547
"updated_at": time.Now(),
4648
"processed_at": time.Now(),
4749
}).Error
@@ -68,25 +70,54 @@ func (s *store) Find(db *gorm.DB, filter ListFilter) ([]*model.OnchainBtcProcess
6870
query = query.Where("status = ?", filter.Status)
6971
}
7072
if filter.EVMAddress != "" {
71-
query = query.Joins("JOIN onchain_icy_swap_transactions ON onchain_btc_processed_transactions.swap_transaction_hash = onchain_icy_swap_transactions.transaction_hash").Where("LOWER(onchain_icy_swap_transactions.from_address) = ?", strings.ToLower(filter.EVMAddress))
73+
query = query.Joins("LEFT JOIN onchain_icy_swap_transactions ON onchain_icy_swap_transactions.transaction_hash = onchain_btc_processed_transactions.icy_transaction_hash").
74+
Where("LOWER(onchain_icy_swap_transactions.from_address) = ?", strings.ToLower(filter.EVMAddress))
7275
}
7376

7477
// Count total records
7578
if err := query.Count(&total).Error; err != nil {
7679
return nil, 0, err
7780
}
7881

79-
// Apply pagination
80-
query = query.Offset(filter.Offset).Limit(filter.Limit)
82+
// Prepare final query with preloading
83+
finalQuery := db.Model(&model.OnchainBtcProcessedTransaction{}).
84+
Preload("OnchainIcySwapTransaction")
8185

82-
// Order by updated_at descending
83-
query = query.Joins("JOIN onchain_icy_swap_transactions ON onchain_btc_processed_transactions.swap_transaction_hash = onchain_icy_swap_transactions.transaction_hash").
86+
// Reapply all filters to final query
87+
if filter.BTCAddress != "" {
88+
finalQuery = finalQuery.Where("LOWER(btc_address) = ?", strings.ToLower(filter.BTCAddress))
89+
}
90+
if filter.Status != "" {
91+
finalQuery = finalQuery.Where("status = ?", filter.Status)
92+
}
93+
if filter.EVMAddress != "" {
94+
finalQuery = finalQuery.Joins("LEFT JOIN onchain_icy_swap_transactions ON onchain_icy_swap_transactions.transaction_hash = onchain_btc_processed_transactions.icy_transaction_hash").
95+
Where("LOWER(onchain_icy_swap_transactions.from_address) = ?", strings.ToLower(filter.EVMAddress))
96+
}
97+
98+
// Apply pagination and ordering
99+
finalQuery = finalQuery.
100+
Offset(filter.Offset).
101+
Limit(filter.Limit).
84102
Order("updated_at DESC")
85103

86104
// Fetch transactions
87-
if err := query.Find(&transactions).Error; err != nil {
105+
if err := finalQuery.Find(&transactions).Error; err != nil {
88106
return nil, 0, err
89107
}
90108

109+
for i := range transactions {
110+
amount, err := strconv.ParseInt(transactions[i].Amount, 10, 64)
111+
if err != nil {
112+
continue
113+
}
114+
networkFee, err := strconv.ParseInt(transactions[i].NetworkFee, 10, 64)
115+
if err != nil {
116+
continue
117+
}
118+
totalAmount := amount - networkFee
119+
transactions[i].TotalAmount = strconv.FormatInt(totalAmount, 10)
120+
}
121+
91122
return transactions, total, nil
92123
}

internal/telemetry/btc.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ func (t *Telemetry) ProcessPendingBtcTransactions() error {
117117
Value: pendingTx.Amount,
118118
Decimal: consts.BTC_DECIMALS,
119119
}
120-
tx, err := t.btcRpc.Send(pendingTx.BTCAddress, amount)
120+
tx, networkFee, err := t.btcRpc.Send(pendingTx.BTCAddress, amount)
121121
if err != nil {
122122
t.logger.Error("[ProcessPendingBtcTransactions][Send]", map[string]string{
123123
"error": err.Error(),
@@ -126,7 +126,7 @@ func (t *Telemetry) ProcessPendingBtcTransactions() error {
126126
}
127127

128128
// update processed transaction
129-
err = t.store.OnchainBtcProcessedTransaction.UpdateToCompleted(t.db, pendingTx.ID, tx)
129+
err = t.store.OnchainBtcProcessedTransaction.UpdateToCompleted(t.db, pendingTx.ID, tx, networkFee)
130130
if err != nil {
131131
t.logger.Error("[ProcessPendingBtcTransactions][UpdateToCompleted]", map[string]string{
132132
"error": err.Error(),
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
-- Remove network_fee column from onchain_btc_processed_transactions
1+
-- Remove network_fee column and foreign key from onchain_btc_processed_transactions
22
ALTER TABLE onchain_btc_processed_transactions
3+
DROP CONSTRAINT IF EXISTS fk_btc_processed_icy_swap_transaction,
34
DROP COLUMN IF EXISTS network_fee;
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
-- Add network_fee column to onchain_btc_processed_transactions
22
ALTER TABLE onchain_btc_processed_transactions
3-
ADD COLUMN network_fee VARCHAR(255) DEFAULT '0';
3+
ADD COLUMN network_fee VARCHAR(255) DEFAULT '0',
4+
ADD CONSTRAINT fk_btc_processed_icy_swap_transaction
5+
FOREIGN KEY (swap_transaction_hash)
6+
REFERENCES onchain_icy_swap_transactions(transaction_hash)
7+
ON DELETE SET NULL;

0 commit comments

Comments
 (0)