-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbroadcast.go
More file actions
221 lines (190 loc) · 6.04 KB
/
broadcast.go
File metadata and controls
221 lines (190 loc) · 6.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
// Package paywall implements transaction broadcasting for Bitcoin multisig payments
package paywall
import (
"bytes"
"fmt"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/rpcclient"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/opd-ai/paywall/wallet"
)
// BTCBroadcaster handles Bitcoin transaction broadcasting to the network
// It wraps btcd RPC client and provides validation before broadcasting
type BTCBroadcaster struct {
client *rpcclient.Client
network *chaincfg.Params
}
// NewBTCBroadcaster creates a new Bitcoin broadcaster with RPC client
// Parameters:
// - host: Bitcoin RPC server address (e.g., "localhost:18332")
// - user: RPC username for authentication
// - pass: RPC password for authentication
// - useTLS: Whether to use TLS for RPC connection
// - network: Bitcoin network parameters (mainnet or testnet)
//
// Returns:
// - *BTCBroadcaster: Initialized broadcaster instance
// - error: If RPC connection fails
func NewBTCBroadcaster(host, user, pass string, useTLS bool, network *chaincfg.Params) (*BTCBroadcaster, error) {
if host == "" {
return nil, fmt.Errorf("btc rpc host is required")
}
if user == "" {
return nil, fmt.Errorf("btc rpc user is required")
}
if pass == "" {
return nil, fmt.Errorf("btc rpc password is required")
}
connCfg := &rpcclient.ConnConfig{
Host: host,
User: user,
Pass: pass,
HTTPPostMode: true,
DisableTLS: !useTLS,
}
client, err := rpcclient.New(connCfg, nil)
if err != nil {
return nil, fmt.Errorf("create rpc client: %w", err)
}
return &BTCBroadcaster{
client: client,
network: network,
}, nil
}
// Broadcast sends a Bitcoin transaction to the network
// Parameters:
// - txBytes: Raw transaction bytes to broadcast
//
// Returns:
// - string: Transaction hash/ID if successful
// - error: If broadcast fails or validation fails
func (b *BTCBroadcaster) Broadcast(txBytes []byte) (string, error) {
if len(txBytes) == 0 {
return "", fmt.Errorf("transaction bytes cannot be empty")
}
// Parse transaction to validate it
tx := wire.NewMsgTx(wire.TxVersion)
if err := tx.Deserialize(bytes.NewReader(txBytes)); err != nil {
return "", fmt.Errorf("invalid transaction format: %w", err)
}
// Basic validation
if len(tx.TxIn) == 0 {
return "", fmt.Errorf("transaction has no inputs")
}
if len(tx.TxOut) == 0 {
return "", fmt.Errorf("transaction has no outputs")
}
// Send raw transaction to network
txHash, err := b.client.SendRawTransaction(tx, false)
if err != nil {
return "", fmt.Errorf("broadcast transaction: %w", err)
}
return txHash.String(), nil
}
// ValidateTransaction validates a transaction against payment requirements
// This checks that the transaction matches the expected payment details
// Parameters:
// - txBytes: Raw transaction bytes to validate
// - payment: Payment record containing expected details
//
// Returns:
// - error: nil if valid, error describing validation failure otherwise
func (b *BTCBroadcaster) ValidateTransaction(txBytes []byte, payment *Payment) error {
if len(txBytes) == 0 {
return fmt.Errorf("transaction bytes cannot be empty")
}
if payment == nil {
return fmt.Errorf("payment cannot be nil")
}
// Parse transaction
tx := wire.NewMsgTx(wire.TxVersion)
if err := tx.Deserialize(bytes.NewReader(txBytes)); err != nil {
return fmt.Errorf("invalid transaction format: %w", err)
}
// Validate transaction has inputs and outputs
if len(tx.TxIn) == 0 {
return fmt.Errorf("transaction has no inputs")
}
if len(tx.TxOut) == 0 {
return fmt.Errorf("transaction has no outputs")
}
// Validate outputs match expected payment
btcAddress, ok := payment.Addresses[wallet.Bitcoin]
if !ok || btcAddress == "" {
return fmt.Errorf("payment has no bitcoin address")
}
expectedAmount, ok := payment.Amounts[wallet.Bitcoin]
if !ok {
return fmt.Errorf("payment has no bitcoin amount")
}
// Convert expected amount to satoshis
expectedSatoshis := int64(expectedAmount * 1e8)
// Parse expected address
addr, err := btcutil.DecodeAddress(btcAddress, b.network)
if err != nil {
return fmt.Errorf("invalid payment address: %w", err)
}
// Check if any output sends to the expected address with correct amount
foundCorrectOutput := false
for _, txOut := range tx.TxOut {
// Extract address from output script
_, addrs, _, err := txscript.ExtractPkScriptAddrs(txOut.PkScript, b.network)
if err != nil {
continue
}
// Check if this output matches our expected address
for _, outputAddr := range addrs {
if outputAddr.String() == addr.String() {
// Check amount matches (allow small fee differences)
if txOut.Value >= expectedSatoshis {
foundCorrectOutput = true
break
}
}
}
if foundCorrectOutput {
break
}
}
if !foundCorrectOutput {
return fmt.Errorf("transaction does not pay correct amount to expected address")
}
// Calculate total fee (input value - output value)
// Note: Full validation would require fetching input UTXOs to calculate fee
// For now, we validate structure and outputs only
totalOutput := int64(0)
for _, txOut := range tx.TxOut {
totalOutput += txOut.Value
}
// Sanity check: outputs shouldn't be zero
if totalOutput == 0 {
return fmt.Errorf("transaction has zero output value")
}
return nil
}
// GetLatestBlockTime retrieves the timestamp of the latest Bitcoin block
func (b *BTCBroadcaster) GetLatestBlockTime() (time.Time, error) {
if b.client == nil {
return time.Time{}, fmt.Errorf("rpc client not initialized")
}
// Get best block hash
blockHash, err := b.client.GetBestBlockHash()
if err != nil {
return time.Time{}, fmt.Errorf("get best block hash: %w", err)
}
// Get block header which contains timestamp
blockHeader, err := b.client.GetBlockHeader(blockHash)
if err != nil {
return time.Time{}, fmt.Errorf("get block header: %w", err)
}
return blockHeader.Timestamp, nil
}
// Close closes the RPC client connection
func (b *BTCBroadcaster) Close() {
if b.client != nil {
b.client.Shutdown()
}
}