Skip to content

Commit 7f8c838

Browse files
authored
Merge pull request #268 from bsv-blockchain/fix/feemodel
Conform this function to match SV Node calculation
2 parents 739d1d8 + d3b8b77 commit 7f8c838

File tree

4 files changed

+120
-27
lines changed

4 files changed

+120
-27
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. The format
44

55
## Table of Contents
66

7+
- [1.2.12 - 2025-11-12](#1212---2025-11-12)
78
- [1.2.11 - 2025-10-27](#1211---2025-10-27)
89
- [1.2.10 - 2025-09-16](#1210---2025-09-16)
910
- [1.2.9 - 2025-09-07](#129---2025-09-07)
@@ -46,6 +47,19 @@ All notable changes to this project will be documented in this file. The format
4647
- [1.1.0 - 2024-08-19](#110---2024-08-19)
4748
- [1.0.0 - 2024-06-06](#100---2024-06-06)
4849

50+
## [1.2.12] - 2025-11-12
51+
52+
### Added
53+
- `ArcBroadcast` method in Arc broadcaster for direct access to ARC response
54+
- Missing ARC status constants: `MINED`, `CONFIRMED`, `DOUBLE_SPEND_ATTEMPTED`, `SEEN_IN_ORPHAN_MEMPOOL`
55+
- Test coverage for fee calculation with `TestCalculateFee`
56+
57+
### Changed
58+
- Arc broadcaster refactored with `ArcBroadcast` abstraction for better error handling
59+
60+
### Fixed
61+
- Fee calculation formula to multiply in float space before casting to uint64, ensuring accurate fees for all satoshi rates
62+
4963
## [1.2.11] - 2025-10-27
5064

5165
### Added

transaction/broadcaster/arc.go

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,19 @@ import (
1717
type ArcStatus string
1818

1919
const (
20-
REJECTED ArcStatus = "REJECTED"
21-
QUEUED ArcStatus = "QUEUED"
22-
RECEIVED ArcStatus = "RECEIVED"
23-
STORED ArcStatus = "STORED"
24-
ANNOUNCED_TO_NETWORK ArcStatus = "ANNOUNCED_TO_NETWORK"
25-
REQUESTED_BY_NETWORK ArcStatus = "REQUESTED_BY_NETWORK"
26-
SENT_TO_NETWORK ArcStatus = "SENT_TO_NETWORK"
27-
ACCEPTED_BY_NETWORK ArcStatus = "ACCEPTED_BY_NETWORK"
28-
SEEN_ON_NETWORK ArcStatus = "SEEN_ON_NETWORK"
20+
REJECTED ArcStatus = "REJECTED"
21+
QUEUED ArcStatus = "QUEUED"
22+
RECEIVED ArcStatus = "RECEIVED"
23+
STORED ArcStatus = "STORED"
24+
ANNOUNCED_TO_NETWORK ArcStatus = "ANNOUNCED_TO_NETWORK"
25+
REQUESTED_BY_NETWORK ArcStatus = "REQUESTED_BY_NETWORK"
26+
SENT_TO_NETWORK ArcStatus = "SENT_TO_NETWORK"
27+
ACCEPTED_BY_NETWORK ArcStatus = "ACCEPTED_BY_NETWORK"
28+
SEEN_ON_NETWORK ArcStatus = "SEEN_ON_NETWORK"
29+
MINED ArcStatus = "MINED"
30+
CONFIRMED ArcStatus = "CONFIRMED"
31+
DOUBLE_SPEND_ATTEMPTED ArcStatus = "DOUBLE_SPEND_ATTEMPTED"
32+
SEEN_IN_ORPHAN_MEMPOOL ArcStatus = "SEEN_IN_ORPHAN_MEMPOOL"
2933
)
3034

3135
type Arc struct {
@@ -64,7 +68,7 @@ func (a *Arc) Broadcast(t *transaction.Transaction) (*transaction.BroadcastSucce
6468
return a.BroadcastCtx(context.Background(), t)
6569
}
6670

67-
func (a *Arc) BroadcastCtx(ctx context.Context, t *transaction.Transaction) (*transaction.BroadcastSuccess, *transaction.BroadcastFailure) {
71+
func (a *Arc) ArcBroadcast(ctx context.Context, t *transaction.Transaction) (*ArcResponse, error) {
6872
var buf *bytes.Buffer
6973
for _, input := range t.Inputs {
7074
if input.SourceTxOutput() == nil {
@@ -74,10 +78,7 @@ func (a *Arc) BroadcastCtx(ctx context.Context, t *transaction.Transaction) (*tr
7478
}
7579
if buf == nil {
7680
if ef, err := t.EF(); err != nil {
77-
return nil, &transaction.BroadcastFailure{
78-
Code: "500",
79-
Description: err.Error(),
80-
}
81+
return nil, err
8182
} else {
8283
buf = bytes.NewBuffer(ef)
8384
}
@@ -90,10 +91,7 @@ func (a *Arc) BroadcastCtx(ctx context.Context, t *transaction.Transaction) (*tr
9091
buf,
9192
)
9293
if err != nil {
93-
return nil, &transaction.BroadcastFailure{
94-
Code: "500",
95-
Description: err.Error(),
96-
}
94+
return nil, err
9795
}
9896

9997
req.Header.Set("Content-Type", "application/octet-stream")
@@ -140,25 +138,28 @@ func (a *Arc) BroadcastCtx(ctx context.Context, t *transaction.Transaction) (*tr
140138
}
141139
resp, err := a.Client.Do(req)
142140
if err != nil {
143-
return nil, &transaction.BroadcastFailure{
144-
Code: "500",
145-
Description: err.Error(),
146-
}
141+
return nil, err
147142
}
148143
defer resp.Body.Close()
149144
msg, err := io.ReadAll(resp.Body)
150145
if err != nil {
151-
return nil, &transaction.BroadcastFailure{
152-
Code: "500",
153-
Description: err.Error(),
154-
}
146+
return nil, err
155147
}
156148

157149
response := &ArcResponse{}
158150
if a.Verbose {
159151
log.Println("msg", string(msg))
160152
}
161153
err = json.Unmarshal(msg, &response)
154+
if err != nil {
155+
return nil, err
156+
}
157+
158+
return response, nil
159+
}
160+
161+
func (a *Arc) BroadcastCtx(ctx context.Context, t *transaction.Transaction) (*transaction.BroadcastSuccess, *transaction.BroadcastFailure) {
162+
response, err := a.ArcBroadcast(ctx, t)
162163
if err != nil {
163164
return nil, &transaction.BroadcastFailure{
164165
Code: "500",

transaction/fee_model/sats_per_kb.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,9 @@ func (s *SatoshisPerKilobyte) ComputeFee(tx *transaction.Transaction) (uint64, e
3333
size += len(*o.LockingScript)
3434
}
3535
size += 4
36-
return (uint64(math.Ceil(float64(size) / 1000))) * s.Satoshis, nil
36+
return calculateFee(size, s.Satoshis), nil
37+
}
38+
39+
func calculateFee(txSizeBytes int, satoshisPerKB uint64) uint64 {
40+
return uint64(math.Ceil(float64(txSizeBytes) / 1000 * float64(satoshisPerKB)))
3741
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package feemodel
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestCalculateFee(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
txSize int
13+
satoshisPerKB uint64
14+
expectedFee uint64
15+
description string
16+
}{
17+
{
18+
name: "240 bytes at 100 sats/KB",
19+
txSize: 240,
20+
satoshisPerKB: 100,
21+
expectedFee: 24,
22+
description: "240/1000 * 100 = 24 - tests the bug where casting happened before multiplication",
23+
},
24+
{
25+
name: "240 bytes at 1 sat/KB",
26+
txSize: 240,
27+
satoshisPerKB: 1,
28+
expectedFee: 1,
29+
description: "Edge case that would pass even with buggy implementation",
30+
},
31+
{
32+
name: "240 bytes at 10 sats/KB",
33+
txSize: 240,
34+
satoshisPerKB: 10,
35+
expectedFee: 3,
36+
description: "240/1000 * 10 = 2.4, ceil = 3",
37+
},
38+
{
39+
name: "250 bytes at 500 sats/KB",
40+
txSize: 250,
41+
satoshisPerKB: 500,
42+
expectedFee: 125,
43+
description: "250/1000 * 500 = 125",
44+
},
45+
{
46+
name: "1000 bytes at 100 sats/KB",
47+
txSize: 1000,
48+
satoshisPerKB: 100,
49+
expectedFee: 100,
50+
description: "1000/1000 * 100 = 100",
51+
},
52+
{
53+
name: "1500 bytes at 100 sats/KB",
54+
txSize: 1500,
55+
satoshisPerKB: 100,
56+
expectedFee: 150,
57+
description: "1500/1000 * 100 = 150",
58+
},
59+
{
60+
name: "1500 bytes at 500 sats/KB",
61+
txSize: 1500,
62+
satoshisPerKB: 500,
63+
expectedFee: 750,
64+
description: "1500/1000 * 500 = 750",
65+
},
66+
}
67+
68+
for _, tt := range tests {
69+
t.Run(tt.name, func(t *testing.T) {
70+
fee := calculateFee(tt.txSize, tt.satoshisPerKB)
71+
require.Equal(t, tt.expectedFee, fee, tt.description)
72+
})
73+
}
74+
}

0 commit comments

Comments
 (0)