bsv-sdk 0.24.0 → 0.25.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac0ee4456475effef9fa92627f77e4b973cfe834efda2f936725074c35060f95
4
- data.tar.gz: acd91340c476db84531efd41003ea068e3b5029cbb63e74bab782d17b48694d5
3
+ metadata.gz: 9a0dbc7f524004e1db715058dd17d336e62f99687239ec4c7807512d730e6ef5
4
+ data.tar.gz: 45b1fc354cbfe718a38e01c0d4896d537600d188bba909f351da1bc17bac66c6
5
5
  SHA512:
6
- metadata.gz: 02fffbfda10161282e87c26d8be157377a3b3861830b2891fe1331a3ea4b51e2743bc707fd01c8f3b1a5921173037d70322259c3d8dda39ea7caeffd2ee3cfd5
7
- data.tar.gz: 0b4d5dea8e5b447d98603150a8873f14f8e24b7b75377fa5baa43bf497fedb62c11865ccc96eb15f78c49323987b896290955b483169dec065a307f468f3e584
6
+ metadata.gz: 6e212bf3c077ab32a6d5187c0e3cbd93fd61f26844d40dce31b28c0574a1111db3a081dd0d843be90dc682779ede74ece03f0f7eccc0eec930a372306cdc3ca2
7
+ data.tar.gz: ec1e1b2e0c7b5ae142b52cb1863000fd262d0c1dc30346c2a7d412af2e1f66cbc3057c864bc9d6229737be59c30eae2eaf6d7fee7c296a9393c6e0f8b636b558
data/CHANGELOG.md CHANGED
@@ -5,6 +5,34 @@ 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.25.0 — 2026-06-18
9
+
10
+ ### Added
11
+ - `BSV::Storage::Utils` — UHRP URL helpers (`normalise_url`, `url_for_hash`,
12
+ `url_for_file`, `hash_from_url`, `valid_url?`) (#817).
13
+ - `BSV::Overlay::Historian` — generic output-history walker over `LookupResolver`,
14
+ enabling protocol-specific replay of an outpoint's lineage (#818).
15
+ - `BSV::Registry` — typed `resolve_basket`, `resolve_protocol`, and
16
+ `resolve_certificate` methods on the registry client (#819).
17
+ - `BSV::KVStore::Interpreter` — `Historian`-compatible interpreter for KV
18
+ protocol outputs (#820).
19
+ - `BSV::Storage::Downloader` — UHRP resolver and content fetcher with
20
+ hash verification (#821).
21
+ - `BSV::KVStore::Global` — overlay-backed read-only key-value reader (#822).
22
+ - `BSV::Script::BIP276` — text encoding for scripts and templates (`encode`,
23
+ `decode`) (#830).
24
+ - `BSV::Transaction::BeefParty` — `Beef` subclass tracking per-party knowledge
25
+ of transactions for multi-party BEEF exchange, plus supporting methods on
26
+ `Beef` (#831).
27
+
28
+ ### Changed
29
+ - `Beef` wire-inputs debug log now uses the existing `TransactionInput#dtxid_hex`
30
+ and `Tx#dtxid` accessors instead of inlined byte-order conversions (#828).
31
+
32
+ ### Fixed
33
+ - ECIES bitcore variant — added `iv:` kwarg and TypeScript SDK conformance
34
+ vector (#829).
35
+
8
36
  ## 0.24.0 — 2026-06-11
9
37
 
10
38
  ### Breaking Changes
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module KVStore
5
+ # An immutable KVStore entry returned from {Global#get}.
6
+ #
7
+ # +token+ is populated only when +include_token: true+ is passed to +#get+.
8
+ # +history+ is populated only when +history: true+ is passed to +#get+.
9
+ Entry = Data.define(:key, :value, :controller, :protocol_id, :tags, :token, :history) do
10
+ def initialize(key:, value:, controller:, protocol_id:, tags:, token: nil, history: nil)
11
+ super
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module BSV
6
+ module KVStore
7
+ # Read-only client for the global KVStore overlay service.
8
+ #
9
+ # Queries the +ls_kvstore+ lookup service via a {BSV::Overlay::LookupResolver},
10
+ # decodes PushDrop tokens (5-field legacy and 6-field current formats), verifies
11
+ # the token signature, and returns typed {Entry} objects.
12
+ #
13
+ # Set/remove operations require wallet writes and are out of scope — those live
14
+ # in the +bsv-wallet+ gem.
15
+ #
16
+ # == Selector requirement
17
+ #
18
+ # {#get} raises +ArgumentError+ unless at least one of +key+, +controller+,
19
+ # +protocol_id+, or a non-empty +tags+ array is supplied in the query.
20
+ #
21
+ # == Always returns Array
22
+ #
23
+ # {#get} always returns +Array<Entry>+, even for single-result selectors such
24
+ # as +key + controller+. Callers that expect at most one result should use
25
+ # +.first+.
26
+ #
27
+ # == Silent skipping
28
+ #
29
+ # Outputs that fail BEEF parsing, PushDrop decoding, field-count validation, or
30
+ # signature verification are silently skipped (matching the TS SDK contract).
31
+ class Global
32
+ # @param network_preset [Symbol] :mainnet, :testnet, or :local
33
+ # @param lookup_resolver [BSV::Overlay::LookupResolver, nil] injectable resolver
34
+ # @param service_name [String] lookup service identifier
35
+ # @param proto_wallet [BSV::Wallet::ProtoWallet, nil] injectable wallet used for
36
+ # signature verification; defaults to +ProtoWallet.new('anyone')+. Tests can
37
+ # substitute a double to avoid +allow_any_instance_of+.
38
+ def initialize(network_preset: :mainnet, lookup_resolver: nil, service_name: 'ls_kvstore', proto_wallet: nil)
39
+ @lookup_resolver = lookup_resolver || BSV::Overlay::LookupResolver.new(network_preset: network_preset)
40
+ @service_name = service_name
41
+ @proto_wallet = proto_wallet || BSV::Wallet::ProtoWallet.new('anyone')
42
+ end
43
+
44
+ # Query the overlay for KVStore entries.
45
+ #
46
+ # @param query [Hash] selector: +:key+, +:controller+, +:protocol_id+, +:tags+,
47
+ # +:tag_query_mode+. At least one of the first four must be present.
48
+ # @param include_token [Boolean] populate {Token} on each entry when true
49
+ # @param history [Boolean] populate history via {BSV::Overlay::Historian} when true
50
+ # @return [Array<Entry>] matching entries (empty array when nothing matches)
51
+ # @raise [ArgumentError] if no selector is provided
52
+ def get(query, include_token: false, history: false)
53
+ validate_selector!(query)
54
+
55
+ question = BSV::Overlay::LookupQuestion.new(
56
+ service: @service_name,
57
+ query: camelise(query)
58
+ )
59
+
60
+ answer = @lookup_resolver.query(question)
61
+ return [] unless answer.type == 'output-list'
62
+
63
+ answer.outputs.filter_map do |output|
64
+ build_entry(output, include_token: include_token, history: history)
65
+ rescue StandardError
66
+ nil
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ # ---- Selector validation ----
73
+
74
+ def validate_selector!(query)
75
+ raise ArgumentError, 'query must be a Hash' unless query.is_a?(Hash)
76
+
77
+ has_key = query[:key].is_a?(String) && !query[:key].empty?
78
+ has_controller = query[:controller].is_a?(String) && !query[:controller].empty?
79
+ has_protocol = query[:protocol_id].is_a?(Array) && query[:protocol_id].length == 2
80
+ has_tags = query[:tags].is_a?(Array) && !query[:tags].empty?
81
+
82
+ return if has_key || has_controller || has_protocol || has_tags
83
+
84
+ raise ArgumentError,
85
+ 'At least one selector required (key, controller, protocol_id, or non-empty tags)'
86
+ end
87
+
88
+ # ---- Entry construction ----
89
+
90
+ def build_entry(output, include_token:, history:)
91
+ beef_data = output['beef'] || output[:beef]
92
+ output_index = (output['outputIndex'] || output[:output_index] || 0).to_i
93
+ raise 'Negative outputIndex' if output_index.negative?
94
+
95
+ beef = parse_beef!(beef_data)
96
+ beef_tx = beef.transactions.last
97
+ raise 'No transaction in BEEF' if beef_tx.nil? || beef_tx.is_a?(BSV::Transaction::Beef::TxidOnlyEntry)
98
+
99
+ tx = beef_tx.transaction
100
+ txout = tx.outputs[output_index]
101
+ raise 'outputIndex out of range' if txout.nil?
102
+
103
+ locking_script = txout.locking_script
104
+ raise 'No locking script' unless locking_script&.pushdrop?
105
+
106
+ raw_fields = locking_script.pushdrop_fields
107
+ raise 'Not a PushDrop script' unless raw_fields
108
+
109
+ field_count = raw_fields.length
110
+ raise "Unexpected field count: #{field_count}" unless [5, 6].include?(field_count)
111
+
112
+ # Separate data fields from trailing signature field
113
+ data_fields = raw_fields[0...-1]
114
+ sig_bytes = raw_fields.last
115
+
116
+ # Decode common fields (indices match kvProtocol in TS SDK)
117
+ protocol_id = decode_protocol_id!(data_fields[0])
118
+ key = decode_utf8!(data_fields[1])
119
+ value = decode_utf8!(data_fields[2])
120
+ controller = data_fields[3].unpack1('H*')
121
+
122
+ # New format (6 fields) has tags at index 4; old format (5 fields) has no tags field
123
+ tags = if field_count == 6
124
+ begin
125
+ JSON.parse(decode_utf8!(data_fields[4]))
126
+ rescue StandardError
127
+ nil
128
+ end
129
+ end
130
+
131
+ verify_signature!(data_fields, sig_bytes, protocol_id, key, controller)
132
+
133
+ token = if include_token
134
+ Token.new(
135
+ dtxid: tx.dtxid_hex,
136
+ output_index: output_index,
137
+ beef: beef,
138
+ satoshis: txout.satoshis || 0
139
+ )
140
+ end
141
+
142
+ entry_history = if history
143
+ BSV::Overlay::Historian
144
+ .new(BSV::KVStore::Interpreter)
145
+ .build_history(tx, { key: key, protocol_id: protocol_id })
146
+ end
147
+
148
+ Entry.new(
149
+ key: key,
150
+ value: value,
151
+ controller: controller,
152
+ protocol_id: protocol_id,
153
+ tags: tags,
154
+ token: token,
155
+ history: entry_history
156
+ )
157
+ end
158
+
159
+ # ---- Field decoding helpers ----
160
+
161
+ def parse_beef!(beef_data)
162
+ case beef_data
163
+ when String
164
+ BSV::Transaction::Beef.from_binary(beef_data)
165
+ when Array
166
+ BSV::Transaction::Beef.from_binary(beef_data.pack('C*'))
167
+ else
168
+ raise 'Missing or unsupported BEEF data'
169
+ end
170
+ end
171
+
172
+ def decode_utf8!(bytes)
173
+ raise 'Nil field' if bytes.nil?
174
+
175
+ str = bytes.force_encoding(Encoding::UTF_8)
176
+ raise 'Invalid UTF-8' unless str.valid_encoding?
177
+
178
+ str
179
+ end
180
+
181
+ def decode_protocol_id!(bytes)
182
+ JSON.parse(decode_utf8!(bytes))
183
+ end
184
+
185
+ # ---- Signature verification ----
186
+
187
+ def verify_signature!(data_fields, sig_bytes, protocol_id, key, controller)
188
+ @proto_wallet.verify_signature(
189
+ data: data_fields.join.bytes,
190
+ signature: sig_bytes.bytes,
191
+ protocol_id: protocol_id,
192
+ key_id: key,
193
+ counterparty: controller
194
+ )
195
+ end
196
+
197
+ # ---- Query key camelisation ----
198
+
199
+ def camelise(query)
200
+ query.transform_keys do |k|
201
+ case k
202
+ when :protocol_id then :protocolID
203
+ when :tag_query_mode then :tagQueryMode
204
+ else k
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module BSV
6
+ module KVStore
7
+ # Historian interpreter for KVStore PushDrop tokens.
8
+ #
9
+ # Decodes a PushDrop locking script from the specified output, validates it
10
+ # has the expected field count (5 old format / 6 new format), and filters by
11
+ # +ctx[:key]+ and +ctx[:protocol_id]+.
12
+ #
13
+ # Returns the value as a UTF-8 string, or +nil+ for any non-match or error.
14
+ # Never raises.
15
+ #
16
+ # Field layout (old format — 5 fields):
17
+ # [0] protocolID (JSON-encoded array)
18
+ # [1] key
19
+ # [2] value
20
+ # [3] controller
21
+ # [4] signature
22
+ #
23
+ # Field layout (new format — 6 fields):
24
+ # [0] protocolID (JSON-encoded array)
25
+ # [1] key
26
+ # [2] value
27
+ # [3] controller
28
+ # [4] tags
29
+ # [5] signature
30
+ module Interpreter
31
+ PROTOCOL_ID = 0
32
+ KEY = 1
33
+ VALUE = 2
34
+ CONTROLLER = 3
35
+ TAGS_NEW = 4
36
+ SIGNATURE_OLD = 4
37
+ SIGNATURE_NEW = 5
38
+
39
+ KV_PROTOCOL_FIELDS_OLD = 5
40
+ KV_PROTOCOL_FIELDS_NEW = 6
41
+
42
+ # Decode the KVStore token at +output_index+ in +tx+.
43
+ #
44
+ # Implements the Historian interpreter contract:
45
+ # +interpreter.call(tx, output_index, ctx)+ → String or nil.
46
+ #
47
+ # @param tx [Transaction::Tx, nil] the transaction to inspect
48
+ # @param output_index [Integer] index into +tx.outputs+
49
+ # @param ctx [Hash, nil] must contain +:key+ (String) and +:protocol_id+ (Array)
50
+ # @return [String, nil] the decoded UTF-8 value, or nil on any mismatch/error
51
+ def self.call(tx, output_index, ctx)
52
+ return nil if tx.nil?
53
+ return nil if ctx.nil? || ctx[:key].nil?
54
+
55
+ output = tx.outputs[output_index]
56
+ return nil if output.nil?
57
+
58
+ locking_script = output.locking_script
59
+ return nil if locking_script.nil?
60
+
61
+ return nil unless locking_script.pushdrop?
62
+
63
+ fields = locking_script.pushdrop_fields
64
+ return nil unless fields
65
+
66
+ field_count = fields.length
67
+ return nil unless [KV_PROTOCOL_FIELDS_OLD, KV_PROTOCOL_FIELDS_NEW].include?(field_count)
68
+
69
+ protocol_id_bytes = fields[PROTOCOL_ID]
70
+ return nil if protocol_id_bytes.nil?
71
+
72
+ begin
73
+ protocol_id_str = protocol_id_bytes.force_encoding(Encoding::UTF_8)
74
+ return nil unless protocol_id_str.valid_encoding?
75
+
76
+ parsed_protocol_id = JSON.parse(protocol_id_str)
77
+ return nil unless parsed_protocol_id == ctx[:protocol_id]
78
+ rescue JSON::ParserError
79
+ return nil
80
+ end
81
+
82
+ key_bytes = fields[KEY]
83
+ return nil if key_bytes.nil?
84
+
85
+ begin
86
+ key_str = key_bytes.force_encoding(Encoding::UTF_8)
87
+ return nil unless key_str.valid_encoding?
88
+ return nil unless key_str == ctx[:key]
89
+ rescue StandardError
90
+ return nil
91
+ end
92
+
93
+ begin
94
+ value_bytes = fields[VALUE]
95
+ return nil if value_bytes.nil?
96
+
97
+ value_str = value_bytes.force_encoding(Encoding::UTF_8)
98
+ return nil unless value_str.valid_encoding?
99
+
100
+ value_str
101
+ rescue StandardError
102
+ nil
103
+ end
104
+ rescue StandardError
105
+ nil
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module KVStore
5
+ # Transaction output reference for a KVStore token.
6
+ #
7
+ # Populated when +include_token: true+ is passed to {Global#get}.
8
+ Token = Data.define(:dtxid, :output_index, :beef, :satoshis)
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module KVStore
5
+ autoload :Interpreter, 'bsv/kv_store/interpreter'
6
+ autoload :Entry, 'bsv/kv_store/entry'
7
+ autoload :Token, 'bsv/kv_store/token'
8
+ autoload :Global, 'bsv/kv_store/global'
9
+ end
10
+ end
@@ -21,7 +21,7 @@ module BSV
21
21
 
22
22
  # Convert a Transaction to a hash suitable for JSON responses.
23
23
  #
24
- # @param tx [BSV::Transaction::Tx]
24
+ # @param tx [Transaction::Tx]
25
25
  # @return [Hash]
26
26
  def self.transaction_to_h(tx)
27
27
  {
@@ -33,7 +33,7 @@ module BSV
33
33
  }
34
34
  end
35
35
 
36
- # Convert a TransactionInput to a hash.
36
+ # Convert a Transaction::TransactionInput to a hash.
37
37
  # @api private
38
38
  def self.input_to_h(input, _index)
39
39
  unlock_script = input.unlocking_script
@@ -46,7 +46,7 @@ module BSV
46
46
  }
47
47
  end
48
48
 
49
- # Convert a TransactionOutput to a hash.
49
+ # Convert a Transaction::TransactionOutput to a hash.
50
50
  # @api private
51
51
  def self.output_to_h(output, index)
52
52
  script = output.locking_script
@@ -80,7 +80,7 @@ module BSV
80
80
  # Computes the BIP-143 sighash (SIGHASH_ALL|FORK_ID) and signs it
81
81
  # using the wallet's derived key for the protocol.
82
82
  #
83
- # @param tx [BSV::Transaction::Tx] the spending transaction
83
+ # @param tx [Transaction::Tx] the spending transaction
84
84
  # @param input_index [Integer] which input to sign
85
85
  # @return [BSV::Script::Script] the unlocking script
86
86
  def sign(tx, input_index)
@@ -101,7 +101,7 @@ module BSV
101
101
 
102
102
  # Estimated byte length of the unlocking script.
103
103
  #
104
- # @param _tx [BSV::Transaction::Tx] unused
104
+ # @param _tx [Transaction::Tx] unused
105
105
  # @param _input_index [Integer] unused
106
106
  # @return [Integer]
107
107
  def estimated_length(_tx, _input_index)
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Overlay
5
+ # Builds a chronological history (oldest → newest) of typed values by traversing
6
+ # a transaction's input ancestry and interpreting each output with a provided interpreter.
7
+ #
8
+ # The interpreter contract is: +interpreter.call(tx, output_index, ctx)+, returning the
9
+ # typed value or +nil+. Any callable responding to +:call+ is accepted (lambda, proc,
10
+ # method, or object).
11
+ #
12
+ # Traversal follows +input.source_transaction+ references recursively. Callers must
13
+ # supply transactions whose inputs have +source_transaction+ populated.
14
+ #
15
+ # == Cycle safety
16
+ #
17
+ # Each transaction is visited at most once, tracked by its +wtxid+ (wire-order binary).
18
+ #
19
+ # == Value semantics
20
+ #
21
+ # Only +nil+ is excluded. Falsy non-nil values (+false+, +""+, +0+) are valid history
22
+ # entries and are included in the result.
23
+ #
24
+ # == Caching
25
+ #
26
+ # An optional +history_cache+ (any +[]/[]=+ responder) caches complete history results.
27
+ # Cache keys have the form: <tt>"#{interpreter_version}|#{dtxid_hex}|#{ctx_key}"</tt>.
28
+ # Cached arrays are stored frozen; each retrieval returns a +dup+ to protect the cache.
29
+ #
30
+ # == Note
31
+ #
32
+ # The Ruby Historian is synchronous. There is no async/await semantics.
33
+ class Historian
34
+ # @param interpreter [#call] callable: +call(tx, output_index, ctx) → value|nil+
35
+ # @param debug [Boolean] enable debug logging via +BSV.logger+
36
+ # @param history_cache [Hash, nil] optional cache store (+[]/[]=+ responder)
37
+ # @param interpreter_version [String] version tag for cache key invalidation
38
+ # @param ctx_key_fn [#call, nil] serialises context to a string cache key
39
+ def initialize(interpreter, debug: false, history_cache: nil, interpreter_version: 'v1', ctx_key_fn: nil)
40
+ @interpreter = interpreter
41
+ @debug = debug
42
+ @history_cache = history_cache
43
+ @interpreter_version = interpreter_version
44
+ @ctx_key_fn = ctx_key_fn
45
+ end
46
+
47
+ # Traverses input ancestry from +start_transaction+ and returns all interpreted
48
+ # values in chronological order (oldest first).
49
+ #
50
+ # @param start_transaction [Transaction::Tx]
51
+ # @param context [Object, nil] forwarded verbatim to the interpreter
52
+ # @return [Array] interpreted values, oldest first
53
+ def build_history(start_transaction, context = nil)
54
+ if @history_cache
55
+ key = cache_key(start_transaction, context)
56
+ cached = @history_cache[key]
57
+ unless cached.nil?
58
+ BSV.logger&.debug { "[Historian] History cache hit: #{key}" } if @debug
59
+ return cached.dup
60
+ end
61
+ end
62
+
63
+ history = []
64
+ visited = {}
65
+
66
+ traverse(start_transaction, context, history, visited)
67
+
68
+ result = history.reverse
69
+
70
+ if @history_cache
71
+ key ||= cache_key(start_transaction, context)
72
+ @history_cache[key] = result.dup.freeze
73
+ BSV.logger&.debug { "[Historian] History cached: #{key}" } if @debug
74
+ end
75
+
76
+ result
77
+ end
78
+
79
+ private
80
+
81
+ def cache_key(tx, context)
82
+ ctx_key = @ctx_key_fn ? @ctx_key_fn.call(context) : context.to_s
83
+ "#{@interpreter_version}|#{tx.dtxid_hex}|#{ctx_key}"
84
+ end
85
+
86
+ def traverse(tx, context, history, visited)
87
+ id = tx.wtxid
88
+
89
+ if visited.key?(id)
90
+ BSV.logger&.debug { "[Historian] Skipping already visited transaction: #{tx.dtxid_hex}" } if @debug
91
+ return
92
+ end
93
+
94
+ visited[id] = true
95
+
96
+ BSV.logger&.debug { "[Historian] Processing transaction: #{tx.dtxid_hex}" } if @debug
97
+
98
+ tx.outputs.each_with_index do |_output, index|
99
+ value = @interpreter.call(tx, index, context)
100
+ unless value.nil?
101
+ history << value
102
+ BSV.logger&.debug { "[Historian] Added value to history: #{value.inspect}" } if @debug
103
+ end
104
+ rescue StandardError => e
105
+ BSV.logger&.debug { "[Historian] Failed to interpret output #{index}: #{e.message}" } if @debug
106
+ end
107
+
108
+ tx.inputs.each do |input|
109
+ if input.source_transaction
110
+ traverse(input.source_transaction, context, history, visited)
111
+ elsif @debug
112
+ BSV.logger&.debug { '[Historian] Input missing sourceTransaction, skipping' }
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -74,7 +74,7 @@ module BSV
74
74
 
75
75
  # Broadcast a transaction to all interested overlay hosts.
76
76
  #
77
- # @param tx [BSV::Transaction::Tx] the transaction to broadcast
77
+ # @param tx [Transaction::Tx] the transaction to broadcast
78
78
  # @return [OverlayBroadcastResult]
79
79
  def broadcast(tx)
80
80
  beef = serialise_beef(tx)
data/lib/bsv/overlay.rb CHANGED
@@ -25,5 +25,6 @@ module BSV
25
25
  autoload :HTTPSBroadcastFacilitator, 'bsv/overlay/broadcast_facilitator'
26
26
  autoload :TopicBroadcaster, 'bsv/overlay/topic_broadcaster'
27
27
  autoload :SHIPBroadcaster, 'bsv/overlay/topic_broadcaster'
28
+ autoload :Historian, 'bsv/overlay/historian'
28
29
  end
29
30
  end
@@ -147,15 +147,24 @@ module BSV
147
147
  # @param message [String] the plaintext message
148
148
  # @param public_key [PublicKey] the recipient's public key
149
149
  # @param private_key [PrivateKey, nil] optional ephemeral key (random if omitted)
150
+ # @param iv [String, nil] optional 16-byte ASCII-8BIT IV. When omitted a random IV is
151
+ # generated via +SecureRandom+. Supply a fixed value only for deterministic test
152
+ # vectors — **never use a fixed IV in production**.
150
153
  # @return [String] encrypted payload
151
- def bitcore_encrypt(message, public_key, private_key: nil)
154
+ # @raise [ArgumentError] if +iv+ is supplied but is not exactly 16 bytes
155
+ def bitcore_encrypt(message, public_key, private_key: nil, iv: nil)
152
156
  message = message.b if message.encoding != Encoding::ASCII_8BIT
153
157
 
158
+ if iv
159
+ iv = iv.b if iv.encoding != Encoding::ASCII_8BIT
160
+ raise ArgumentError, 'iv must be exactly 16 bytes' unless iv.bytesize == 16
161
+ else
162
+ iv = SecureRandom.random_bytes(16)
163
+ end
164
+
154
165
  ephemeral = private_key || PrivateKey.generate
155
166
  key_e, key_m = derive_bitcore_keys(ephemeral, public_key)
156
167
 
157
- iv = SecureRandom.random_bytes(16)
158
-
159
168
  cipher = OpenSSL::Cipher.new('aes-256-cbc')
160
169
  cipher.encrypt
161
170
  cipher.key = key_e
@@ -112,6 +112,48 @@ module BSV
112
112
  end
113
113
  end
114
114
 
115
+ # Resolves basket registry definitions.
116
+ #
117
+ # Thin wrapper around {#resolve} for cross-SDK parity with the Go SDK's
118
+ # +ResolveBasket+ method.
119
+ #
120
+ # @param query [Hash] optional filter criteria:
121
+ # - +:basket_id+ [String] exact basket identifier
122
+ # - +:name+ [String] human-readable basket name
123
+ # - +:registry_operators+ [Array<String>] operator public key hexes
124
+ # @return [Array<RegisteredDefinition>] matching registered basket definitions
125
+ def resolve_basket(query = {})
126
+ resolve(DefinitionType::BASKET, query)
127
+ end
128
+
129
+ # Resolves protocol registry definitions.
130
+ #
131
+ # Thin wrapper around {#resolve} for cross-SDK parity with the Go SDK's
132
+ # +ResolveProtocol+ method.
133
+ #
134
+ # @param query [Hash] optional filter criteria:
135
+ # - +:name+ [String] human-readable protocol name
136
+ # - +:protocol_id+ [Array] BRC-43 two-element protocol ID, e.g. +[1, 'protomap']+
137
+ # - +:registry_operators+ [Array<String>] operator public key hexes
138
+ # @return [Array<RegisteredDefinition>] matching registered protocol definitions
139
+ def resolve_protocol(query = {})
140
+ resolve(DefinitionType::PROTOCOL, query)
141
+ end
142
+
143
+ # Resolves certificate type registry definitions.
144
+ #
145
+ # Thin wrapper around {#resolve} for cross-SDK parity with the Go SDK's
146
+ # +ResolveCertificate+ method.
147
+ #
148
+ # @param query [Hash] optional filter criteria:
149
+ # - +:type+ [String] Base64-encoded certificate type identifier
150
+ # - +:name+ [String] human-readable certificate type name
151
+ # - +:registry_operators+ [Array<String>] operator public key hexes
152
+ # @return [Array<RegisteredDefinition>] matching registered certificate type definitions
153
+ def resolve_certificate(query = {})
154
+ resolve(DefinitionType::CERTIFICATE, query)
155
+ end
156
+
115
157
  # Lists the registry operator's own published definitions for the given type.
116
158
  #
117
159
  # Queries the wallet for spendable outputs in the appropriate basket,
@@ -430,7 +472,7 @@ module BSV
430
472
  #
431
473
  # @param definition_type [String]
432
474
  # @param output [Hash] wallet output with :outpoint, :satoshis keys
433
- # @param beef [BSV::Transaction::Beef] parsed BEEF
475
+ # @param beef [Transaction::Beef] parsed BEEF
434
476
  # @param beef_raw [String] raw BEEF bytes
435
477
  # @return [RegisteredDefinition, nil]
436
478
  def parse_own_output_to_registered_definition(definition_type, output, beef, beef_raw)