bsv-wallet 0.7.0 → 0.9.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: 233e4c969fea0e8bb52110ffffe9d034a953854ccb1098c0a3dd3eb76bb2c034
4
- data.tar.gz: e11ab6c70e949cbe62593f620d0367225e80ac84a144f94b07418d02e9383466
3
+ metadata.gz: 7959ebc47fc5d89d3d0db18f4136c925e228f6ea483553decffd4b9e7de9defc
4
+ data.tar.gz: 2365a17193357b64d50c15a61aa12422b966b8f0155a6ab989b3bacb700a0929
5
5
  SHA512:
6
- metadata.gz: 974ffa3042e2bb4fe3c9f7ec94d8503c55ed20e94d5a808a667700339fbb5151eec6b12c185238353ccfd40e62f197477081fdb7b6b9f9ce75da4598a4536b90
7
- data.tar.gz: 0af0b5d238c57a1b640ea243b2bce0e5e33fd55932354689a62b1c05dfe3b41d776fdfcd5fe245fa3973de98632d6f0985bbfc4bed99b4d2527494dc16fc2f6f
6
+ metadata.gz: 895d3c7a661e00b7d70dd833d12e4dd0e17ca2a8d0d8ba6a2c0ef622dfbac6bc0d6fb534a3fe06c14c0b1747981f1e0c9b8b61e9f164e3cc8fea222e5f9e70c1
7
+ data.tar.gz: 3086ff6e236c57d903c947ac7c230148104993837b3d2f46ddef3a09a9b43da42bab1a5ea6f1c7b375b105cc7e8feffe2c8defbd83a325ec7ca0775dbd1f2ea1
data/CHANGELOG.md CHANGED
@@ -5,6 +5,69 @@ All notable changes to the `bsv-wallet` 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.9.0 — 2026-04-16
9
+
10
+ ### Changed — **Breaking**
11
+
12
+ #### Action status taxonomy aligned with BRC-100 reference SDK (HLR #455)
13
+
14
+ This release realigns the action status values with the wallet-toolbox reference
15
+ implementation. The meaning of `'completed'` has changed — **consumers must
16
+ update any code that checks for `'completed'` as the post-broadcast success
17
+ state**.
18
+
19
+ | Status | Old meaning | New meaning |
20
+ |--------|-------------|-------------|
21
+ | `'nosend'` | No change | Transaction built but not broadcast (`no_send: true`) |
22
+ | `'sending'` | No change | Async queue accepted; worker has not yet attempted broadcast |
23
+ | `'unproven'` | *(new)* | Broadcast succeeded; awaiting merkle proof |
24
+ | `'completed'` | Broadcast succeeded | **Merkle proof received and stored** |
25
+ | `'failed'` | No change | Broadcast rejected or transaction invalid |
26
+
27
+ **Migration:**
28
+
29
+ - Code querying `list_actions(status: 'completed')` will return fewer results
30
+ until a proof-watcher is implemented (out of scope for this release). To find
31
+ successfully-broadcast actions that have not yet received a proof, query
32
+ `status: 'unproven'` instead.
33
+ - `create_action` and `sign_action` now raise `BSV::Wallet::WalletError` when
34
+ no broadcaster is configured and `options: { no_send: true }` is not set.
35
+ Previously these calls succeeded silently. To resolve: pass
36
+ `broadcaster: BSV::Network::ARC.default` to `WalletClient.new`, or pass
37
+ `options: { no_send: true }` to `create_action` to build without
38
+ broadcasting.
39
+ - `internalize_action` now sets status to `'completed'` only when the supplied
40
+ BEEF contains a merkle proof for the subject transaction. Plain BEEF (raw
41
+ transaction, no BUMP) results in status `'unproven'`.
42
+ - Wallets configured with a `SolidQueueAdapter` satisfy the broadcaster
43
+ requirement if the adapter carries an embedded `broadcaster:` — the
44
+ `WalletClient` itself does not need one.
45
+
46
+ **Related upstream incidents:** x402-rack #148, x402-rack #158, x402-doom #196.
47
+ **Tracking issue:** #455.
48
+
49
+ ## 0.8.0 — 2026-04-15
50
+
51
+ ### Added
52
+ - BRC-100 substrates: `HTTPWalletJSON` for JSON-over-HTTP, `HTTPWalletWire`
53
+ for binary transport, and `WalletWireTransceiver` Interface adapter (#449–#451)
54
+ - `WalletClient` accepts `substrate:` constructor param for remote wallet
55
+ delegation — all Interface methods delegate to the substrate when set (#452)
56
+ - `list_actions` and `list_outputs` honour `include_labels`, `include_inputs`,
57
+ and `include_outputs` flags (#448)
58
+ - `acquire_certificate` uses `AuthFetch` for BRC-104 authenticated certificate
59
+ issuance (#453)
60
+
61
+ ### Fixed
62
+ - `prove_certificate` now uses correct protocol ID (`'certificate field encryption'`)
63
+ and key ID format (`"#{serial_number} #{field_name}"`) matching TS/Go SDKs —
64
+ previously incompatible cross-SDK (#424)
65
+ - Code review findings addressed for substrates and include flags
66
+
67
+ ### Changed
68
+ - `BSV::WalletInterface` module removed — `VERSION` now lives in `BSV::Wallet::VERSION`
69
+ where all other wallet constants already reside
70
+
8
71
  ## 0.7.0 — 2026-04-12
9
72
 
10
73
  ### Added
@@ -68,6 +68,21 @@ module BSV
68
68
  false
69
69
  end
70
70
 
71
+ # Returns +false+ by default — adapters without a broadcaster cannot
72
+ # broadcast on-chain.
73
+ #
74
+ # Override in adapters that hold a broadcaster reference so that
75
+ # +WalletClient+ can determine broadcast availability from the queue
76
+ # alone. This is the correct delegation point because users may pass a
77
+ # broadcaster-equipped queue (e.g.
78
+ # +SolidQueueAdapter.new(broadcaster: arc)+) without also passing
79
+ # +broadcaster:+ directly to +WalletClient+.
80
+ #
81
+ # @return [Boolean]
82
+ def broadcast_enabled?
83
+ false
84
+ end
85
+
71
86
  # Returns the broadcast status for a previously enqueued transaction.
72
87
  #
73
88
  # @param _txid [String] hex transaction identifier
@@ -30,6 +30,17 @@ module BSV
30
30
  false
31
31
  end
32
32
 
33
+ # Returns +true+ when a broadcaster has been configured.
34
+ #
35
+ # +WalletClient+ delegates its own +broadcast_enabled?+ to this method
36
+ # so the check works correctly when the broadcaster is embedded in the
37
+ # queue rather than passed directly to the wallet.
38
+ #
39
+ # @return [Boolean]
40
+ def broadcast_enabled?
41
+ !@broadcaster.nil?
42
+ end
43
+
33
44
  # Returns the broadcast status for a previously enqueued transaction.
34
45
  #
35
46
  # Delegates to storage and returns the action status field, or +nil+ if
@@ -96,8 +107,10 @@ module BSV
96
107
  }
97
108
  end
98
109
 
99
- # Broadcast succeeded — promote all pending state to final.
100
- promote(input_outpoints, change_outpoints, txid)
110
+ # Broadcast succeeded — promote all pending state; set status to
111
+ # 'unproven' (transaction is on-chain but lacks a merkle proof).
112
+ # 'completed' is reserved for transactions confirmed by a proof-watcher.
113
+ promote(input_outpoints, change_outpoints, txid, status: 'unproven')
101
114
 
102
115
  result = {
103
116
  txid: txid,
@@ -109,14 +122,24 @@ module BSV
109
122
  result
110
123
  end
111
124
 
112
- # Promotes UTXO state without broadcasting — backwards-compatible fallback
113
- # used when no broadcaster is configured.
125
+ # Promotes UTXO state without broadcasting.
114
126
  #
115
- # If +accept_delayed_broadcast+ is set the action status is +unproven+;
116
- # otherwise it is +completed+.
127
+ # This path is reached when no broadcaster is configured. It is only
128
+ # valid when +accept_delayed_broadcast+ is set on the create_action
129
+ # call — the caller explicitly accepts that the transaction will be
130
+ # broadcast out-of-band. Action status is set to +unproven+.
131
+ #
132
+ # +completed+ is reserved for transactions that have received a merkle
133
+ # proof (set by +internalize_action+ or a future proof-watcher).
134
+ #
135
+ # Defensive guard: raises +WalletError+ if reached without
136
+ # +accept_delayed_broadcast+. The normal entry point for this guard is
137
+ # the +create_action+ validation added in Task 1 (#456), but this guard
138
+ # protects against other code paths that bypass it.
117
139
  #
118
140
  # @param payload [Hash] broadcast payload
119
141
  # @return [Hash] result hash containing +:txid+ and +:tx+
142
+ # @raise [BSV::Wallet::WalletError] if +accept_delayed_broadcast+ is not set
120
143
  def promote_without_broadcast(payload)
121
144
  txid = payload[:txid]
122
145
  beef_binary = payload[:beef_binary]
@@ -124,8 +147,14 @@ module BSV
124
147
  change_outpoints = payload[:change_outpoints]
125
148
  delayed = payload[:accept_delayed_broadcast]
126
149
 
127
- final_status = delayed ? 'unproven' : 'completed'
128
- promote(input_outpoints, change_outpoints, txid, status: final_status)
150
+ unless delayed
151
+ raise BSV::Wallet::WalletError,
152
+ 'InlineQueue cannot promote without a broadcaster unless ' \
153
+ 'accept_delayed_broadcast is set. This indicates a bypass of ' \
154
+ 'the create_action guard — report as a bug.'
155
+ end
156
+
157
+ promote(input_outpoints, change_outpoints, txid, status: 'unproven')
129
158
 
130
159
  { txid: txid, tx: beef_binary.unpack('C*') }
131
160
  end
@@ -0,0 +1,268 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+
7
+ module BSV
8
+ module Wallet
9
+ module Substrates
10
+ # BRC-100 wallet substrate that delegates all Interface methods to a remote
11
+ # wallet server via JSON-over-HTTP (POST #{base_url}/#{camelCaseMethodName}).
12
+ #
13
+ # Key conversion is handled by {BSV::WireFormat}: Ruby snake_case symbol keys
14
+ # in args are converted to camelCase strings before the request, and the
15
+ # camelCase JSON response is converted back to snake_case symbol keys.
16
+ #
17
+ # @example
18
+ # wallet = BSV::Wallet::Substrates::HTTPWalletJSON.new('http://localhost:3321',
19
+ # originator: 'myapp.example.com')
20
+ # result = wallet.get_public_key({ identity_key: true })
21
+ # # => { public_key: '02abc...' }
22
+ class HTTPWalletJSON
23
+ include BSV::Wallet::Interface
24
+
25
+ # Maps the 28 BRC-100 Interface method symbols to their camelCase HTTP endpoint names.
26
+ # Derived from Wire::Serializer::CALL_CODES keys via BSV::WireFormat.snake_to_camel.
27
+ METHOD_NAMES = BSV::Wallet::Wire::Serializer::CALL_CODES.keys.to_h do |sym|
28
+ [sym, BSV::WireFormat.snake_to_camel(sym.to_s)]
29
+ end.freeze
30
+
31
+ # @param base_url [String] base URL of the remote wallet server (e.g. 'http://localhost:3321')
32
+ # @param originator [String, nil] FQDN of the originating application (sent as Origin/Originator headers)
33
+ # @param http_client [Object, nil] injectable HTTP client for testing; must respond to
34
+ # `start(uri, &block)` returning a Net::HTTP-compatible response
35
+ def initialize(base_url, originator: nil, http_client: nil)
36
+ @base_url = base_url
37
+ @originator = originator
38
+ @http_client = http_client
39
+ end
40
+
41
+ # Per-call originator is accepted for Interface conformance but not forwarded.
42
+ # The Origin header uses the constructor-level @originator for the lifetime of
43
+ # the connection — matching the TS SDK's HTTPWalletJSON, which also ignores per-call
44
+ # originator. WalletWireTransceiver supports per-call originator because the wire
45
+ # frame encodes it per-message; HTTP substrates identify by connection, not by call.
46
+ # rubocop:disable Lint/UnusedMethodArgument
47
+
48
+ def create_action(args, originator: nil)
49
+ call(METHOD_NAMES[:create_action], args)
50
+ end
51
+
52
+ def sign_action(args, originator: nil)
53
+ call(METHOD_NAMES[:sign_action], args)
54
+ end
55
+
56
+ def abort_action(args, originator: nil)
57
+ call(METHOD_NAMES[:abort_action], args)
58
+ end
59
+
60
+ def list_actions(args, originator: nil)
61
+ call(METHOD_NAMES[:list_actions], args)
62
+ end
63
+
64
+ def internalize_action(args, originator: nil)
65
+ call(METHOD_NAMES[:internalize_action], args)
66
+ end
67
+
68
+ def list_outputs(args, originator: nil)
69
+ call(METHOD_NAMES[:list_outputs], args)
70
+ end
71
+
72
+ def relinquish_output(args, originator: nil)
73
+ call(METHOD_NAMES[:relinquish_output], args)
74
+ end
75
+
76
+ def get_public_key(args, originator: nil)
77
+ call(METHOD_NAMES[:get_public_key], args)
78
+ end
79
+
80
+ def reveal_counterparty_key_linkage(args, originator: nil)
81
+ call(METHOD_NAMES[:reveal_counterparty_key_linkage], args)
82
+ end
83
+
84
+ def reveal_specific_key_linkage(args, originator: nil)
85
+ call(METHOD_NAMES[:reveal_specific_key_linkage], args)
86
+ end
87
+
88
+ def encrypt(args, originator: nil)
89
+ call(METHOD_NAMES[:encrypt], args)
90
+ end
91
+
92
+ def decrypt(args, originator: nil)
93
+ call(METHOD_NAMES[:decrypt], args)
94
+ end
95
+
96
+ def create_hmac(args, originator: nil)
97
+ call(METHOD_NAMES[:create_hmac], args)
98
+ end
99
+
100
+ def verify_hmac(args, originator: nil)
101
+ call(METHOD_NAMES[:verify_hmac], args)
102
+ end
103
+
104
+ def create_signature(args, originator: nil)
105
+ call(METHOD_NAMES[:create_signature], args)
106
+ end
107
+
108
+ def verify_signature(args, originator: nil)
109
+ call(METHOD_NAMES[:verify_signature], args)
110
+ end
111
+
112
+ def acquire_certificate(args, originator: nil)
113
+ call(METHOD_NAMES[:acquire_certificate], args)
114
+ end
115
+
116
+ def list_certificates(args, originator: nil)
117
+ call(METHOD_NAMES[:list_certificates], args)
118
+ end
119
+
120
+ def prove_certificate(args, originator: nil)
121
+ call(METHOD_NAMES[:prove_certificate], args)
122
+ end
123
+
124
+ def relinquish_certificate(args, originator: nil)
125
+ call(METHOD_NAMES[:relinquish_certificate], args)
126
+ end
127
+
128
+ def discover_by_identity_key(args, originator: nil)
129
+ call(METHOD_NAMES[:discover_by_identity_key], args)
130
+ end
131
+
132
+ def discover_by_attributes(args, originator: nil)
133
+ call(METHOD_NAMES[:discover_by_attributes], args)
134
+ end
135
+
136
+ def is_authenticated(args = {}, originator: nil)
137
+ call(METHOD_NAMES[:is_authenticated], args)
138
+ end
139
+
140
+ def wait_for_authentication(args = {}, originator: nil)
141
+ call(METHOD_NAMES[:wait_for_authentication], args)
142
+ end
143
+
144
+ def get_height(args = {}, originator: nil)
145
+ call(METHOD_NAMES[:get_height], args)
146
+ end
147
+
148
+ def get_header_for_height(args, originator: nil)
149
+ call(METHOD_NAMES[:get_header_for_height], args)
150
+ end
151
+
152
+ def get_network(args = {}, originator: nil)
153
+ call(METHOD_NAMES[:get_network], args)
154
+ end
155
+
156
+ def get_version(args = {}, originator: nil)
157
+ call(METHOD_NAMES[:get_version], args)
158
+ end
159
+
160
+ # rubocop:enable Lint/UnusedMethodArgument
161
+
162
+ private
163
+
164
+ # Posts args to the remote wallet endpoint for the given camelCase method name.
165
+ #
166
+ # Outbound: converts snake_case symbol keys to camelCase strings via WireFormat.to_wire.
167
+ # Inbound: converts camelCase string keys to snake_case symbols via WireFormat.from_wire.
168
+ # Errors: non-2xx response raises the appropriate WalletError subclass.
169
+ #
170
+ # @param method_name [String] camelCase endpoint name (e.g. 'getPublicKey')
171
+ # @param args [Hash] method arguments (snake_case symbol keys)
172
+ # @return [Hash] response with snake_case symbol keys
173
+ def call(method_name, args)
174
+ args ||= {}
175
+ wire_args = BSV::WireFormat.to_wire(args)
176
+ body = JSON.generate(wire_args)
177
+
178
+ uri = build_uri(method_name)
179
+ headers = build_headers
180
+
181
+ response = execute_request(uri, body, headers)
182
+
183
+ handle_response(response, method_name, args)
184
+ end
185
+
186
+ def build_uri(method_name)
187
+ URI.parse("#{@base_url}/#{method_name}")
188
+ end
189
+
190
+ def build_headers
191
+ h = {
192
+ 'Content-Type' => 'application/json',
193
+ 'Accept' => 'application/json'
194
+ }
195
+
196
+ if @originator
197
+ origin_value = to_origin_header(@originator)
198
+ h['Origin'] = origin_value
199
+ h['Originator'] = origin_value
200
+ end
201
+
202
+ h
203
+ end
204
+
205
+ # Converts an originator domain to a full Origin header value.
206
+ # Prepends 'http://' if no scheme is present (matching TS SDK toOriginHeader).
207
+ def to_origin_header(originator)
208
+ return originator if originator.match?(%r{\A[a-z][a-z0-9+.-]*://}i)
209
+
210
+ "http://#{originator}"
211
+ end
212
+
213
+ def execute_request(uri, body, headers)
214
+ if @http_client
215
+ @http_client.post(uri, body, headers)
216
+ else
217
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
218
+ request = Net::HTTP::Post.new(uri.request_uri, headers)
219
+ request.body = body
220
+ http.request(request)
221
+ end
222
+ end
223
+ end
224
+
225
+ def handle_response(response, method_name, args)
226
+ code = response.code.to_i
227
+
228
+ unless (200..299).cover?(code)
229
+ data = begin
230
+ parse_json_body(response.body)
231
+ rescue JSON::ParserError
232
+ nil
233
+ end
234
+ raise_error_response(code, data, method_name, args)
235
+ end
236
+
237
+ data = parse_json_body(response.body)
238
+ return {} if data.nil?
239
+
240
+ data.is_a?(Hash) ? BSV::WireFormat.from_wire(data) : data
241
+ end
242
+
243
+ def parse_json_body(body)
244
+ return nil if body.nil? || body.empty?
245
+
246
+ JSON.parse(body)
247
+ end
248
+
249
+ def raise_error_response(code, data, method_name, _args)
250
+ if code == 400 && data.is_a?(Hash) && data['isError']
251
+ case data['code']
252
+ when 5
253
+ raise BSV::Wallet::WalletError.new(data['message'] || 'Review actions required', 5)
254
+ when 6
255
+ raise BSV::Wallet::InvalidParameterError, data['parameter'] || 'unknown'
256
+ when 7
257
+ raise BSV::Wallet::InsufficientFundsError, data['message']
258
+ end
259
+ end
260
+
261
+ message = (data.is_a?(Hash) && data['message']) ||
262
+ "HTTP #{code} error calling #{method_name}"
263
+ raise BSV::Wallet::WalletError, message
264
+ end
265
+ end
266
+ end
267
+ end
268
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+
6
+ module BSV
7
+ module Wallet
8
+ module Substrates
9
+ # Binary wire transport that transmits BRC-100 wallet wire messages over HTTP.
10
+ #
11
+ # Implements the single-method WalletWire interface: given a raw binary frame
12
+ # (as an Array of byte integers), parses the call code, maps it to a URL path,
13
+ # and POSTs the payload to the remote wallet endpoint.
14
+ #
15
+ # @example
16
+ # wire = BSV::Wallet::Substrates::HTTPWalletWire.new('http://localhost:3301')
17
+ # response_bytes = wire.transmit_to_wallet(frame_bytes)
18
+ class HTTPWalletWire
19
+ # @param base_url [String] the base URL of the remote wallet (e.g. 'http://localhost:3301')
20
+ # @param originator [String, nil] FQDN of the calling application (sent as Origin header)
21
+ # @param http_client [#call, nil] injectable HTTP client for testing; nil uses Net::HTTP
22
+ def initialize(base_url, originator: nil, http_client: nil)
23
+ @base_url = base_url
24
+ @originator = originator
25
+ @http_client = http_client
26
+ end
27
+
28
+ # Transmits a binary wallet wire message to the remote wallet.
29
+ #
30
+ # Parses the call code from byte 0, reads the originator from the header,
31
+ # and POSTs the remaining payload bytes to the appropriate URL path.
32
+ #
33
+ # @param message [Array<Integer>] raw wire frame as array of byte integers
34
+ # @return [Array<Integer>] response body as array of byte integers
35
+ # @raise [ArgumentError] if the message is empty or contains an unknown call code
36
+ # @raise [RuntimeError] if the HTTP response indicates an error (non-2xx)
37
+ def transmit_to_wallet(message)
38
+ raise ArgumentError, 'message must not be empty' if message.nil? || message.empty?
39
+
40
+ call_code = message[0]
41
+ call_name = BSV::Wallet::Wire::Serializer::METHODS_BY_CODE[call_code]
42
+ raise ArgumentError, "unknown call code: #{call_code}" if call_name.nil?
43
+
44
+ originator_length = message[1] || 0
45
+ originator = message[2, originator_length].pack('C*').force_encoding('UTF-8') if originator_length.positive?
46
+
47
+ payload_start = 2 + originator_length
48
+ payload = message[payload_start..] || []
49
+
50
+ camel_name = BSV::WireFormat.snake_to_camel(call_name.to_s)
51
+ url = "#{@base_url}/#{camel_name}"
52
+
53
+ response_body = post_binary(url, payload, originator || @originator)
54
+ response_body.bytes.to_a
55
+ end
56
+
57
+ private
58
+
59
+ def post_binary(url, payload_bytes, originator)
60
+ uri = URI.parse(url)
61
+ body = payload_bytes.pack('C*')
62
+
63
+ if @http_client
64
+ @http_client.call(uri, body, originator)
65
+ else
66
+ perform_request(uri, body, originator)
67
+ end
68
+ end
69
+
70
+ def perform_request(uri, body, originator)
71
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
72
+ request = Net::HTTP::Post.new(uri.request_uri)
73
+ request['Content-Type'] = 'application/octet-stream'
74
+ request['Origin'] = originator if originator && !originator.empty?
75
+ request.body = body
76
+ response = http.request(request)
77
+ raise "HTTPWalletWire: HTTP #{response.code} from #{uri}" unless response.is_a?(Net::HTTPSuccess)
78
+
79
+ response.body || ''
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ module Substrates
6
+ # BRC-100 wallet Interface implementation that transmits calls over a binary wire transport.
7
+ #
8
+ # Serialises each Interface method call into a binary wire frame via
9
+ # {BSV::Wallet::Wire::Serializer}, transmits it via a wire transport (any object
10
+ # responding to `#transmit_to_wallet`), then deserialises the response.
11
+ #
12
+ # The wire transport is duck-typed — any object that accepts
13
+ # `transmit_to_wallet(message)` where +message+ is an Array of byte integers
14
+ # and returns an Array of byte integers qualifies. The canonical wire transport
15
+ # is {HTTPWalletWire}.
16
+ #
17
+ # @example Using with HTTPWalletWire
18
+ # wire = BSV::Wallet::Substrates::HTTPWalletWire.new('http://localhost:3301')
19
+ # wallet = BSV::Wallet::Substrates::WalletWireTransceiver.new(wire, originator: 'myapp.example.com')
20
+ # result = wallet.get_public_key({ identity_key: true })
21
+ # # => { public_key: '02abc...' }
22
+ class WalletWireTransceiver
23
+ include BSV::Wallet::Interface
24
+
25
+ # @param wire [#transmit_to_wallet] wire transport (duck-typed)
26
+ # @param originator [String, nil] default FQDN of the originating application;
27
+ # may be overridden per-call via the method-level originator keyword argument
28
+ def initialize(wire, originator: nil)
29
+ @wire = wire
30
+ @originator = originator
31
+ end
32
+
33
+ def create_action(args, originator: nil)
34
+ transmit(:create_action, args, originator || @originator)
35
+ end
36
+
37
+ def sign_action(args, originator: nil)
38
+ transmit(:sign_action, args, originator || @originator)
39
+ end
40
+
41
+ def abort_action(args, originator: nil)
42
+ transmit(:abort_action, args, originator || @originator)
43
+ end
44
+
45
+ def list_actions(args, originator: nil)
46
+ transmit(:list_actions, args, originator || @originator)
47
+ end
48
+
49
+ def internalize_action(args, originator: nil)
50
+ transmit(:internalize_action, args, originator || @originator)
51
+ end
52
+
53
+ def list_outputs(args, originator: nil)
54
+ transmit(:list_outputs, args, originator || @originator)
55
+ end
56
+
57
+ def relinquish_output(args, originator: nil)
58
+ transmit(:relinquish_output, args, originator || @originator)
59
+ end
60
+
61
+ def get_public_key(args, originator: nil)
62
+ transmit(:get_public_key, args, originator || @originator)
63
+ end
64
+
65
+ def reveal_counterparty_key_linkage(args, originator: nil)
66
+ transmit(:reveal_counterparty_key_linkage, args, originator || @originator)
67
+ end
68
+
69
+ def reveal_specific_key_linkage(args, originator: nil)
70
+ transmit(:reveal_specific_key_linkage, args, originator || @originator)
71
+ end
72
+
73
+ def encrypt(args, originator: nil)
74
+ transmit(:encrypt, args, originator || @originator)
75
+ end
76
+
77
+ def decrypt(args, originator: nil)
78
+ transmit(:decrypt, args, originator || @originator)
79
+ end
80
+
81
+ def create_hmac(args, originator: nil)
82
+ transmit(:create_hmac, args, originator || @originator)
83
+ end
84
+
85
+ def verify_hmac(args, originator: nil)
86
+ transmit(:verify_hmac, args, originator || @originator)
87
+ end
88
+
89
+ def create_signature(args, originator: nil)
90
+ transmit(:create_signature, args, originator || @originator)
91
+ end
92
+
93
+ def verify_signature(args, originator: nil)
94
+ transmit(:verify_signature, args, originator || @originator)
95
+ end
96
+
97
+ def acquire_certificate(args, originator: nil)
98
+ transmit(:acquire_certificate, args, originator || @originator)
99
+ end
100
+
101
+ def list_certificates(args, originator: nil)
102
+ transmit(:list_certificates, args, originator || @originator)
103
+ end
104
+
105
+ def prove_certificate(args, originator: nil)
106
+ transmit(:prove_certificate, args, originator || @originator)
107
+ end
108
+
109
+ def relinquish_certificate(args, originator: nil)
110
+ transmit(:relinquish_certificate, args, originator || @originator)
111
+ end
112
+
113
+ def discover_by_identity_key(args, originator: nil)
114
+ transmit(:discover_by_identity_key, args, originator || @originator)
115
+ end
116
+
117
+ def discover_by_attributes(args, originator: nil)
118
+ transmit(:discover_by_attributes, args, originator || @originator)
119
+ end
120
+
121
+ def is_authenticated(args = {}, originator: nil)
122
+ transmit(:is_authenticated, args, originator || @originator)
123
+ end
124
+
125
+ def wait_for_authentication(args = {}, originator: nil)
126
+ transmit(:wait_for_authentication, args, originator || @originator)
127
+ end
128
+
129
+ def get_height(args = {}, originator: nil)
130
+ transmit(:get_height, args, originator || @originator)
131
+ end
132
+
133
+ def get_header_for_height(args, originator: nil)
134
+ transmit(:get_header_for_height, args, originator || @originator)
135
+ end
136
+
137
+ def get_network(args = {}, originator: nil)
138
+ transmit(:get_network, args, originator || @originator)
139
+ end
140
+
141
+ def get_version(args = {}, originator: nil)
142
+ transmit(:get_version, args, originator || @originator)
143
+ end
144
+
145
+ private
146
+
147
+ # Serialises +method_name+ and +args+ into a binary wire frame, transmits it
148
+ # via the wire transport, and deserialises the response.
149
+ #
150
+ # Error responses (non-zero first byte) are parsed and raised as {WalletError}
151
+ # by {BSV::Wallet::Wire::Serializer.deserialize_response}.
152
+ #
153
+ # @param method_name [Symbol] BRC-100 method name (snake_case)
154
+ # @param args [Hash] method arguments
155
+ # @param orig [String, nil] FQDN of the originating application
156
+ # @return [Hash] deserialised result
157
+ def transmit(method_name, args, orig)
158
+ frame = BSV::Wallet::Wire::Serializer.serialize_request(
159
+ method_name, args || {}, originator: orig.to_s
160
+ )
161
+ response_bytes = @wire.transmit_to_wallet(frame.bytes.to_a)
162
+ response_binary = response_bytes.pack('C*')
163
+ BSV::Wallet::Wire::Serializer.deserialize_response(method_name, response_binary)
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ # BRC-100 wallet substrates — alternative transport layers for the wallet interface.
6
+ #
7
+ # Substrates are raw transport adapters. They are not full Wallet::Interface implementations;
8
+ # instead they provide the low-level connectivity that a WalletWireTransceiver or
9
+ # HTTPWalletJSON wraps to expose the full interface.
10
+ module Substrates
11
+ autoload :HTTPWalletJSON, 'bsv/wallet_interface/substrates/http_wallet_json'
12
+ autoload :HTTPWalletWire, 'bsv/wallet_interface/substrates/http_wallet_wire'
13
+ autoload :WalletWireTransceiver, 'bsv/wallet_interface/substrates/wallet_wire_transceiver'
14
+ end
15
+ end
16
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BSV
4
- module WalletInterface
5
- VERSION = '0.7.0'
4
+ module Wallet
5
+ VERSION = '0.9.0'
6
6
  end
7
7
  end
@@ -45,6 +45,9 @@ module BSV
45
45
  # @return [BroadcastQueue] the broadcast queue used to dispatch transactions
46
46
  attr_reader :broadcast_queue
47
47
 
48
+ # @return [Interface, nil] the optional substrate for remote wallet delegation
49
+ attr_reader :substrate
50
+
48
51
  # @param key [BSV::Primitives::PrivateKey, String, KeyDeriver] signing key
49
52
  # @param storage [StorageAdapter] persistence adapter (default: FileStore).
50
53
  # Use +storage: MemoryStore.new+ for tests.
@@ -54,6 +57,10 @@ module BSV
54
57
  # @param http_client [#request, nil] injectable HTTP client for certificate issuance
55
58
  # @param broadcaster [#broadcast, nil] optional broadcaster; any object responding to #broadcast(tx)
56
59
  # @param broadcast_queue [BroadcastQueue, nil] optional broadcast queue; defaults to InlineQueue
60
+ # @param substrate [Interface, nil] optional remote wallet substrate; when set, all Interface
61
+ # methods delegate to the substrate instead of using local storage and key derivation.
62
+ # Accepts any object implementing {Interface} (e.g. {Substrates::HTTPWalletJSON},
63
+ # {Substrates::WalletWireTransceiver}).
57
64
  def initialize(
58
65
  key,
59
66
  storage: FileStore.new,
@@ -65,9 +72,11 @@ module BSV
65
72
  coin_selector: nil,
66
73
  change_generator: nil,
67
74
  broadcaster: nil,
68
- broadcast_queue: nil
75
+ broadcast_queue: nil,
76
+ substrate: nil
69
77
  )
70
78
  super(key)
79
+ @substrate = substrate
71
80
  @storage = storage
72
81
  @network = network
73
82
  @chain_provider = chain_provider
@@ -85,9 +94,13 @@ module BSV
85
94
  )
86
95
  end
87
96
 
88
- # Returns true when a broadcaster has been configured.
97
+ # Returns +true+ when broadcast is available.
98
+ #
99
+ # Delegates to the broadcast queue so that queue-embedded broadcasters
100
+ # (e.g. +SolidQueueAdapter.new(broadcaster: arc)+) are recognised even
101
+ # when no +broadcaster:+ was passed directly to +WalletClient+.
89
102
  def broadcast_enabled?
90
- !@broadcaster.nil?
103
+ @broadcast_queue.broadcast_enabled?
91
104
  end
92
105
 
93
106
  # --- Transaction Operations ---
@@ -110,8 +123,11 @@ module BSV
110
123
  # UTXOs and generates change; requires +:outputs+ and no +:inputs+
111
124
  # @param _originator [String, nil] FQDN of the originating application
112
125
  # @return [Hash] finalised result or signable_transaction
113
- def create_action(args, _originator: nil)
126
+ def create_action(args, originator: nil)
127
+ return @substrate.create_action(args, originator: originator) if @substrate
128
+
114
129
  validate_create_action!(args)
130
+ validate_broadcast_configuration!(args)
115
131
 
116
132
  send_with_txids = Array(args.dig(:options, :send_with))
117
133
 
@@ -156,7 +172,9 @@ module BSV
156
172
  # @option args [String] :reference base64 reference from create_action
157
173
  # @param _originator [String, nil] FQDN of the originating application
158
174
  # @return [Hash] with :txid and :tx (BEEF bytes)
159
- def sign_action(args, _originator: nil)
175
+ def sign_action(args, originator: nil)
176
+ return @substrate.sign_action(args, originator: originator) if @substrate
177
+
160
178
  reference = args[:reference]
161
179
  pending = @pending[reference]
162
180
  raise WalletError, 'Transaction not found for the given reference' unless pending
@@ -172,6 +190,11 @@ module BSV
172
190
  else
173
191
  pending[:args]
174
192
  end
193
+
194
+ # Re-validate broadcast configuration on merged args — options may have
195
+ # been flipped between create_action and sign_action (e.g. no_send toggled).
196
+ validate_broadcast_configuration!(merged_args)
197
+
175
198
  finalize_action(tx, merged_args)
176
199
  end
177
200
 
@@ -185,7 +208,9 @@ module BSV
185
208
  # @option args [String] :reference base64 reference to abort
186
209
  # @param _originator [String, nil] FQDN of the originating application
187
210
  # @return [Hash] { aborted: true }
188
- def abort_action(args, _originator: nil)
211
+ def abort_action(args, originator: nil)
212
+ return @substrate.abort_action(args, originator: originator) if @substrate
213
+
189
214
  reference = args[:reference]
190
215
  raise WalletError, 'Transaction not found for the given reference' unless @pending.key?(reference)
191
216
 
@@ -210,12 +235,14 @@ module BSV
210
235
  # @option args [Integer] :offset results to skip (default 0)
211
236
  # @param _originator [String, nil] FQDN of the originating application
212
237
  # @return [Hash] { total_actions: Integer, actions: Array }
213
- def list_actions(args, _originator: nil)
238
+ def list_actions(args, originator: nil)
239
+ return @substrate.list_actions(args, originator: originator) if @substrate
240
+
214
241
  validate_list_actions!(args)
215
242
  query = build_action_query(args)
216
243
  total = @storage.count_actions(query)
217
244
  actions = @storage.find_actions(query)
218
- { total_actions: total, actions: actions }
245
+ { total_actions: total, actions: strip_action_fields(actions, args) }
219
246
  end
220
247
 
221
248
  # Lists spendable outputs in a basket.
@@ -228,12 +255,14 @@ module BSV
228
255
  # @option args [Integer] :offset results to skip (default 0)
229
256
  # @param _originator [String, nil] FQDN of the originating application
230
257
  # @return [Hash] { total_outputs: Integer, outputs: Array }
231
- def list_outputs(args, _originator: nil)
258
+ def list_outputs(args, originator: nil)
259
+ return @substrate.list_outputs(args, originator: originator) if @substrate
260
+
232
261
  validate_list_outputs!(args)
233
262
  query = build_output_query(args)
234
263
  total = @storage.count_outputs(query)
235
264
  outputs = @storage.find_outputs(query)
236
- { total_outputs: total, outputs: outputs }
265
+ { total_outputs: total, outputs: strip_output_fields(outputs, args) }
237
266
  end
238
267
 
239
268
  # Removes an output from basket tracking.
@@ -243,7 +272,9 @@ module BSV
243
272
  # @option args [String] :output outpoint string
244
273
  # @param _originator [String, nil] FQDN of the originating application
245
274
  # @return [Hash] { relinquished: true }
246
- def relinquish_output(args, _originator: nil)
275
+ def relinquish_output(args, originator: nil)
276
+ return @substrate.relinquish_output(args, originator: originator) if @substrate
277
+
247
278
  Validators.validate_basket!(args[:basket])
248
279
  Validators.validate_outpoint!(args[:output])
249
280
  raise WalletError, 'Output not found' unless @storage.delete_output(args[:output])
@@ -264,7 +295,9 @@ module BSV
264
295
  # @option args [Array<String>] :labels optional labels
265
296
  # @param _originator [String, nil] FQDN of the originating application
266
297
  # @return [Hash] { accepted: true }
267
- def internalize_action(args, _originator: nil)
298
+ def internalize_action(args, originator: nil)
299
+ return @substrate.internalize_action(args, originator: originator) if @substrate
300
+
268
301
  validate_internalize_action!(args)
269
302
  beef_binary = args[:tx].pack('C*')
270
303
  beef = BSV::Transaction::Beef.from_binary(beef_binary)
@@ -280,7 +313,8 @@ module BSV
280
313
  store_proofs_from_beef(beef)
281
314
  @storage.store_transaction(tx.txid_hex, tx.to_hex)
282
315
  process_internalize_outputs(tx, args[:outputs])
283
- store_action(tx, args, status: 'completed')
316
+ has_proof = !beef.find_bump(tx.txid).nil?
317
+ store_action(tx, args, status: has_proof ? 'completed' : 'unproven')
284
318
  { accepted: true }
285
319
  end
286
320
 
@@ -290,7 +324,9 @@ module BSV
290
324
  #
291
325
  # @param _args [Hash] unused (empty hash)
292
326
  # @return [Hash] { height: Integer }
293
- def get_height(_args = {}, _originator: nil)
327
+ def get_height(args = {}, originator: nil)
328
+ return @substrate.get_height(args, originator: originator) if @substrate
329
+
294
330
  { height: @chain_provider.get_height }
295
331
  end
296
332
 
@@ -299,7 +335,9 @@ module BSV
299
335
  # @param args [Hash]
300
336
  # @option args [Integer] :height block height
301
337
  # @return [Hash] { header: String } 80-byte hex-encoded block header
302
- def get_header_for_height(args, _originator: nil)
338
+ def get_header_for_height(args, originator: nil)
339
+ return @substrate.get_header_for_height(args, originator: originator) if @substrate
340
+
303
341
  raise InvalidParameterError.new('height', 'a positive Integer') unless args[:height].is_a?(Integer) && args[:height].positive?
304
342
 
305
343
  { header: @chain_provider.get_header(args[:height]) }
@@ -309,7 +347,9 @@ module BSV
309
347
  #
310
348
  # @param _args [Hash] unused (empty hash)
311
349
  # @return [Hash] { network: String } 'mainnet' or 'testnet'
312
- def get_network(_args = {}, _originator: nil)
350
+ def get_network(args = {}, originator: nil)
351
+ return @substrate.get_network(args, originator: originator) if @substrate
352
+
313
353
  { network: @network }
314
354
  end
315
355
 
@@ -317,8 +357,10 @@ module BSV
317
357
  #
318
358
  # @param _args [Hash] unused (empty hash)
319
359
  # @return [Hash] { version: String } in vendor-major.minor.patch format
320
- def get_version(_args = {}, _originator: nil)
321
- { version: "bsv-wallet-#{BSV::WalletInterface::VERSION}" }
360
+ def get_version(args = {}, originator: nil)
361
+ return @substrate.get_version(args, originator: originator) if @substrate
362
+
363
+ { version: "bsv-wallet-#{BSV::Wallet::VERSION}" }
322
364
  end
323
365
 
324
366
  # Discovers on-chain UTXOs for the wallet's identity address and imports
@@ -427,7 +469,9 @@ module BSV
427
469
  #
428
470
  # @param _args [Hash] unused (empty hash)
429
471
  # @return [Hash] { authenticated: Boolean }
430
- def is_authenticated(_args = {}, _originator: nil)
472
+ def is_authenticated(args = {}, originator: nil)
473
+ return @substrate.is_authenticated(args, originator: originator) if @substrate
474
+
431
475
  { authenticated: true }
432
476
  end
433
477
 
@@ -436,7 +480,9 @@ module BSV
436
480
  #
437
481
  # @param _args [Hash] unused (empty hash)
438
482
  # @return [Hash] { authenticated: true }
439
- def wait_for_authentication(_args = {}, _originator: nil)
483
+ def wait_for_authentication(args = {}, originator: nil)
484
+ return @substrate.wait_for_authentication(args, originator: originator) if @substrate
485
+
440
486
  { authenticated: true }
441
487
  end
442
488
 
@@ -458,7 +504,9 @@ module BSV
458
504
  # @option args [String] :keyring_revealer pubkey hex or 'certifier' (required for direct)
459
505
  # @option args [Hash] :keyring_for_subject field_name => base64 key (required for direct)
460
506
  # @return [Hash] the stored certificate
461
- def acquire_certificate(args, _originator: nil)
507
+ def acquire_certificate(args, originator: nil)
508
+ return @substrate.acquire_certificate(args, originator: originator) if @substrate
509
+
462
510
  validate_acquire_certificate!(args)
463
511
 
464
512
  cert = if args[:acquisition_protocol] == 'issuance'
@@ -479,7 +527,9 @@ module BSV
479
527
  # @option args [Integer] :limit max results (default 10)
480
528
  # @option args [Integer] :offset number to skip (default 0)
481
529
  # @return [Hash] { total_certificates:, certificates: [...] }
482
- def list_certificates(args, _originator: nil)
530
+ def list_certificates(args, originator: nil)
531
+ return @substrate.list_certificates(args, originator: originator) if @substrate
532
+
483
533
  raise InvalidParameterError.new('certifiers', 'a non-empty Array') unless args[:certifiers].is_a?(Array) && !args[:certifiers].empty?
484
534
  raise InvalidParameterError.new('types', 'a non-empty Array') unless args[:types].is_a?(Array) && !args[:types].empty?
485
535
 
@@ -505,7 +555,9 @@ module BSV
505
555
  # @option args [Array<String>] :fields_to_reveal field names to reveal
506
556
  # @option args [String] :verifier verifier public key hex
507
557
  # @return [Hash] { keyring_for_verifier: { field_name => Array<Integer> } }
508
- def prove_certificate(args, _originator: nil)
558
+ def prove_certificate(args, originator: nil)
559
+ return @substrate.prove_certificate(args, originator: originator) if @substrate
560
+
509
561
  cert_arg = args[:certificate]
510
562
  fields_to_reveal = args[:fields_to_reveal]
511
563
  verifier = args[:verifier]
@@ -528,8 +580,8 @@ module BSV
528
580
  # Encrypt the keyring entry for the verifier
529
581
  encrypted = encrypt({
530
582
  plaintext: key_value.bytes,
531
- protocol_id: [2, 'certificate field revelation'],
532
- key_id: "#{cert_arg[:type]} #{cert_arg[:serial_number]} #{field_name}",
583
+ protocol_id: [2, 'certificate field encryption'],
584
+ key_id: "#{cert_arg[:serial_number]} #{field_name}",
533
585
  counterparty: verifier
534
586
  })
535
587
  keyring_for_verifier[field_name] = encrypted[:ciphertext]
@@ -545,7 +597,9 @@ module BSV
545
597
  # @option args [String] :serial_number serial number
546
598
  # @option args [String] :certifier certifier public key hex
547
599
  # @return [Hash] { relinquished: true }
548
- def relinquish_certificate(args, _originator: nil)
600
+ def relinquish_certificate(args, originator: nil)
601
+ return @substrate.relinquish_certificate(args, originator: originator) if @substrate
602
+
549
603
  deleted = @storage.delete_certificate(
550
604
  type: args[:type],
551
605
  serial_number: args[:serial_number],
@@ -566,7 +620,9 @@ module BSV
566
620
  # @option args [Integer] :limit max results (default 10)
567
621
  # @option args [Integer] :offset number to skip (default 0)
568
622
  # @return [Hash] { total_certificates:, certificates: [...] }
569
- def discover_by_identity_key(args, _originator: nil)
623
+ def discover_by_identity_key(args, originator: nil)
624
+ return @substrate.discover_by_identity_key(args, originator: originator) if @substrate
625
+
570
626
  Validators.validate_pub_key_hex!(args[:identity_key], 'identity_key')
571
627
 
572
628
  query = { subject: args[:identity_key], limit: args[:limit] || 10, offset: args[:offset] || 0 }
@@ -585,7 +641,9 @@ module BSV
585
641
  # @option args [Integer] :limit max results (default 10)
586
642
  # @option args [Integer] :offset number to skip (default 0)
587
643
  # @return [Hash] { total_certificates:, certificates: [...] }
588
- def discover_by_attributes(args, _originator: nil)
644
+ def discover_by_attributes(args, originator: nil)
645
+ return @substrate.discover_by_attributes(args, originator: originator) if @substrate
646
+
589
647
  raise InvalidParameterError.new('attributes', 'a non-empty Hash') unless args[:attributes].is_a?(Hash) && !args[:attributes].empty?
590
648
 
591
649
  query = { attributes: args[:attributes], limit: args[:limit] || 10, offset: args[:offset] || 0 }
@@ -594,6 +652,65 @@ module BSV
594
652
  { total_certificates: total, certificates: certs.map { |c| cert_without_keyring(c) } }
595
653
  end
596
654
 
655
+ # --- ProtoWallet crypto method overrides for substrate delegation ---
656
+ #
657
+ # When a substrate is configured, these methods delegate to it rather than
658
+ # performing local key derivation and cryptographic operations.
659
+
660
+ def get_public_key(args, originator: nil)
661
+ return @substrate.get_public_key(args, originator: originator) if @substrate
662
+
663
+ super
664
+ end
665
+
666
+ def reveal_counterparty_key_linkage(args, originator: nil)
667
+ return @substrate.reveal_counterparty_key_linkage(args, originator: originator) if @substrate
668
+
669
+ super
670
+ end
671
+
672
+ def reveal_specific_key_linkage(args, originator: nil)
673
+ return @substrate.reveal_specific_key_linkage(args, originator: originator) if @substrate
674
+
675
+ super
676
+ end
677
+
678
+ def encrypt(args, originator: nil)
679
+ return @substrate.encrypt(args, originator: originator) if @substrate
680
+
681
+ super
682
+ end
683
+
684
+ def decrypt(args, originator: nil)
685
+ return @substrate.decrypt(args, originator: originator) if @substrate
686
+
687
+ super
688
+ end
689
+
690
+ def create_hmac(args, originator: nil)
691
+ return @substrate.create_hmac(args, originator: originator) if @substrate
692
+
693
+ super
694
+ end
695
+
696
+ def verify_hmac(args, originator: nil)
697
+ return @substrate.verify_hmac(args, originator: originator) if @substrate
698
+
699
+ super
700
+ end
701
+
702
+ def create_signature(args, originator: nil)
703
+ return @substrate.create_signature(args, originator: originator) if @substrate
704
+
705
+ super
706
+ end
707
+
708
+ def verify_signature(args, originator: nil)
709
+ return @substrate.verify_signature(args, originator: originator) if @substrate
710
+
711
+ super
712
+ end
713
+
597
714
  # Maximum ancestor depth to traverse when wiring source transactions.
598
715
  # Guards against stack overflow on pathologically deep or cyclic chains.
599
716
  ANCESTOR_DEPTH_CAP = 64
@@ -962,6 +1079,25 @@ module BSV
962
1079
 
963
1080
  # --- Validation ---
964
1081
 
1082
+ # Raises +WalletError+ when a broadcast is required but unavailable.
1083
+ #
1084
+ # Broadcast is required whenever +no_send+ is absent or false. Callers
1085
+ # that do not intend to broadcast on-chain must pass
1086
+ # +options: { no_send: true }+ to opt out.
1087
+ #
1088
+ # @param args [Hash] action arguments (merged, as seen at call site)
1089
+ # @raise [WalletError] if broadcast is needed but +broadcast_enabled?+ is false
1090
+ def validate_broadcast_configuration!(args)
1091
+ no_send = args.dig(:options, :no_send)
1092
+ return if no_send
1093
+ return if broadcast_enabled?
1094
+
1095
+ raise WalletError,
1096
+ 'create_action requires a broadcaster for on-chain broadcast. ' \
1097
+ 'Pass broadcaster: BSV::Network::ARC.default to WalletClient.new, ' \
1098
+ 'or options: { no_send: true } to build a transaction without broadcasting.'
1099
+ end
1100
+
965
1101
  def validate_create_action!(args)
966
1102
  Validators.validate_description!(args[:description])
967
1103
  inputs_present = args[:inputs] && !args[:inputs].empty?
@@ -1436,6 +1572,47 @@ module BSV
1436
1572
  query
1437
1573
  end
1438
1574
 
1575
+ # --- Include-flag stripping ---
1576
+
1577
+ def strip_action_fields(actions, args)
1578
+ actions.map do |action|
1579
+ a = action.dup
1580
+ a.delete(:labels) unless args[:include_labels] == true
1581
+ a.delete(:inputs) unless args[:include_inputs] == true
1582
+
1583
+ if a.key?(:inputs)
1584
+ strip_src = args[:include_input_source_locking_scripts] != true
1585
+ strip_unlock = args[:include_input_unlocking_scripts] != true
1586
+ if strip_src || strip_unlock
1587
+ a[:inputs] = a[:inputs].map do |i|
1588
+ d = i.dup
1589
+ d.delete(:source_locking_script) if strip_src
1590
+ d.delete(:unlocking_script) if strip_unlock
1591
+ d
1592
+ end
1593
+ end
1594
+ end
1595
+
1596
+ a.delete(:outputs) unless args[:include_outputs] == true
1597
+
1598
+ if a.key?(:outputs) && args[:include_output_locking_scripts] != true
1599
+ a[:outputs] = a[:outputs].map { |o| o.dup.tap { |h| h.delete(:locking_script) } }
1600
+ end
1601
+
1602
+ a
1603
+ end
1604
+ end
1605
+
1606
+ def strip_output_fields(outputs, args)
1607
+ outputs.map do |output|
1608
+ o = output.dup
1609
+ o.delete(:tags) unless args[:include_tags] == true
1610
+ o.delete(:labels) unless args[:include_labels] == true
1611
+ o.delete(:custom_instructions) unless args[:include_custom_instructions] == true
1612
+ o
1613
+ end
1614
+ end
1615
+
1439
1616
  # --- Internalize helpers ---
1440
1617
 
1441
1618
  def store_proofs_from_beef(beef)
@@ -1580,20 +1757,19 @@ module BSV
1580
1757
  end
1581
1758
 
1582
1759
  def acquire_via_issuance(args)
1583
- uri = URI(args[:certifier_url])
1584
- request = Net::HTTP::Post.new(uri)
1585
- request['Content-Type'] = 'application/json'
1586
- request.body = JSON.generate({
1587
- type: args[:type],
1588
- subject: @key_deriver.identity_key,
1589
- certifier: args[:certifier],
1590
- fields: args[:fields]
1591
- })
1592
-
1593
- response = execute_http(uri, request)
1594
- code = response.code.to_i
1760
+ response = auth_fetch_client.fetch(
1761
+ args[:certifier_url],
1762
+ method: 'POST',
1763
+ headers: { 'content-type' => 'application/json' },
1764
+ body: JSON.generate({
1765
+ type: args[:type],
1766
+ subject: @key_deriver.identity_key,
1767
+ certifier: args[:certifier],
1768
+ fields: args[:fields]
1769
+ })
1770
+ )
1595
1771
 
1596
- raise WalletError, "Certificate issuance failed: HTTP #{code}" unless (200..299).cover?(code)
1772
+ raise WalletError, "Certificate issuance failed: HTTP #{response.status}" unless (200..299).cover?(response.status)
1597
1773
 
1598
1774
  body = JSON.parse(response.body)
1599
1775
 
@@ -1619,6 +1795,10 @@ module BSV
1619
1795
  raise WalletError, 'Certificate issuance failed: invalid JSON response'
1620
1796
  end
1621
1797
 
1798
+ def auth_fetch_client
1799
+ @auth_fetch_client ||= BSV::Auth::AuthFetch.new(wallet: self)
1800
+ end
1801
+
1622
1802
  def execute_http(uri, request)
1623
1803
  if @http_client
1624
1804
  @http_client.request(uri, request)
@@ -1,11 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BSV
4
- module WalletInterface
5
- autoload :VERSION, 'bsv/wallet_interface/version'
6
- end
7
-
8
4
  module Wallet
5
+ autoload :VERSION, 'bsv/wallet_interface/version'
6
+
9
7
  # BRC-100 Interface
10
8
  autoload :Interface, 'bsv/wallet_interface/interface'
11
9
  autoload :KeyDeriver, 'bsv/wallet_interface/key_deriver'
@@ -23,6 +21,7 @@ module BSV
23
21
  autoload :WhatsOnChainProvider, 'bsv/wallet_interface/whats_on_chain_provider'
24
22
  autoload :WalletClient, 'bsv/wallet_interface/wallet_client'
25
23
  autoload :Wire, 'bsv/wallet_interface/wire'
24
+ autoload :Substrates, 'bsv/wallet_interface/substrates'
26
25
  autoload :CertificateSignature, 'bsv/wallet_interface/certificate_signature'
27
26
  autoload :FeeModel, 'bsv/wallet_interface/fee_model'
28
27
  autoload :FeeEstimator, 'bsv/wallet_interface/fee_estimator'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bsv-wallet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Bettison
@@ -29,7 +29,7 @@ dependencies:
29
29
  requirements:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 0.10.0
32
+ version: 0.11.0
33
33
  - - "<"
34
34
  - !ruby/object:Gem::Version
35
35
  version: '1.0'
@@ -39,7 +39,7 @@ dependencies:
39
39
  requirements:
40
40
  - - ">="
41
41
  - !ruby/object:Gem::Version
42
- version: 0.10.0
42
+ version: 0.11.0
43
43
  - - "<"
44
44
  - !ruby/object:Gem::Version
45
45
  version: '1.0'
@@ -75,6 +75,10 @@ files:
75
75
  - lib/bsv/wallet_interface/proof_store.rb
76
76
  - lib/bsv/wallet_interface/proto_wallet.rb
77
77
  - lib/bsv/wallet_interface/storage_adapter.rb
78
+ - lib/bsv/wallet_interface/substrates.rb
79
+ - lib/bsv/wallet_interface/substrates/http_wallet_json.rb
80
+ - lib/bsv/wallet_interface/substrates/http_wallet_wire.rb
81
+ - lib/bsv/wallet_interface/substrates/wallet_wire_transceiver.rb
78
82
  - lib/bsv/wallet_interface/validators.rb
79
83
  - lib/bsv/wallet_interface/version.rb
80
84
  - lib/bsv/wallet_interface/wallet_client.rb