bsv-sdk 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +53 -0
- data/lib/bsv/auth/certificate.rb +6 -2
- data/lib/bsv/auth/get_verifiable_certificates.rb +6 -6
- data/lib/bsv/auth/peer.rb +10 -4
- data/lib/bsv/auth/session_manager.rb +81 -5
- data/lib/bsv/identity/client.rb +5 -2
- data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +4 -4
- data/lib/bsv/mcp/tools/check_balance.rb +2 -2
- data/lib/bsv/mcp/tools/fetch_utxos.rb +2 -2
- data/lib/bsv/mcp/tools/helpers.rb +2 -2
- data/lib/bsv/network/broadcast_error.rb +2 -0
- data/lib/bsv/network/broadcast_response.rb +4 -1
- data/lib/bsv/network/protocol.rb +56 -4
- data/lib/bsv/network/protocols/arc.rb +10 -6
- data/lib/bsv/network/protocols/chaintracks.rb +6 -2
- data/lib/bsv/network/protocols/jungle_bus.rb +52 -0
- data/lib/bsv/network/protocols/ordinals.rb +110 -8
- data/lib/bsv/network/protocols/taal_binary.rb +18 -4
- data/lib/bsv/network/protocols/woc_rest.rb +166 -85
- data/lib/bsv/network/protocols.rb +1 -0
- data/lib/bsv/network/provider.rb +36 -5
- data/lib/bsv/network/providers/gorilla_pool.rb +42 -20
- data/lib/bsv/network/providers/taal.rb +38 -15
- data/lib/bsv/network/providers/whats_on_chain.rb +42 -21
- data/lib/bsv/network/utxo.rb +8 -2
- data/lib/bsv/overlay/lookup_resolver.rb +5 -4
- data/lib/bsv/overlay/topic_broadcaster.rb +2 -2
- data/lib/bsv/overlay/types.rb +2 -0
- data/lib/bsv/primitives/hex.rb +64 -0
- data/lib/bsv/registry/client.rb +10 -8
- data/lib/bsv/registry/types.rb +2 -0
- data/lib/bsv/script/interpreter/interpreter.rb +7 -0
- data/lib/bsv/script/interpreter/operations/crypto.rb +7 -1
- data/lib/bsv/transaction/beef.rb +223 -147
- data/lib/bsv/transaction/merkle_path.rb +54 -38
- data/lib/bsv/transaction/transaction.rb +103 -40
- data/lib/bsv/transaction/transaction_input.rb +23 -18
- data/lib/bsv/version.rb +1 -1
- data/lib/bsv/wallet/interface/brc100.rb +5 -2
- data/lib/bsv/wallet/proto_wallet/key_deriver.rb +2 -0
- data/lib/bsv/wallet/proto_wallet.rb +6 -0
- data/lib/bsv/wire_format.rb +40 -14
- data/lib/bsv-sdk.rb +14 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6bb4b97822f38d793a308e9beddc94bd90e64457bda8f1782875ddcd8a1ff1f3
|
|
4
|
+
data.tar.gz: 85009a67a04ee1264acd883224ea418ed46558629a18db3d1757486cc544852d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 85e2958f6bb6af774b2ca06e5187bbc1c756546e322430375108617b4675e6e5d5373aa8d93fd4504327d3c48e29507a2caea566d0b9af5e16c3fdd089bc2fbc
|
|
7
|
+
data.tar.gz: 56797e445e8996c2a41e85484e49f53b3568eaebeaf01de39a1e919c6949f40b172236ad5bd89fbc77eb9405a532653c9ab1972e696e699d6410e503318360d7
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,59 @@ All notable changes to the `bsv-sdk` gem are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
6
6
|
and this gem adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## 0.18.0 — 2026-05-09
|
|
9
|
+
|
|
10
|
+
### Breaking Changes
|
|
11
|
+
- Eliminated display-order binary txid from public API — all methods now use `wtxid` (wire-order) internally (#690)
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- Provider auth and rate limit metadata: `auth:`, `rate_limit:`, `authenticated?` on Provider; generic auth dispatch in Protocol `build_request` (#718)
|
|
15
|
+
- JungleBus protocol: transaction lookup, address queries, block headers via GorillaPool JungleBus REST API (#717)
|
|
16
|
+
- Ordinals protocol: fixed endpoint paths, added tx details, tx status, UTXOs, balance, spends, chain tip (#727)
|
|
17
|
+
- Full WoC API coverage: expanded from 30 to 54 endpoints with complete address, script, UTXO, and analytics support (#715)
|
|
18
|
+
- Integration tests for ARC, JungleBus, Ordinals, and WoCREST against live APIs (#726)
|
|
19
|
+
- API documentation URLs in all protocol file headers (#730)
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- Ordinals `get_tx` path corrected (`/hex` → `/raw`) and response handler fixed (binary, not JSON) (#727)
|
|
23
|
+
- Auth normalisation: `{ bearer: nil }` and `{ api_key: nil }` treated as unauthenticated (#725)
|
|
24
|
+
- WoC health endpoint path corrected (`/health` → `/woc`)
|
|
25
|
+
- Bulk POST body formats and `get_utxos_all` path corrected for WoC
|
|
26
|
+
- Lazy removal of expired sessions during identity-key lookup (#707)
|
|
27
|
+
- Session TTL expiration added to SessionManager (#707)
|
|
28
|
+
- Guard against nil source data and out-of-bounds input_index in sighash (#702)
|
|
29
|
+
- Wire format deep conversion depth limit (#709)
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- BeefTx refactored to polymorphic subclass hierarchy with validated constructors (#692-698)
|
|
33
|
+
- WoCREST `build_request` override removed — replaced by generic auth dispatch
|
|
34
|
+
- MCP dependency bumped from ~> 0.12 to ~> 0.15 (#700)
|
|
35
|
+
|
|
36
|
+
## 0.17.0 — 2026-05-02
|
|
37
|
+
|
|
38
|
+
### Breaking Changes
|
|
39
|
+
- Renamed `TransactionInput#prev_tx_id` to `#prev_wtxid` — all call sites must update (#678)
|
|
40
|
+
- Renamed MerklePath parameters from `txid_hex` to `dtxid_hex` throughout (#681)
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
- `Transaction#wtxid` — wire-order transaction ID (raw SHA-256d bytes) (#673)
|
|
44
|
+
- `Transaction#dtxid` / `#dtxid_hex` — display-order hex aliases (#680)
|
|
45
|
+
- `TransactionInput#dtxid_hex` — display-order hex of the referenced transaction (#678)
|
|
46
|
+
- `TransactionInput.wtxid_from_hex` — convert display-order hex to wire-order bytes (#678)
|
|
47
|
+
- `Hex.validate_wtxid!` / `Hex.validate_dtxid_hex!` — runtime validation for wire/display txid formats (#685)
|
|
48
|
+
- `Hex.validate_hash32!` — general-purpose 32-byte binary hash validator (#686)
|
|
49
|
+
- `BSV.logger` — opt-in debug instrumentation with zero overhead when unused (#686)
|
|
50
|
+
- Debug logging at txid conversions, sighash preimage, script interpreter, ARC broadcast, BEEF ancestry, and ProtoWallet key derivation (#686, #688)
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
- All internal txid variables renamed to `wtxid` (wire-order) convention (#679)
|
|
54
|
+
- BEEF internals enforce wire-order throughout — no byte-reversals inside the bundle (#675)
|
|
55
|
+
- MerklePath `compute_root_hex` and `from_tsc` now validate hex input (#686)
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
- `BeefTx#dtxid` now returns hex string (was returning binary) (#677)
|
|
59
|
+
- Certificate field naming aligned with BRC-52 convention (#677)
|
|
60
|
+
|
|
8
61
|
## 0.16.0 — 2026-05-01
|
|
9
62
|
|
|
10
63
|
### Breaking Changes
|
data/lib/bsv/auth/certificate.rb
CHANGED
|
@@ -81,6 +81,9 @@ module BSV
|
|
|
81
81
|
buf << [@subject].pack('H*')
|
|
82
82
|
buf << [@certifier].pack('H*')
|
|
83
83
|
|
|
84
|
+
# Certificate binary format: revocation outpoint txid stored in display byte
|
|
85
|
+
# order (matching TS and Go SDKs). Go encodes via WriteBytesReverse(wire_order),
|
|
86
|
+
# TS encodes via toArray(display_hex) — both produce identical display-order bytes.
|
|
84
87
|
txid_hex, output_index_str = @revocation_outpoint.to_s.split('.', 2)
|
|
85
88
|
buf << [txid_hex].pack('H*')
|
|
86
89
|
buf << BSV::Transaction::VarInt.encode(output_index_str.to_i)
|
|
@@ -124,7 +127,8 @@ module BSV
|
|
|
124
127
|
certifier_bytes = data.byteslice(pos, 33)
|
|
125
128
|
pos += 33
|
|
126
129
|
|
|
127
|
-
|
|
130
|
+
# Outpoint txid bytes — stored in display byte order (matching TS and Go SDKs).
|
|
131
|
+
outpoint_txid = data.byteslice(pos, 32)
|
|
128
132
|
pos += 32
|
|
129
133
|
output_index, vi_len = BSV::Transaction::VarInt.decode(data, pos)
|
|
130
134
|
pos += vi_len
|
|
@@ -158,7 +162,7 @@ module BSV
|
|
|
158
162
|
serial_number: Base64.strict_encode64(serial_bytes),
|
|
159
163
|
subject: subject_bytes.unpack1('H*'),
|
|
160
164
|
certifier: certifier_bytes.unpack1('H*'),
|
|
161
|
-
revocation_outpoint: "#{
|
|
165
|
+
revocation_outpoint: "#{outpoint_txid.unpack1('H*')}.#{output_index}",
|
|
162
166
|
fields: fields,
|
|
163
167
|
signature: signature
|
|
164
168
|
)
|
|
@@ -21,7 +21,9 @@ module BSV
|
|
|
21
21
|
# - +:types+ [Hash] type (Base64 string) → array of field names to reveal
|
|
22
22
|
# @param verifier_identity_key [String] the verifier's compressed public key hex
|
|
23
23
|
# @return [Array<VerifiableCertificate>] list of verifiable certificates ready for
|
|
24
|
-
# presentation, or +[]+
|
|
24
|
+
# presentation, or +[]+ when the wallet does not support certificate operations
|
|
25
|
+
# @raise [StandardError] propagates unexpected errors (network failures, key derivation
|
|
26
|
+
# errors, etc.) to the caller — only +UnsupportedActionError+ is swallowed
|
|
25
27
|
def get_verifiable_certificates(wallet, requested_certificates, verifier_identity_key)
|
|
26
28
|
return [] unless wallet.respond_to?(:list_certificates) && wallet.respond_to?(:prove_certificate)
|
|
27
29
|
|
|
@@ -65,11 +67,9 @@ module BSV
|
|
|
65
67
|
signature: cert[:signature] || cert['signature']
|
|
66
68
|
)
|
|
67
69
|
end
|
|
68
|
-
rescue
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
# "no certificates" gracefully — the requesting peer enforces its own
|
|
72
|
-
# certificate requirements independently.
|
|
70
|
+
rescue BSV::Wallet::UnsupportedActionError
|
|
71
|
+
# Wallet does not implement certificate operations (e.g. ProtoWallet).
|
|
72
|
+
# Return empty — the requesting peer enforces its own requirements independently.
|
|
73
73
|
[]
|
|
74
74
|
end
|
|
75
75
|
end
|
data/lib/bsv/auth/peer.rb
CHANGED
|
@@ -488,7 +488,7 @@ module BSV
|
|
|
488
488
|
signature = fetch!(message, :signature)
|
|
489
489
|
|
|
490
490
|
# Verify the echoed nonce is one we created
|
|
491
|
-
|
|
491
|
+
verify_nonce!(our_nonce, context: "initial response from peer: #{peer_key}")
|
|
492
492
|
|
|
493
493
|
session = @session_manager.get_session(our_nonce)
|
|
494
494
|
raise AuthError, "No pending session for nonce: #{our_nonce.inspect}" unless session
|
|
@@ -566,7 +566,7 @@ module BSV
|
|
|
566
566
|
msg_nonce = fetch!(message, :nonce)
|
|
567
567
|
|
|
568
568
|
# Verify the echoed nonce is one we created
|
|
569
|
-
|
|
569
|
+
verify_nonce!(our_nonce, context: "general message from: #{peer_key}")
|
|
570
570
|
|
|
571
571
|
session = @session_manager.get_session(our_nonce)
|
|
572
572
|
raise AuthError, "Session not found for nonce: #{our_nonce.inspect}" unless session
|
|
@@ -602,7 +602,7 @@ module BSV
|
|
|
602
602
|
requested = message[:requested_certificates] || message['requested_certificates']
|
|
603
603
|
signature = fetch!(message, :signature)
|
|
604
604
|
|
|
605
|
-
|
|
605
|
+
verify_nonce!(our_nonce, context: "certificate request from: #{peer_key}")
|
|
606
606
|
|
|
607
607
|
session = @session_manager.get_session(our_nonce)
|
|
608
608
|
raise AuthError, "Session not found for nonce: #{our_nonce.inspect}" unless session
|
|
@@ -647,7 +647,7 @@ module BSV
|
|
|
647
647
|
certs = message[:certificates] || message['certificates'] || []
|
|
648
648
|
signature = fetch!(message, :signature)
|
|
649
649
|
|
|
650
|
-
|
|
650
|
+
verify_nonce!(our_nonce, context: "certificate response from: #{peer_key}")
|
|
651
651
|
|
|
652
652
|
session = @session_manager.get_session(our_nonce)
|
|
653
653
|
raise AuthError, "Session not found for nonce: #{our_nonce.inspect}" unless session
|
|
@@ -773,6 +773,12 @@ module BSV
|
|
|
773
773
|
certs.map { |c| c.respond_to?(:to_h) ? c.to_h : c }
|
|
774
774
|
end
|
|
775
775
|
|
|
776
|
+
def verify_nonce!(nonce, context:)
|
|
777
|
+
return if Nonce.verify(nonce, @wallet)
|
|
778
|
+
|
|
779
|
+
raise AuthError, "Nonce verification failed for #{context}"
|
|
780
|
+
end
|
|
781
|
+
|
|
776
782
|
def current_time_ms
|
|
777
783
|
(Time.now.to_f * 1000).to_i
|
|
778
784
|
end
|
|
@@ -9,9 +9,16 @@ module BSV
|
|
|
9
9
|
# peer identity key are supported — the most recently updated session
|
|
10
10
|
# is returned when looking up by identity key.
|
|
11
11
|
#
|
|
12
|
+
# Sessions expire after +default_ttl+ seconds (default: 3600). Pass
|
|
13
|
+
# +default_ttl: nil+ to disable expiry entirely. Expired sessions are
|
|
14
|
+
# removed lazily on access; call {#sweep_expired} for proactive cleanup.
|
|
15
|
+
#
|
|
12
16
|
# Matches the ts-sdk SessionManager dual-index design.
|
|
13
17
|
class SessionManager
|
|
14
|
-
|
|
18
|
+
# @param default_ttl [Integer, nil] seconds before a session expires;
|
|
19
|
+
# +nil+ disables TTL entirely.
|
|
20
|
+
def initialize(default_ttl: 3600)
|
|
21
|
+
@default_ttl = default_ttl
|
|
15
22
|
# session_nonce -> PeerSession
|
|
16
23
|
@by_nonce = {}
|
|
17
24
|
# peer_identity_key -> Set of session_nonces
|
|
@@ -50,25 +57,40 @@ module BSV
|
|
|
50
57
|
#
|
|
51
58
|
# When the identifier is a session nonce, returns that exact session.
|
|
52
59
|
# When the identifier is a peer identity key, returns the most recently
|
|
53
|
-
# updated session for that peer.
|
|
60
|
+
# updated non-expired session for that peer.
|
|
61
|
+
#
|
|
62
|
+
# Returns +nil+ if the session has expired (and removes it from the store).
|
|
54
63
|
#
|
|
55
64
|
# @param identifier [String]
|
|
56
65
|
# @return [PeerSession, nil]
|
|
57
66
|
def get_session(identifier)
|
|
58
67
|
@mutex.synchronize do
|
|
59
68
|
direct = @by_nonce[identifier]
|
|
60
|
-
|
|
69
|
+
if direct
|
|
70
|
+
if expired_locked?(direct)
|
|
71
|
+
remove_session_locked(direct)
|
|
72
|
+
return nil
|
|
73
|
+
end
|
|
74
|
+
return direct
|
|
75
|
+
end
|
|
61
76
|
|
|
62
77
|
nonces = @by_identity[identifier]
|
|
63
78
|
return nil if nonces.nil? || nonces.empty?
|
|
64
79
|
|
|
65
80
|
best = nil
|
|
81
|
+
expired_nonces = []
|
|
66
82
|
nonces.each do |nonce|
|
|
67
83
|
s = @by_nonce[nonce]
|
|
68
84
|
next if s.nil?
|
|
69
85
|
|
|
86
|
+
if expired_locked?(s)
|
|
87
|
+
expired_nonces << s
|
|
88
|
+
next
|
|
89
|
+
end
|
|
90
|
+
|
|
70
91
|
best = s if best.nil? || (s.last_update || 0) > (best.last_update || 0)
|
|
71
92
|
end
|
|
93
|
+
expired_nonces.each { |s| remove_session_locked(s) }
|
|
72
94
|
best
|
|
73
95
|
end
|
|
74
96
|
end
|
|
@@ -84,13 +106,53 @@ module BSV
|
|
|
84
106
|
# @return [Boolean]
|
|
85
107
|
def session?(identifier)
|
|
86
108
|
@mutex.synchronize do
|
|
87
|
-
|
|
109
|
+
direct = @by_nonce[identifier]
|
|
110
|
+
if direct
|
|
111
|
+
if expired_locked?(direct)
|
|
112
|
+
remove_session_locked(direct)
|
|
113
|
+
return false
|
|
114
|
+
end
|
|
115
|
+
return true
|
|
116
|
+
end
|
|
88
117
|
|
|
89
118
|
nonces = @by_identity[identifier]
|
|
90
|
-
|
|
119
|
+
return false if nonces.nil? || nonces.empty?
|
|
120
|
+
|
|
121
|
+
has_active = false
|
|
122
|
+
expired_sessions = []
|
|
123
|
+
nonces.each do |nonce|
|
|
124
|
+
s = @by_nonce[nonce]
|
|
125
|
+
next if s.nil?
|
|
126
|
+
|
|
127
|
+
if expired_locked?(s)
|
|
128
|
+
expired_sessions << s
|
|
129
|
+
else
|
|
130
|
+
has_active = true
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
expired_sessions.each { |s| remove_session_locked(s) }
|
|
134
|
+
has_active
|
|
91
135
|
end
|
|
92
136
|
end
|
|
93
137
|
|
|
138
|
+
# Removes all expired sessions from the store.
|
|
139
|
+
#
|
|
140
|
+
# Not called automatically — intended for use in long-running servers
|
|
141
|
+
# that want to proactively reclaim memory.
|
|
142
|
+
#
|
|
143
|
+
# @return [Integer] number of sessions removed
|
|
144
|
+
def sweep_expired
|
|
145
|
+
removed = 0
|
|
146
|
+
@mutex.synchronize do
|
|
147
|
+
expired = @by_nonce.values.select { |s| expired_locked?(s) }
|
|
148
|
+
expired.each do |s|
|
|
149
|
+
remove_session_locked(s)
|
|
150
|
+
removed += 1
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
removed
|
|
154
|
+
end
|
|
155
|
+
|
|
94
156
|
private
|
|
95
157
|
|
|
96
158
|
def add_session_locked(session)
|
|
@@ -115,6 +177,20 @@ module BSV
|
|
|
115
177
|
nonces.delete(session.session_nonce)
|
|
116
178
|
@by_identity.delete(session.peer_identity_key) if nonces.empty?
|
|
117
179
|
end
|
|
180
|
+
|
|
181
|
+
# Must be called within @mutex.
|
|
182
|
+
def expired_locked?(session)
|
|
183
|
+
return false if @default_ttl.nil?
|
|
184
|
+
|
|
185
|
+
last = session.last_update
|
|
186
|
+
return true if last.nil? || last.zero?
|
|
187
|
+
|
|
188
|
+
current_time_ms - last > @default_ttl * 1000
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def current_time_ms
|
|
192
|
+
(Time.now.to_f * 1000).to_i
|
|
193
|
+
end
|
|
118
194
|
end
|
|
119
195
|
end
|
|
120
196
|
end
|
data/lib/bsv/identity/client.rb
CHANGED
|
@@ -203,10 +203,13 @@ module BSV
|
|
|
203
203
|
raise 'Revoke failed: invalid outputIndex from overlay' if output_idx.negative?
|
|
204
204
|
|
|
205
205
|
beef = BSV::Transaction::Beef.from_binary(beef_bytes)
|
|
206
|
-
|
|
207
|
-
raise 'Revoke failed: no transaction found in BEEF'
|
|
206
|
+
beef_tx = beef.transactions.last
|
|
207
|
+
raise 'Revoke failed: no transaction found in BEEF' if beef_tx.nil? || beef_tx.is_a?(BSV::Transaction::Beef::TxidOnlyEntry)
|
|
208
|
+
|
|
209
|
+
tx = beef_tx.transaction
|
|
208
210
|
raise 'Revoke failed: outputIndex out of range' if output_idx >= tx.outputs.length
|
|
209
211
|
|
|
212
|
+
# Overlay API boundary: outpoint uses display-order hex txid as per overlay convention
|
|
210
213
|
txid = tx.txid_hex
|
|
211
214
|
outpoint = "#{txid}.#{output_idx}"
|
|
212
215
|
|
|
@@ -97,8 +97,8 @@ module BSV
|
|
|
97
97
|
|
|
98
98
|
all_utxos = utxo_result.data.map do |entry|
|
|
99
99
|
BSV::Network::UTXO.new(
|
|
100
|
-
tx_hash: entry[
|
|
101
|
-
|
|
100
|
+
tx_hash: entry['tx_hash'], tx_pos: entry['tx_pos'],
|
|
101
|
+
value: entry['value'], height: entry['height']
|
|
102
102
|
)
|
|
103
103
|
end
|
|
104
104
|
|
|
@@ -116,7 +116,7 @@ module BSV
|
|
|
116
116
|
return Helpers.error_response("Broadcast failed: #{arc_result.message}") unless arc_result.success?
|
|
117
117
|
|
|
118
118
|
result = {
|
|
119
|
-
txid: arc_result.data[:txid],
|
|
119
|
+
txid: arc_result.data[:txid], # MCP tool boundary: display-order hex from ARC response
|
|
120
120
|
tx_status: arc_result.data[:tx_status],
|
|
121
121
|
hex: tx.to_hex
|
|
122
122
|
}
|
|
@@ -154,7 +154,7 @@ module BSV
|
|
|
154
154
|
selected_utxos.each do |utxo|
|
|
155
155
|
locking_script = p2pkh_lock_for(sender_address)
|
|
156
156
|
input = BSV::Transaction::TransactionInput.new(
|
|
157
|
-
|
|
157
|
+
prev_wtxid: BSV::Transaction::TransactionInput.wtxid_from_hex(utxo.tx_hash),
|
|
158
158
|
prev_tx_out_index: utxo.tx_pos
|
|
159
159
|
)
|
|
160
160
|
input.source_satoshis = utxo.satoshis
|
|
@@ -71,8 +71,8 @@ module BSV
|
|
|
71
71
|
|
|
72
72
|
utxos = utxo_result.data.map do |entry|
|
|
73
73
|
BSV::Network::UTXO.new(
|
|
74
|
-
tx_hash: entry[
|
|
75
|
-
|
|
74
|
+
tx_hash: entry['tx_hash'], tx_pos: entry['tx_pos'],
|
|
75
|
+
value: entry['value'], height: entry['height']
|
|
76
76
|
)
|
|
77
77
|
end
|
|
78
78
|
|
|
@@ -62,8 +62,8 @@ module BSV
|
|
|
62
62
|
|
|
63
63
|
utxos = utxo_result.data.map do |entry|
|
|
64
64
|
BSV::Network::UTXO.new(
|
|
65
|
-
tx_hash: entry[
|
|
66
|
-
|
|
65
|
+
tx_hash: entry['tx_hash'], tx_pos: entry['tx_pos'],
|
|
66
|
+
value: entry['value'], height: entry['height']
|
|
67
67
|
)
|
|
68
68
|
end
|
|
69
69
|
|
|
@@ -25,7 +25,7 @@ module BSV
|
|
|
25
25
|
# @return [Hash]
|
|
26
26
|
def self.transaction_to_h(tx)
|
|
27
27
|
{
|
|
28
|
-
txid: tx.txid_hex,
|
|
28
|
+
txid: tx.txid_hex, # MCP tool boundary: display-order hex for human consumption
|
|
29
29
|
version: tx.version,
|
|
30
30
|
lock_time: tx.lock_time,
|
|
31
31
|
inputs: tx.inputs.each_with_index.map { |inp, i| input_to_h(inp, i) },
|
|
@@ -38,7 +38,7 @@ module BSV
|
|
|
38
38
|
def self.input_to_h(input, _index)
|
|
39
39
|
unlock_script = input.unlocking_script
|
|
40
40
|
{
|
|
41
|
-
prev_txid: input.
|
|
41
|
+
prev_txid: input.dtxid_hex,
|
|
42
42
|
vout: input.prev_tx_out_index,
|
|
43
43
|
script_hex: unlock_script ? unlock_script.to_hex : '',
|
|
44
44
|
script_asm: unlock_script ? unlock_script.to_asm : '',
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
module BSV
|
|
4
4
|
module Network
|
|
5
5
|
class BroadcastError < StandardError
|
|
6
|
+
# ARC API boundary: display-order hex txid as returned by the ARC error response.
|
|
6
7
|
attr_reader :status_code, :txid, :arc_status
|
|
7
8
|
|
|
8
9
|
def initialize(message, status_code: nil, txid: nil, arc_status: nil)
|
|
9
10
|
@status_code = status_code
|
|
11
|
+
BSV::Primitives::Hex.validate_dtxid_hex!(txid, name: 'ARC error txid') if txid
|
|
10
12
|
@txid = txid
|
|
11
13
|
@arc_status = arc_status
|
|
12
14
|
super(message)
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
module BSV
|
|
4
4
|
module Network
|
|
5
5
|
class BroadcastResponse
|
|
6
|
+
# ARC API boundary: display-order hex txid as returned by the ARC broadcast endpoint.
|
|
6
7
|
attr_reader :txid, :tx_status, :message, :extra_info, :block_hash, :block_height, :timestamp, :competing_txs
|
|
7
8
|
|
|
8
9
|
def initialize(attrs = {})
|
|
9
|
-
|
|
10
|
+
txid = attrs[:txid]
|
|
11
|
+
BSV::Primitives::Hex.validate_dtxid_hex!(txid, name: 'ARC broadcast txid') if txid
|
|
12
|
+
@txid = txid
|
|
10
13
|
@tx_status = attrs[:tx_status]
|
|
11
14
|
@message = attrs[:message]
|
|
12
15
|
@extra_info = attrs[:extra_info]
|
data/lib/bsv/network/protocol.rb
CHANGED
|
@@ -103,14 +103,21 @@ module BSV
|
|
|
103
103
|
@endpoints = {}
|
|
104
104
|
@subscriptions = {}
|
|
105
105
|
|
|
106
|
-
attr_reader :base_url, :api_key, :network, :http_client
|
|
106
|
+
attr_reader :base_url, :api_key, :auth, :network, :http_client
|
|
107
107
|
|
|
108
108
|
# @param base_url [String] base URL, may contain +{network}+ placeholder
|
|
109
|
-
# @param api_key [String, nil] API key
|
|
109
|
+
# @param api_key [String, nil] legacy API key — sends +Authorization: Bearer <key>+
|
|
110
|
+
# @param auth [Hash, Symbol, nil] auth config hash; takes precedence over +api_key:+.
|
|
111
|
+
# Supported forms:
|
|
112
|
+
# - +{ bearer: 'token' }+ → +Authorization: Bearer token+
|
|
113
|
+
# - +{ api_key: 'key' }+ → +Authorization: key+ (no Bearer prefix, WoC style)
|
|
114
|
+
# - +{ api_key: 'key', header: 'X-Custom' }+ → +X-Custom: key+
|
|
115
|
+
# - +:none+ or +nil+ → no auth header
|
|
110
116
|
# @param network [String, Symbol, nil] network name (e.g. 'main', 'test')
|
|
111
117
|
# @param http_client [Object, nil] injectable HTTP client (used in Task 3)
|
|
112
|
-
def initialize(base_url:, api_key: nil, network: nil, http_client: nil)
|
|
118
|
+
def initialize(base_url:, api_key: nil, auth: nil, network: nil, http_client: nil)
|
|
113
119
|
@api_key = api_key
|
|
120
|
+
@auth = normalise_auth(auth)
|
|
114
121
|
@network = network
|
|
115
122
|
@http_client = http_client
|
|
116
123
|
@base_url = build_base_url(base_url, network)
|
|
@@ -228,6 +235,14 @@ module BSV
|
|
|
228
235
|
|
|
229
236
|
# Builds a Net::HTTP request for the given method, URI, and optional body.
|
|
230
237
|
#
|
|
238
|
+
# Auth header dispatch (in priority order):
|
|
239
|
+
# 1. +auth:+ config hash takes precedence over the legacy +api_key:+ shorthand.
|
|
240
|
+
# 2. +{ bearer: 'token' }+ → +Authorization: Bearer token+
|
|
241
|
+
# 3. +{ api_key: 'key', header: 'X-Custom' }+ → +X-Custom: key+
|
|
242
|
+
# 4. +{ api_key: 'key' }+ → +Authorization: key+ (no Bearer prefix)
|
|
243
|
+
# 5. +auth: :none+ or no auth at all → no Authorization header set
|
|
244
|
+
# 6. Legacy +api_key:+ (no auth: provided) → +Authorization: Bearer api_key+
|
|
245
|
+
#
|
|
231
246
|
# @param http_method [Symbol] +:get+ or +:post+
|
|
232
247
|
# @param uri [URI]
|
|
233
248
|
# @param body [String, nil] raw body for POST requests
|
|
@@ -240,7 +255,8 @@ module BSV
|
|
|
240
255
|
else raise ArgumentError, "unsupported HTTP method: #{http_method}"
|
|
241
256
|
end
|
|
242
257
|
|
|
243
|
-
request
|
|
258
|
+
apply_auth(request)
|
|
259
|
+
|
|
244
260
|
if body && request.respond_to?(:body=)
|
|
245
261
|
request.body = body
|
|
246
262
|
request.content_type = 'application/json' unless request.content_type
|
|
@@ -248,6 +264,40 @@ module BSV
|
|
|
248
264
|
request
|
|
249
265
|
end
|
|
250
266
|
|
|
267
|
+
# Applies the auth header to the request based on the +auth:+ config or
|
|
268
|
+
# the legacy +api_key:+ shorthand.
|
|
269
|
+
#
|
|
270
|
+
# @param request [Net::HTTPRequest]
|
|
271
|
+
def apply_auth(request)
|
|
272
|
+
# auth: config takes precedence over legacy api_key:
|
|
273
|
+
if @auth != :none
|
|
274
|
+
auth = @auth
|
|
275
|
+
if auth[:bearer]
|
|
276
|
+
request['Authorization'] = "Bearer #{auth[:bearer]}"
|
|
277
|
+
elsif auth[:api_key]
|
|
278
|
+
header = auth[:header] || 'Authorization'
|
|
279
|
+
request[header] = auth[:api_key]
|
|
280
|
+
end
|
|
281
|
+
elsif @api_key
|
|
282
|
+
# Legacy shorthand: api_key: without auth: sends Bearer
|
|
283
|
+
request['Authorization'] = "Bearer #{@api_key}"
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Normalises the +auth+ argument so that +nil+ and empty hashes are
|
|
288
|
+
# stored as +:none+, giving a single canonical sentinel value for
|
|
289
|
+
# "no authentication".
|
|
290
|
+
#
|
|
291
|
+
# @param auth [Hash, Symbol, nil]
|
|
292
|
+
# @return [Hash, Symbol]
|
|
293
|
+
def normalise_auth(auth)
|
|
294
|
+
return :none if auth.nil?
|
|
295
|
+
return :none if auth == :none
|
|
296
|
+
return :none if auth.is_a?(Hash) && (auth.empty? || (auth[:bearer].nil? && auth[:api_key].nil?))
|
|
297
|
+
|
|
298
|
+
auth
|
|
299
|
+
end
|
|
300
|
+
|
|
251
301
|
# Executes the request via the injectable client or +Net::HTTP.start+.
|
|
252
302
|
#
|
|
253
303
|
# @param uri [URI]
|
|
@@ -305,6 +355,8 @@ module BSV
|
|
|
305
355
|
JSON.parse(body)
|
|
306
356
|
when :json_array
|
|
307
357
|
parsed = JSON.parse(body)
|
|
358
|
+
# Some providers (e.g. WoC) wrap arrays in { "result": [...] }
|
|
359
|
+
parsed = parsed['result'] if parsed.is_a?(Hash) && parsed.key?('result')
|
|
308
360
|
raise TypeError, "expected Array, got #{parsed.class}" unless parsed.is_a?(Array)
|
|
309
361
|
|
|
310
362
|
parsed
|
|
@@ -24,6 +24,8 @@ module BSV
|
|
|
24
24
|
# result = arc.call(:broadcast, tx)
|
|
25
25
|
# result.success? # => true
|
|
26
26
|
# result.data[:txid] # => "abc123..."
|
|
27
|
+
#
|
|
28
|
+
# @see https://docs.gorillapool.io/arc/api.html ARC API v1 documentation
|
|
27
29
|
class ARC < Protocol
|
|
28
30
|
# ARC response statuses that indicate a transaction was NOT accepted.
|
|
29
31
|
# Matches the TypeScript SDK's ARC broadcaster failure set.
|
|
@@ -45,16 +47,17 @@ module BSV
|
|
|
45
47
|
endpoint :health, :get, '/v1/health', response: :json
|
|
46
48
|
|
|
47
49
|
# @param base_url [String] ARC base URL (may contain {network})
|
|
48
|
-
# @param api_key [String, nil]
|
|
50
|
+
# @param api_key [String, nil] legacy bearer token shorthand — use +auth:+ for new code
|
|
51
|
+
# @param auth [Hash, Symbol, nil] auth config; takes precedence over +api_key:+
|
|
49
52
|
# @param network [String, nil] network name for base URL interpolation
|
|
50
53
|
# @param deployment_id [String, nil] deployment identifier for the
|
|
51
54
|
# XDeployment-ID header; defaults to a per-instance random hex value
|
|
52
55
|
# @param callback_url [String, nil] optional X-CallbackUrl header value
|
|
53
56
|
# @param callback_token [String, nil] optional X-CallbackToken header value
|
|
54
57
|
# @param http_client [#request, nil] injectable HTTP client for testing
|
|
55
|
-
def initialize(base_url:, api_key: nil, network: nil, deployment_id: nil,
|
|
58
|
+
def initialize(base_url:, api_key: nil, auth: nil, network: nil, deployment_id: nil,
|
|
56
59
|
callback_url: nil, callback_token: nil, http_client: nil)
|
|
57
|
-
super(base_url: base_url, api_key: api_key, network: network, http_client: http_client)
|
|
60
|
+
super(base_url: base_url, api_key: api_key, auth: auth, network: network, http_client: http_client)
|
|
58
61
|
@deployment_id = deployment_id || "bsv-ruby-sdk-#{SecureRandom.hex(8)}"
|
|
59
62
|
@callback_url = callback_url
|
|
60
63
|
@callback_token = callback_token
|
|
@@ -138,7 +141,8 @@ module BSV
|
|
|
138
141
|
# lacks source_satoshis / source_locking_script.
|
|
139
142
|
def ef_hex_with_fallback(tx)
|
|
140
143
|
tx.to_ef_hex
|
|
141
|
-
rescue ArgumentError
|
|
144
|
+
rescue ArgumentError => e
|
|
145
|
+
BSV.logger&.debug { "[ARC] EF serialisation failed: #{e.message} — falling back to raw hex" }
|
|
142
146
|
tx.to_hex
|
|
143
147
|
end
|
|
144
148
|
|
|
@@ -269,7 +273,7 @@ module BSV
|
|
|
269
273
|
# same field set as broadcast responses rather than the raw parsed JSON.
|
|
270
274
|
# Also checks for rejection status and missing txid (malformed 2xx).
|
|
271
275
|
#
|
|
272
|
-
# @param txid [String]
|
|
276
|
+
# @param txid [String] ARC API boundary: display-order hex transaction ID to query
|
|
273
277
|
# @return [Result::Success, Result::Error, Result::NotFound]
|
|
274
278
|
def call_get_tx_status(txid, **)
|
|
275
279
|
response = default_call(:get_tx_status, txid)
|
|
@@ -306,7 +310,7 @@ module BSV
|
|
|
306
310
|
# @return [Hash]
|
|
307
311
|
def arc_data_from(body)
|
|
308
312
|
{
|
|
309
|
-
txid: body['txid'],
|
|
313
|
+
txid: body['txid'], # ARC API boundary: display-order hex from the ARC JSON response
|
|
310
314
|
tx_status: body['txStatus'],
|
|
311
315
|
message: body['title'],
|
|
312
316
|
extra_info: body['extraInfo'],
|
|
@@ -22,15 +22,19 @@ module BSV
|
|
|
22
22
|
#
|
|
23
23
|
# result = ct.call(:get_block_header, 800_000)
|
|
24
24
|
# result.data # => { 'hash' => '...', 'height' => 800000, 'merkleRoot' => '...' }
|
|
25
|
+
#
|
|
26
|
+
# @note Chaintracks is an internal GorillaPool service; no public API documentation
|
|
27
|
+
# is available.
|
|
25
28
|
class Chaintracks < Protocol
|
|
26
29
|
endpoint :get_block_header, :get, '/chaintracks/v2/header/height/{height}', response: :json
|
|
27
30
|
endpoint :current_height, :get, '/chaintracks/v2/tip',
|
|
28
31
|
response: ->(body) { JSON.parse(body)['height'] }
|
|
29
32
|
|
|
30
33
|
# @param base_url [String] base URL for the Chaintracks API
|
|
31
|
-
# @param api_key [String, nil]
|
|
34
|
+
# @param api_key [String, nil] legacy Bearer API key shorthand — use +auth:+ for new code
|
|
35
|
+
# @param auth [Hash, Symbol, nil] auth config; takes precedence over +api_key:+
|
|
32
36
|
# @param http_client [Object, nil] injectable HTTP client for testing
|
|
33
|
-
def initialize(base_url:, api_key: nil, http_client: nil)
|
|
37
|
+
def initialize(base_url:, api_key: nil, auth: nil, http_client: nil)
|
|
34
38
|
super
|
|
35
39
|
end
|
|
36
40
|
end
|