From d1a98498f9b366d1839703fd9b3677e661cd5fbf Mon Sep 17 00:00:00 2001 From: David Case Date: Mon, 3 Nov 2025 02:28:53 -0500 Subject: [PATCH 1/5] add ArcBroadcast abstraction --- transaction/broadcaster/arc.go | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/transaction/broadcaster/arc.go b/transaction/broadcaster/arc.go index 6315a124..3be6375a 100644 --- a/transaction/broadcaster/arc.go +++ b/transaction/broadcaster/arc.go @@ -64,7 +64,7 @@ func (a *Arc) Broadcast(t *transaction.Transaction) (*transaction.BroadcastSucce return a.BroadcastCtx(context.Background(), t) } -func (a *Arc) BroadcastCtx(ctx context.Context, t *transaction.Transaction) (*transaction.BroadcastSuccess, *transaction.BroadcastFailure) { +func (a *Arc) ArcBroadcast(ctx context.Context, t *transaction.Transaction) (*ArcResponse, error) { var buf *bytes.Buffer for _, input := range t.Inputs { if input.SourceTxOutput() == nil { @@ -74,10 +74,7 @@ func (a *Arc) BroadcastCtx(ctx context.Context, t *transaction.Transaction) (*tr } if buf == nil { if ef, err := t.EF(); err != nil { - return nil, &transaction.BroadcastFailure{ - Code: "500", - Description: err.Error(), - } + return nil, err } else { buf = bytes.NewBuffer(ef) } @@ -90,10 +87,7 @@ func (a *Arc) BroadcastCtx(ctx context.Context, t *transaction.Transaction) (*tr buf, ) if err != nil { - return nil, &transaction.BroadcastFailure{ - Code: "500", - Description: err.Error(), - } + return nil, err } req.Header.Set("Content-Type", "application/octet-stream") @@ -140,18 +134,12 @@ func (a *Arc) BroadcastCtx(ctx context.Context, t *transaction.Transaction) (*tr } resp, err := a.Client.Do(req) if err != nil { - return nil, &transaction.BroadcastFailure{ - Code: "500", - Description: err.Error(), - } + return nil, err } defer resp.Body.Close() msg, err := io.ReadAll(resp.Body) if err != nil { - return nil, &transaction.BroadcastFailure{ - Code: "500", - Description: err.Error(), - } + return nil, err } response := &ArcResponse{} @@ -159,6 +147,15 @@ func (a *Arc) BroadcastCtx(ctx context.Context, t *transaction.Transaction) (*tr log.Println("msg", string(msg)) } err = json.Unmarshal(msg, &response) + if err != nil { + return nil, err + } + + return response, nil +} + +func (a *Arc) BroadcastCtx(ctx context.Context, t *transaction.Transaction) (*transaction.BroadcastSuccess, *transaction.BroadcastFailure) { + response, err := a.ArcBroadcast(ctx, t) if err != nil { return nil, &transaction.BroadcastFailure{ Code: "500", From a342376e1847ec1e810aa98c693aba3994b95bcc Mon Sep 17 00:00:00 2001 From: David Case Date: Tue, 4 Nov 2025 00:21:10 -0500 Subject: [PATCH 2/5] add missing ARC status --- transaction/broadcaster/arc.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/transaction/broadcaster/arc.go b/transaction/broadcaster/arc.go index 3be6375a..53eece88 100644 --- a/transaction/broadcaster/arc.go +++ b/transaction/broadcaster/arc.go @@ -17,15 +17,19 @@ import ( type ArcStatus string const ( - REJECTED ArcStatus = "REJECTED" - QUEUED ArcStatus = "QUEUED" - RECEIVED ArcStatus = "RECEIVED" - STORED ArcStatus = "STORED" - ANNOUNCED_TO_NETWORK ArcStatus = "ANNOUNCED_TO_NETWORK" - REQUESTED_BY_NETWORK ArcStatus = "REQUESTED_BY_NETWORK" - SENT_TO_NETWORK ArcStatus = "SENT_TO_NETWORK" - ACCEPTED_BY_NETWORK ArcStatus = "ACCEPTED_BY_NETWORK" - SEEN_ON_NETWORK ArcStatus = "SEEN_ON_NETWORK" + REJECTED ArcStatus = "REJECTED" + QUEUED ArcStatus = "QUEUED" + RECEIVED ArcStatus = "RECEIVED" + STORED ArcStatus = "STORED" + ANNOUNCED_TO_NETWORK ArcStatus = "ANNOUNCED_TO_NETWORK" + REQUESTED_BY_NETWORK ArcStatus = "REQUESTED_BY_NETWORK" + SENT_TO_NETWORK ArcStatus = "SENT_TO_NETWORK" + ACCEPTED_BY_NETWORK ArcStatus = "ACCEPTED_BY_NETWORK" + SEEN_ON_NETWORK ArcStatus = "SEEN_ON_NETWORK" + MINED ArcStatus = "MINED" + CONFIRMED ArcStatus = "CONFIRMED" + DOUBLE_SPEND_ATTEMPTED ArcStatus = "DOUBLE_SPEND_ATTEMPTED" + SEEN_IN_ORPHAN_MEMPOOL ArcStatus = "SEEN_IN_ORPHAN_MEMPOOL" ) type Arc struct { From 43ca85f3b7fc52a8ec1f09e631b35da5604bf29f Mon Sep 17 00:00:00 2001 From: Deggen Date: Wed, 12 Nov 2025 11:18:57 -0600 Subject: [PATCH 3/5] Conform this function to match SV Node calculation --- transaction/fee_model/sats_per_kb.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transaction/fee_model/sats_per_kb.go b/transaction/fee_model/sats_per_kb.go index 33e0b0fc..d535309a 100644 --- a/transaction/fee_model/sats_per_kb.go +++ b/transaction/fee_model/sats_per_kb.go @@ -33,5 +33,5 @@ func (s *SatoshisPerKilobyte) ComputeFee(tx *transaction.Transaction) (uint64, e size += len(*o.LockingScript) } size += 4 - return (uint64(math.Ceil(float64(size) / 1000))) * s.Satoshis, nil + return (uint64(math.Ceil(float64(size)/1000)) * s.Satoshis), nil } From 2ca2c8c0856fb2f9b5eaf68b0718f3e881b62bbf Mon Sep 17 00:00:00 2001 From: David Case Date: Wed, 12 Nov 2025 14:36:52 -0500 Subject: [PATCH 4/5] Refactor fee calculation and add unit tests for calculateFee function --- transaction/fee_model/sats_per_kb.go | 6 +- transaction/fee_model/sats_per_kb_test.go | 74 +++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 transaction/fee_model/sats_per_kb_test.go diff --git a/transaction/fee_model/sats_per_kb.go b/transaction/fee_model/sats_per_kb.go index d535309a..709d8fd7 100644 --- a/transaction/fee_model/sats_per_kb.go +++ b/transaction/fee_model/sats_per_kb.go @@ -33,5 +33,9 @@ func (s *SatoshisPerKilobyte) ComputeFee(tx *transaction.Transaction) (uint64, e size += len(*o.LockingScript) } size += 4 - return (uint64(math.Ceil(float64(size)/1000)) * s.Satoshis), nil + return calculateFee(size, s.Satoshis), nil +} + +func calculateFee(txSizeBytes int, satoshisPerKB uint64) uint64 { + return uint64(math.Ceil(float64(txSizeBytes) / 1000 * float64(satoshisPerKB))) } diff --git a/transaction/fee_model/sats_per_kb_test.go b/transaction/fee_model/sats_per_kb_test.go new file mode 100644 index 00000000..160f8b7c --- /dev/null +++ b/transaction/fee_model/sats_per_kb_test.go @@ -0,0 +1,74 @@ +package feemodel + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCalculateFee(t *testing.T) { + tests := []struct { + name string + txSize int + satoshisPerKB uint64 + expectedFee uint64 + description string + }{ + { + name: "240 bytes at 100 sats/KB", + txSize: 240, + satoshisPerKB: 100, + expectedFee: 24, + description: "240/1000 * 100 = 24 - tests the bug where casting happened before multiplication", + }, + { + name: "240 bytes at 1 sat/KB", + txSize: 240, + satoshisPerKB: 1, + expectedFee: 1, + description: "Edge case that would pass even with buggy implementation", + }, + { + name: "240 bytes at 10 sats/KB", + txSize: 240, + satoshisPerKB: 10, + expectedFee: 3, + description: "240/1000 * 10 = 2.4, ceil = 3", + }, + { + name: "250 bytes at 500 sats/KB", + txSize: 250, + satoshisPerKB: 500, + expectedFee: 125, + description: "250/1000 * 500 = 125", + }, + { + name: "1000 bytes at 100 sats/KB", + txSize: 1000, + satoshisPerKB: 100, + expectedFee: 100, + description: "1000/1000 * 100 = 100", + }, + { + name: "1500 bytes at 100 sats/KB", + txSize: 1500, + satoshisPerKB: 100, + expectedFee: 150, + description: "1500/1000 * 100 = 150", + }, + { + name: "1500 bytes at 500 sats/KB", + txSize: 1500, + satoshisPerKB: 500, + expectedFee: 750, + description: "1500/1000 * 500 = 750", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fee := calculateFee(tt.txSize, tt.satoshisPerKB) + require.Equal(t, tt.expectedFee, fee, tt.description) + }) + } +} From d3b8b77cc32f86c629049a1e9b90747a1e457534 Mon Sep 17 00:00:00 2001 From: David Case Date: Wed, 12 Nov 2025 14:43:49 -0500 Subject: [PATCH 5/5] update changelog for v1.2.12 --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a5b92a4..1db12699 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. The format ## Table of Contents +- [1.2.12 - 2025-11-12](#1212---2025-11-12) - [1.2.11 - 2025-10-27](#1211---2025-10-27) - [1.2.10 - 2025-09-16](#1210---2025-09-16) - [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 - [1.1.0 - 2024-08-19](#110---2024-08-19) - [1.0.0 - 2024-06-06](#100---2024-06-06) +## [1.2.12] - 2025-11-12 + +### Added +- `ArcBroadcast` method in Arc broadcaster for direct access to ARC response +- Missing ARC status constants: `MINED`, `CONFIRMED`, `DOUBLE_SPEND_ATTEMPTED`, `SEEN_IN_ORPHAN_MEMPOOL` +- Test coverage for fee calculation with `TestCalculateFee` + +### Changed +- Arc broadcaster refactored with `ArcBroadcast` abstraction for better error handling + +### Fixed +- Fee calculation formula to multiply in float space before casting to uint64, ensuring accurate fees for all satoshi rates + ## [1.2.11] - 2025-10-27 ### Added