bsv-wallet 0.7.0 → 0.8.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: c71d9a7e4b68e5d47c4d95e3a8bcd6c23c698a92ca3faa4809a9a7abf1171b8e
4
+ data.tar.gz: f61618ef74ba89eae855dc710244f5a9208bff34a2244f4bb2ab6425d6774fea
5
5
  SHA512:
6
- metadata.gz: 974ffa3042e2bb4fe3c9f7ec94d8503c55ed20e94d5a808a667700339fbb5151eec6b12c185238353ccfd40e62f197477081fdb7b6b9f9ce75da4598a4536b90
7
- data.tar.gz: 0af0b5d238c57a1b640ea243b2bce0e5e33fd55932354689a62b1c05dfe3b41d776fdfcd5fe245fa3973de98632d6f0985bbfc4bed99b4d2527494dc16fc2f6f
6
+ metadata.gz: 9c9d6badbfaac1bb19ed0bf5745ccfe21aba5372822e692be72f1065df56175481277e8f1b41daf4c17dd0acc758e43a2dcf73d4e8c96147d982dafd7b7a7ca3
7
+ data.tar.gz: 161733d4e9c277c55d4e3b1d89eab89bf156d066575c3f924e066c636d55f65606cd1e4df7afdbbdf0ff8cea615d5d6c09c30aafc63773294370c22659a3bf85
data/CHANGELOG.md CHANGED
@@ -5,6 +5,28 @@ 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.8.0 — 2026-04-15
9
+
10
+ ### Added
11
+ - BRC-100 substrates: `HTTPWalletJSON` for JSON-over-HTTP, `HTTPWalletWire`
12
+ for binary transport, and `WalletWireTransceiver` Interface adapter (#449–#451)
13
+ - `WalletClient` accepts `substrate:` constructor param for remote wallet
14
+ delegation — all Interface methods delegate to the substrate when set (#452)
15
+ - `list_actions` and `list_outputs` honour `include_labels`, `include_inputs`,
16
+ and `include_outputs` flags (#448)
17
+ - `acquire_certificate` uses `AuthFetch` for BRC-104 authenticated certificate
18
+ issuance (#453)
19
+
20
+ ### Fixed
21
+ - `prove_certificate` now uses correct protocol ID (`'certificate field encryption'`)
22
+ and key ID format (`"#{serial_number} #{field_name}"`) matching TS/Go SDKs —
23
+ previously incompatible cross-SDK (#424)
24
+ - Code review findings addressed for substrates and include flags
25
+
26
+ ### Changed
27
+ - `BSV::WalletInterface` module removed — `VERSION` now lives in `BSV::Wallet::VERSION`
28
+ where all other wallet constants already reside
29
+
8
30
  ## 0.7.0 — 2026-04-12
9
31
 
10
32
  ### Added
@@ -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.8.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
@@ -110,7 +119,9 @@ module BSV
110
119
  # UTXOs and generates change; requires +:outputs+ and no +:inputs+
111
120
  # @param _originator [String, nil] FQDN of the originating application
112
121
  # @return [Hash] finalised result or signable_transaction
113
- def create_action(args, _originator: nil)
122
+ def create_action(args, originator: nil)
123
+ return @substrate.create_action(args, originator: originator) if @substrate
124
+
114
125
  validate_create_action!(args)
115
126
 
116
127
  send_with_txids = Array(args.dig(:options, :send_with))
@@ -156,7 +167,9 @@ module BSV
156
167
  # @option args [String] :reference base64 reference from create_action
157
168
  # @param _originator [String, nil] FQDN of the originating application
158
169
  # @return [Hash] with :txid and :tx (BEEF bytes)
159
- def sign_action(args, _originator: nil)
170
+ def sign_action(args, originator: nil)
171
+ return @substrate.sign_action(args, originator: originator) if @substrate
172
+
160
173
  reference = args[:reference]
161
174
  pending = @pending[reference]
162
175
  raise WalletError, 'Transaction not found for the given reference' unless pending
@@ -185,7 +198,9 @@ module BSV
185
198
  # @option args [String] :reference base64 reference to abort
186
199
  # @param _originator [String, nil] FQDN of the originating application
187
200
  # @return [Hash] { aborted: true }
188
- def abort_action(args, _originator: nil)
201
+ def abort_action(args, originator: nil)
202
+ return @substrate.abort_action(args, originator: originator) if @substrate
203
+
189
204
  reference = args[:reference]
190
205
  raise WalletError, 'Transaction not found for the given reference' unless @pending.key?(reference)
191
206
 
@@ -210,12 +225,14 @@ module BSV
210
225
  # @option args [Integer] :offset results to skip (default 0)
211
226
  # @param _originator [String, nil] FQDN of the originating application
212
227
  # @return [Hash] { total_actions: Integer, actions: Array }
213
- def list_actions(args, _originator: nil)
228
+ def list_actions(args, originator: nil)
229
+ return @substrate.list_actions(args, originator: originator) if @substrate
230
+
214
231
  validate_list_actions!(args)
215
232
  query = build_action_query(args)
216
233
  total = @storage.count_actions(query)
217
234
  actions = @storage.find_actions(query)
218
- { total_actions: total, actions: actions }
235
+ { total_actions: total, actions: strip_action_fields(actions, args) }
219
236
  end
220
237
 
221
238
  # Lists spendable outputs in a basket.
@@ -228,12 +245,14 @@ module BSV
228
245
  # @option args [Integer] :offset results to skip (default 0)
229
246
  # @param _originator [String, nil] FQDN of the originating application
230
247
  # @return [Hash] { total_outputs: Integer, outputs: Array }
231
- def list_outputs(args, _originator: nil)
248
+ def list_outputs(args, originator: nil)
249
+ return @substrate.list_outputs(args, originator: originator) if @substrate
250
+
232
251
  validate_list_outputs!(args)
233
252
  query = build_output_query(args)
234
253
  total = @storage.count_outputs(query)
235
254
  outputs = @storage.find_outputs(query)
236
- { total_outputs: total, outputs: outputs }
255
+ { total_outputs: total, outputs: strip_output_fields(outputs, args) }
237
256
  end
238
257
 
239
258
  # Removes an output from basket tracking.
@@ -243,7 +262,9 @@ module BSV
243
262
  # @option args [String] :output outpoint string
244
263
  # @param _originator [String, nil] FQDN of the originating application
245
264
  # @return [Hash] { relinquished: true }
246
- def relinquish_output(args, _originator: nil)
265
+ def relinquish_output(args, originator: nil)
266
+ return @substrate.relinquish_output(args, originator: originator) if @substrate
267
+
247
268
  Validators.validate_basket!(args[:basket])
248
269
  Validators.validate_outpoint!(args[:output])
249
270
  raise WalletError, 'Output not found' unless @storage.delete_output(args[:output])
@@ -264,7 +285,9 @@ module BSV
264
285
  # @option args [Array<String>] :labels optional labels
265
286
  # @param _originator [String, nil] FQDN of the originating application
266
287
  # @return [Hash] { accepted: true }
267
- def internalize_action(args, _originator: nil)
288
+ def internalize_action(args, originator: nil)
289
+ return @substrate.internalize_action(args, originator: originator) if @substrate
290
+
268
291
  validate_internalize_action!(args)
269
292
  beef_binary = args[:tx].pack('C*')
270
293
  beef = BSV::Transaction::Beef.from_binary(beef_binary)
@@ -290,7 +313,9 @@ module BSV
290
313
  #
291
314
  # @param _args [Hash] unused (empty hash)
292
315
  # @return [Hash] { height: Integer }
293
- def get_height(_args = {}, _originator: nil)
316
+ def get_height(args = {}, originator: nil)
317
+ return @substrate.get_height(args, originator: originator) if @substrate
318
+
294
319
  { height: @chain_provider.get_height }
295
320
  end
296
321
 
@@ -299,7 +324,9 @@ module BSV
299
324
  # @param args [Hash]
300
325
  # @option args [Integer] :height block height
301
326
  # @return [Hash] { header: String } 80-byte hex-encoded block header
302
- def get_header_for_height(args, _originator: nil)
327
+ def get_header_for_height(args, originator: nil)
328
+ return @substrate.get_header_for_height(args, originator: originator) if @substrate
329
+
303
330
  raise InvalidParameterError.new('height', 'a positive Integer') unless args[:height].is_a?(Integer) && args[:height].positive?
304
331
 
305
332
  { header: @chain_provider.get_header(args[:height]) }
@@ -309,7 +336,9 @@ module BSV
309
336
  #
310
337
  # @param _args [Hash] unused (empty hash)
311
338
  # @return [Hash] { network: String } 'mainnet' or 'testnet'
312
- def get_network(_args = {}, _originator: nil)
339
+ def get_network(args = {}, originator: nil)
340
+ return @substrate.get_network(args, originator: originator) if @substrate
341
+
313
342
  { network: @network }
314
343
  end
315
344
 
@@ -317,8 +346,10 @@ module BSV
317
346
  #
318
347
  # @param _args [Hash] unused (empty hash)
319
348
  # @return [Hash] { version: String } in vendor-major.minor.patch format
320
- def get_version(_args = {}, _originator: nil)
321
- { version: "bsv-wallet-#{BSV::WalletInterface::VERSION}" }
349
+ def get_version(args = {}, originator: nil)
350
+ return @substrate.get_version(args, originator: originator) if @substrate
351
+
352
+ { version: "bsv-wallet-#{BSV::Wallet::VERSION}" }
322
353
  end
323
354
 
324
355
  # Discovers on-chain UTXOs for the wallet's identity address and imports
@@ -427,7 +458,9 @@ module BSV
427
458
  #
428
459
  # @param _args [Hash] unused (empty hash)
429
460
  # @return [Hash] { authenticated: Boolean }
430
- def is_authenticated(_args = {}, _originator: nil)
461
+ def is_authenticated(args = {}, originator: nil)
462
+ return @substrate.is_authenticated(args, originator: originator) if @substrate
463
+
431
464
  { authenticated: true }
432
465
  end
433
466
 
@@ -436,7 +469,9 @@ module BSV
436
469
  #
437
470
  # @param _args [Hash] unused (empty hash)
438
471
  # @return [Hash] { authenticated: true }
439
- def wait_for_authentication(_args = {}, _originator: nil)
472
+ def wait_for_authentication(args = {}, originator: nil)
473
+ return @substrate.wait_for_authentication(args, originator: originator) if @substrate
474
+
440
475
  { authenticated: true }
441
476
  end
442
477
 
@@ -458,7 +493,9 @@ module BSV
458
493
  # @option args [String] :keyring_revealer pubkey hex or 'certifier' (required for direct)
459
494
  # @option args [Hash] :keyring_for_subject field_name => base64 key (required for direct)
460
495
  # @return [Hash] the stored certificate
461
- def acquire_certificate(args, _originator: nil)
496
+ def acquire_certificate(args, originator: nil)
497
+ return @substrate.acquire_certificate(args, originator: originator) if @substrate
498
+
462
499
  validate_acquire_certificate!(args)
463
500
 
464
501
  cert = if args[:acquisition_protocol] == 'issuance'
@@ -479,7 +516,9 @@ module BSV
479
516
  # @option args [Integer] :limit max results (default 10)
480
517
  # @option args [Integer] :offset number to skip (default 0)
481
518
  # @return [Hash] { total_certificates:, certificates: [...] }
482
- def list_certificates(args, _originator: nil)
519
+ def list_certificates(args, originator: nil)
520
+ return @substrate.list_certificates(args, originator: originator) if @substrate
521
+
483
522
  raise InvalidParameterError.new('certifiers', 'a non-empty Array') unless args[:certifiers].is_a?(Array) && !args[:certifiers].empty?
484
523
  raise InvalidParameterError.new('types', 'a non-empty Array') unless args[:types].is_a?(Array) && !args[:types].empty?
485
524
 
@@ -505,7 +544,9 @@ module BSV
505
544
  # @option args [Array<String>] :fields_to_reveal field names to reveal
506
545
  # @option args [String] :verifier verifier public key hex
507
546
  # @return [Hash] { keyring_for_verifier: { field_name => Array<Integer> } }
508
- def prove_certificate(args, _originator: nil)
547
+ def prove_certificate(args, originator: nil)
548
+ return @substrate.prove_certificate(args, originator: originator) if @substrate
549
+
509
550
  cert_arg = args[:certificate]
510
551
  fields_to_reveal = args[:fields_to_reveal]
511
552
  verifier = args[:verifier]
@@ -528,8 +569,8 @@ module BSV
528
569
  # Encrypt the keyring entry for the verifier
529
570
  encrypted = encrypt({
530
571
  plaintext: key_value.bytes,
531
- protocol_id: [2, 'certificate field revelation'],
532
- key_id: "#{cert_arg[:type]} #{cert_arg[:serial_number]} #{field_name}",
572
+ protocol_id: [2, 'certificate field encryption'],
573
+ key_id: "#{cert_arg[:serial_number]} #{field_name}",
533
574
  counterparty: verifier
534
575
  })
535
576
  keyring_for_verifier[field_name] = encrypted[:ciphertext]
@@ -545,7 +586,9 @@ module BSV
545
586
  # @option args [String] :serial_number serial number
546
587
  # @option args [String] :certifier certifier public key hex
547
588
  # @return [Hash] { relinquished: true }
548
- def relinquish_certificate(args, _originator: nil)
589
+ def relinquish_certificate(args, originator: nil)
590
+ return @substrate.relinquish_certificate(args, originator: originator) if @substrate
591
+
549
592
  deleted = @storage.delete_certificate(
550
593
  type: args[:type],
551
594
  serial_number: args[:serial_number],
@@ -566,7 +609,9 @@ module BSV
566
609
  # @option args [Integer] :limit max results (default 10)
567
610
  # @option args [Integer] :offset number to skip (default 0)
568
611
  # @return [Hash] { total_certificates:, certificates: [...] }
569
- def discover_by_identity_key(args, _originator: nil)
612
+ def discover_by_identity_key(args, originator: nil)
613
+ return @substrate.discover_by_identity_key(args, originator: originator) if @substrate
614
+
570
615
  Validators.validate_pub_key_hex!(args[:identity_key], 'identity_key')
571
616
 
572
617
  query = { subject: args[:identity_key], limit: args[:limit] || 10, offset: args[:offset] || 0 }
@@ -585,7 +630,9 @@ module BSV
585
630
  # @option args [Integer] :limit max results (default 10)
586
631
  # @option args [Integer] :offset number to skip (default 0)
587
632
  # @return [Hash] { total_certificates:, certificates: [...] }
588
- def discover_by_attributes(args, _originator: nil)
633
+ def discover_by_attributes(args, originator: nil)
634
+ return @substrate.discover_by_attributes(args, originator: originator) if @substrate
635
+
589
636
  raise InvalidParameterError.new('attributes', 'a non-empty Hash') unless args[:attributes].is_a?(Hash) && !args[:attributes].empty?
590
637
 
591
638
  query = { attributes: args[:attributes], limit: args[:limit] || 10, offset: args[:offset] || 0 }
@@ -594,6 +641,65 @@ module BSV
594
641
  { total_certificates: total, certificates: certs.map { |c| cert_without_keyring(c) } }
595
642
  end
596
643
 
644
+ # --- ProtoWallet crypto method overrides for substrate delegation ---
645
+ #
646
+ # When a substrate is configured, these methods delegate to it rather than
647
+ # performing local key derivation and cryptographic operations.
648
+
649
+ def get_public_key(args, originator: nil)
650
+ return @substrate.get_public_key(args, originator: originator) if @substrate
651
+
652
+ super
653
+ end
654
+
655
+ def reveal_counterparty_key_linkage(args, originator: nil)
656
+ return @substrate.reveal_counterparty_key_linkage(args, originator: originator) if @substrate
657
+
658
+ super
659
+ end
660
+
661
+ def reveal_specific_key_linkage(args, originator: nil)
662
+ return @substrate.reveal_specific_key_linkage(args, originator: originator) if @substrate
663
+
664
+ super
665
+ end
666
+
667
+ def encrypt(args, originator: nil)
668
+ return @substrate.encrypt(args, originator: originator) if @substrate
669
+
670
+ super
671
+ end
672
+
673
+ def decrypt(args, originator: nil)
674
+ return @substrate.decrypt(args, originator: originator) if @substrate
675
+
676
+ super
677
+ end
678
+
679
+ def create_hmac(args, originator: nil)
680
+ return @substrate.create_hmac(args, originator: originator) if @substrate
681
+
682
+ super
683
+ end
684
+
685
+ def verify_hmac(args, originator: nil)
686
+ return @substrate.verify_hmac(args, originator: originator) if @substrate
687
+
688
+ super
689
+ end
690
+
691
+ def create_signature(args, originator: nil)
692
+ return @substrate.create_signature(args, originator: originator) if @substrate
693
+
694
+ super
695
+ end
696
+
697
+ def verify_signature(args, originator: nil)
698
+ return @substrate.verify_signature(args, originator: originator) if @substrate
699
+
700
+ super
701
+ end
702
+
597
703
  # Maximum ancestor depth to traverse when wiring source transactions.
598
704
  # Guards against stack overflow on pathologically deep or cyclic chains.
599
705
  ANCESTOR_DEPTH_CAP = 64
@@ -1436,6 +1542,47 @@ module BSV
1436
1542
  query
1437
1543
  end
1438
1544
 
1545
+ # --- Include-flag stripping ---
1546
+
1547
+ def strip_action_fields(actions, args)
1548
+ actions.map do |action|
1549
+ a = action.dup
1550
+ a.delete(:labels) unless args[:include_labels] == true
1551
+ a.delete(:inputs) unless args[:include_inputs] == true
1552
+
1553
+ if a.key?(:inputs)
1554
+ strip_src = args[:include_input_source_locking_scripts] != true
1555
+ strip_unlock = args[:include_input_unlocking_scripts] != true
1556
+ if strip_src || strip_unlock
1557
+ a[:inputs] = a[:inputs].map do |i|
1558
+ d = i.dup
1559
+ d.delete(:source_locking_script) if strip_src
1560
+ d.delete(:unlocking_script) if strip_unlock
1561
+ d
1562
+ end
1563
+ end
1564
+ end
1565
+
1566
+ a.delete(:outputs) unless args[:include_outputs] == true
1567
+
1568
+ if a.key?(:outputs) && args[:include_output_locking_scripts] != true
1569
+ a[:outputs] = a[:outputs].map { |o| o.dup.tap { |h| h.delete(:locking_script) } }
1570
+ end
1571
+
1572
+ a
1573
+ end
1574
+ end
1575
+
1576
+ def strip_output_fields(outputs, args)
1577
+ outputs.map do |output|
1578
+ o = output.dup
1579
+ o.delete(:tags) unless args[:include_tags] == true
1580
+ o.delete(:labels) unless args[:include_labels] == true
1581
+ o.delete(:custom_instructions) unless args[:include_custom_instructions] == true
1582
+ o
1583
+ end
1584
+ end
1585
+
1439
1586
  # --- Internalize helpers ---
1440
1587
 
1441
1588
  def store_proofs_from_beef(beef)
@@ -1580,20 +1727,19 @@ module BSV
1580
1727
  end
1581
1728
 
1582
1729
  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
1730
+ response = auth_fetch_client.fetch(
1731
+ args[:certifier_url],
1732
+ method: 'POST',
1733
+ headers: { 'content-type' => 'application/json' },
1734
+ body: JSON.generate({
1735
+ type: args[:type],
1736
+ subject: @key_deriver.identity_key,
1737
+ certifier: args[:certifier],
1738
+ fields: args[:fields]
1739
+ })
1740
+ )
1595
1741
 
1596
- raise WalletError, "Certificate issuance failed: HTTP #{code}" unless (200..299).cover?(code)
1742
+ raise WalletError, "Certificate issuance failed: HTTP #{response.status}" unless (200..299).cover?(response.status)
1597
1743
 
1598
1744
  body = JSON.parse(response.body)
1599
1745
 
@@ -1619,6 +1765,10 @@ module BSV
1619
1765
  raise WalletError, 'Certificate issuance failed: invalid JSON response'
1620
1766
  end
1621
1767
 
1768
+ def auth_fetch_client
1769
+ @auth_fetch_client ||= BSV::Auth::AuthFetch.new(wallet: self)
1770
+ end
1771
+
1622
1772
  def execute_http(uri, request)
1623
1773
  if @http_client
1624
1774
  @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.8.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