bsv-sdk 0.20.0 → 0.22.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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +5 -3
  4. data/lib/bsv/network/protocols/arc.rb +4 -30
  5. data/lib/bsv/network/protocols/arcade.rb +163 -0
  6. data/lib/bsv/network/protocols/chaintracks.rb +6 -3
  7. data/lib/bsv/network/protocols/jungle_bus.rb +6 -0
  8. data/lib/bsv/network/protocols.rb +1 -0
  9. data/lib/bsv/network/providers/gorilla_pool.rb +18 -18
  10. data/lib/bsv/network/util.rb +44 -0
  11. data/lib/bsv/network.rb +1 -0
  12. data/lib/bsv/transaction/chain_tracker.rb +74 -13
  13. data/lib/bsv/transaction/chain_trackers.rb +0 -10
  14. data/lib/bsv/transaction/fee_models/live_policy.rb +10 -8
  15. data/lib/bsv/version.rb +1 -1
  16. data/lib/bsv/wallet/errors.rb +65 -21
  17. data/lib/bsv/wallet/proto_wallet/validators.rb +7 -49
  18. data/lib/bsv/wallet/proto_wallet.rb +14 -1
  19. data/lib/bsv/wallet/serializer/abort_action.rb +38 -0
  20. data/lib/bsv/wallet/serializer/acquire_certificate.rb +171 -0
  21. data/lib/bsv/wallet/serializer/certificate.rb +184 -0
  22. data/lib/bsv/wallet/serializer/common.rb +207 -0
  23. data/lib/bsv/wallet/serializer/create_action_args.rb +259 -0
  24. data/lib/bsv/wallet/serializer/create_action_result.rb +85 -0
  25. data/lib/bsv/wallet/serializer/create_hmac.rb +67 -0
  26. data/lib/bsv/wallet/serializer/create_signature.rb +90 -0
  27. data/lib/bsv/wallet/serializer/decrypt.rb +60 -0
  28. data/lib/bsv/wallet/serializer/discover_by_attributes.rb +61 -0
  29. data/lib/bsv/wallet/serializer/discover_by_identity_key.rb +49 -0
  30. data/lib/bsv/wallet/serializer/discover_certificates_result.rb +39 -0
  31. data/lib/bsv/wallet/serializer/encrypt.rb +60 -0
  32. data/lib/bsv/wallet/serializer/get_header_for_height.rb +71 -0
  33. data/lib/bsv/wallet/serializer/get_height.rb +46 -0
  34. data/lib/bsv/wallet/serializer/get_network.rb +65 -0
  35. data/lib/bsv/wallet/serializer/get_public_key.rb +86 -0
  36. data/lib/bsv/wallet/serializer/get_version.rb +44 -0
  37. data/lib/bsv/wallet/serializer/internalize_action.rb +151 -0
  38. data/lib/bsv/wallet/serializer/list_actions.rb +348 -0
  39. data/lib/bsv/wallet/serializer/list_certificates.rb +124 -0
  40. data/lib/bsv/wallet/serializer/list_outputs.rb +167 -0
  41. data/lib/bsv/wallet/serializer/prove_certificate.rb +146 -0
  42. data/lib/bsv/wallet/serializer/relinquish_certificate.rb +56 -0
  43. data/lib/bsv/wallet/serializer/relinquish_output.rb +44 -0
  44. data/lib/bsv/wallet/serializer/reveal_counterparty_key_linkage.rb +108 -0
  45. data/lib/bsv/wallet/serializer/reveal_specific_key_linkage.rb +116 -0
  46. data/lib/bsv/wallet/serializer/sign_action_args.rb +94 -0
  47. data/lib/bsv/wallet/serializer/sign_action_result.rb +49 -0
  48. data/lib/bsv/wallet/serializer/status.rb +85 -0
  49. data/lib/bsv/wallet/serializer/verify_hmac.rb +67 -0
  50. data/lib/bsv/wallet/serializer/verify_signature.rb +101 -0
  51. data/lib/bsv/wallet/serializer.rb +180 -0
  52. data/lib/bsv/wallet/substrates/http_wallet_json.rb +129 -0
  53. data/lib/bsv/wallet/substrates/http_wallet_wire.rb +99 -0
  54. data/lib/bsv/wallet/wallet_wire.rb +20 -0
  55. data/lib/bsv/wallet/wallet_wire_processor.rb +61 -0
  56. data/lib/bsv/wallet/wallet_wire_transceiver.rb +61 -0
  57. data/lib/bsv/wallet/wire/calls.rb +79 -0
  58. data/lib/bsv/wallet/wire/frame.rb +181 -0
  59. data/lib/bsv/wallet/wire/reader_writer.rb +402 -0
  60. data/lib/bsv/wallet/wire/validation.rb +213 -0
  61. data/lib/bsv/wallet/wire.rb +13 -0
  62. data/lib/bsv/wallet.rb +17 -0
  63. metadata +46 -2
  64. data/lib/bsv/transaction/chain_trackers/chaintracks.rb +0 -83
@@ -0,0 +1,129 @@
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
+ # JSON-RPC-style HTTP substrate implementing BRC-100.
11
+ #
12
+ # Dispatches all 28 BRC-100 methods as JSON POST requests to
13
+ # {base_url}/v1/wallet/{methodNameInCamelCase}. Request bodies are
14
+ # deep-converted from snake_case to camelCase via {BSV::WireFormat.to_wire};
15
+ # responses are deep-converted back via {BSV::WireFormat.from_wire}.
16
+ #
17
+ # This substrate does NOT use the BRC-103 binary frame codec — it speaks
18
+ # plain JSON over HTTP, matching the Go SDK HTTPWalletJSON implementation.
19
+ #
20
+ # @example
21
+ # client = BSV::Wallet::Substrates::HTTPWalletJSON.new(
22
+ # base_url: 'https://wallet.example.com',
23
+ # headers: { 'Authorization' => 'Bearer token' }
24
+ # )
25
+ # client.get_network # => { network: 'mainnet' }
26
+ class HTTPWalletJSON
27
+ include Interface::BRC100
28
+
29
+ # Maps each BRC-100 Ruby method name to its camelCase wire name.
30
+ # authenticated? maps to isAuthenticated — the BRC-100 wire name uses the
31
+ # is_ prefix convention; the Ruby predicate suffix is dropped for the lookup.
32
+ WIRE_METHOD_NAMES = Wire::Calls::CALL_TO_METHOD.each_with_object({}) do |(call_byte, ruby_method), map|
33
+ snake = call_byte == Wire::Calls::IS_AUTHENTICATED ? 'is_authenticated' : ruby_method.to_s
34
+ map[ruby_method] = BSV::WireFormat.snake_to_camel(snake)
35
+ end.freeze
36
+
37
+ # @param base_url [String] wallet base URL (e.g. 'https://wallet.example')
38
+ # @param http_client [#request, nil] injectable HTTP client for testing;
39
+ # must respond to +#request(uri, net_http_request)+. Defaults to +Net::HTTP+.
40
+ # @param headers [Hash] additional headers merged into every request
41
+ def initialize(base_url:, http_client: nil, headers: {})
42
+ @base_url = base_url.to_s.chomp('/')
43
+ @http_client = http_client
44
+ @headers = headers.transform_keys(&:to_s)
45
+ end
46
+
47
+ Wire::Calls::CALL_TO_METHOD.each_value do |method_name|
48
+ define_method(method_name) do |**args|
49
+ _dispatch(method_name, args)
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def _dispatch(method_name, args)
56
+ wire_name = WIRE_METHOD_NAMES.fetch(method_name)
57
+ url = URI.parse("#{@base_url}/v1/wallet/#{wire_name}")
58
+ body = args.empty? ? {} : BSV::WireFormat.to_wire(args)
59
+
60
+ response = _post(url, body)
61
+ _handle_response(response)
62
+ end
63
+
64
+ def _post(url, body)
65
+ req = Net::HTTP::Post.new(url)
66
+ @headers.each { |k, v| req[k] = v }
67
+ req['Content-Type'] = 'application/json'
68
+ req['Accept'] = 'application/json'
69
+ req.body = body.to_json
70
+
71
+ _execute_request(url, req)
72
+ rescue BSV::Wallet::Error
73
+ raise
74
+ rescue StandardError => e
75
+ raise BSV::Wallet::Error.new("HTTP request failed: #{e.message}", code: 1)
76
+ end
77
+
78
+ def _execute_request(url, req)
79
+ if @http_client
80
+ @http_client.request(url, req)
81
+ else
82
+ Net::HTTP.start(url.host, url.port,
83
+ use_ssl: url.scheme == 'https') { |conn| conn.request(req) }
84
+ end
85
+ end
86
+
87
+ def _handle_response(response)
88
+ status = response.code.to_i
89
+ raw = response.body.to_s
90
+
91
+ if (200..299).cover?(status)
92
+ _parse_success(raw, status)
93
+ else
94
+ _raise_error(raw)
95
+ end
96
+ end
97
+
98
+ def _parse_success(raw, status)
99
+ return {} if status == 204 || raw.empty?
100
+
101
+ begin
102
+ parsed = JSON.parse(raw)
103
+ rescue JSON::ParserError => e
104
+ raise BSV::Wallet::Error.new("Invalid JSON in response: #{e.message}", code: 1)
105
+ end
106
+
107
+ return parsed unless parsed.is_a?(Hash)
108
+
109
+ BSV::WireFormat.from_wire(parsed)
110
+ end
111
+
112
+ def _raise_error(raw)
113
+ error_hash = begin
114
+ JSON.parse(raw)
115
+ rescue JSON::ParserError
116
+ {}
117
+ end
118
+
119
+ code = error_hash['code'].to_i
120
+ message = error_hash['message'].to_s
121
+ stack = error_hash['stack'].to_s
122
+ code = 1 if code.zero?
123
+
124
+ raise BSV::Wallet.error_from_wire(code, message.empty? ? raw : message, stack)
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+
6
+ module BSV
7
+ module Wallet
8
+ module Substrates
9
+ # Concrete {WalletWire} implementation over HTTP/HTTPS.
10
+ #
11
+ # Transmits binary BRC-103 request frames via HTTP POST to `{base_url}/wallet`
12
+ # using `Content-Type: application/octet-stream`, and returns the raw binary
13
+ # response body.
14
+ #
15
+ # The optional `http_client:` parameter accepts any object responding to
16
+ # `#request(uri, net_http_request)` — matching the injectable client
17
+ # convention used throughout this SDK. When omitted, `Net::HTTP` is used
18
+ # directly.
19
+ #
20
+ # @example Direct wire usage
21
+ # wire = BSV::Wallet::Substrates::HTTPWalletWire.new(base_url: 'https://wallet.example')
22
+ # client = BSV::Wallet::WalletWireTransceiver.new(wire)
23
+ # client.get_network #=> { network: :mainnet }
24
+ #
25
+ # @example Convenience factory
26
+ # client = BSV::Wallet::Substrates::HTTPWalletWire.client(base_url: 'https://wallet.example')
27
+ # client.get_network #=> { network: :mainnet }
28
+ class HTTPWalletWire
29
+ include WalletWire
30
+
31
+ # @param base_url [String] wallet base URL (e.g. 'https://wallet.example')
32
+ # @param http_client [#request, nil] injectable HTTP client for testing;
33
+ # must respond to +#request(uri, net_http_request)+. Defaults to +Net::HTTP+.
34
+ # @param headers [Hash] additional headers merged into every request
35
+ def initialize(base_url:, http_client: nil, headers: {})
36
+ @uri = build_uri(base_url)
37
+ @http_client = http_client
38
+ @headers = headers.transform_keys(&:to_s)
39
+ end
40
+
41
+ # Convenience factory: wraps a new {HTTPWalletWire} in a {WalletWireTransceiver}.
42
+ #
43
+ # @param base_url [String]
44
+ # @param http_client [#request, nil]
45
+ # @param headers [Hash]
46
+ # @return [WalletWireTransceiver]
47
+ def self.client(base_url:, http_client: nil, headers: {})
48
+ WalletWireTransceiver.new(new(base_url: base_url, http_client: http_client, headers: headers))
49
+ end
50
+
51
+ # POST binary frame to `{base_url}/wallet` and return the binary response body.
52
+ #
53
+ # @param message [String] binary request frame (ASCII-8BIT)
54
+ # @return [String] binary result frame (ASCII-8BIT)
55
+ # @raise [BSV::Wallet::Error] on non-2xx HTTP status or network failure
56
+ def transmit_to_wallet(message)
57
+ http_post(message)
58
+ rescue BSV::Wallet::Error
59
+ raise
60
+ rescue SocketError, Errno::ECONNREFUSED, Errno::ETIMEDOUT,
61
+ Net::OpenTimeout, Net::ReadTimeout, Net::HTTPError => e
62
+ raise BSV::Wallet::Error.new("wallet connection failed: #{e.message}", code: 1)
63
+ rescue StandardError => e
64
+ raise BSV::Wallet::Error.new("wallet request failed: #{e.message}", code: 1)
65
+ end
66
+
67
+ private
68
+
69
+ def build_uri(base_url)
70
+ normalised = base_url.to_s.chomp('/')
71
+ URI.parse("#{normalised}/wallet")
72
+ end
73
+
74
+ def http_post(body)
75
+ request = Net::HTTP::Post.new(@uri.path)
76
+ @headers.each { |k, v| request[k] = v }
77
+ request['Content-Type'] = 'application/octet-stream'
78
+ request.body = body.respond_to?(:b) ? body.b : body.pack('C*')
79
+
80
+ response = execute_request(request)
81
+
82
+ raise BSV::Wallet::Error.new("HTTP #{response.code}: #{response.body.to_s.strip}", code: 1) unless response.is_a?(Net::HTTPSuccess)
83
+
84
+ (response.body || '').b
85
+ end
86
+
87
+ def execute_request(request)
88
+ if @http_client
89
+ @http_client.request(@uri, request)
90
+ else
91
+ Net::HTTP.start(@uri.host, @uri.port, use_ssl: @uri.scheme == 'https') do |http|
92
+ http.request(request)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ # Abstract binary transport for BRC-103 wallet communication.
6
+ #
7
+ # Include this module and implement {#transmit_to_wallet} to provide a
8
+ # concrete transport (e.g. HTTP, in-process loopback, Unix socket).
9
+ module WalletWire
10
+ # Transmit a binary request frame to the wallet and return the binary result frame.
11
+ #
12
+ # @param message [String] binary request frame (ASCII-8BIT)
13
+ # @return [String] binary result frame (ASCII-8BIT)
14
+ # @raise [NotImplementedError] unless overridden by the including class
15
+ def transmit_to_wallet(message)
16
+ raise NotImplementedError, "#{self.class}#transmit_to_wallet is not implemented"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ # Server-side BRC-103 dispatcher.
6
+ #
7
+ # Takes any {Interface::BRC100} implementation and exposes it as a
8
+ # {WalletWire}. Decodes incoming binary request frames, dispatches to the
9
+ # wallet, serialises the result, and returns a binary result frame.
10
+ #
11
+ # Error boundary: any {Error} from the wallet is serialised into an error
12
+ # frame so the client can rehydrate it. Any other {StandardError} is wrapped
13
+ # in {Error} (code 1) before framing — the processor never lets a Ruby
14
+ # exception propagate past this boundary.
15
+ #
16
+ # @example Wrap a ProtoWallet for loopback testing
17
+ # processor = BSV::Wallet::WalletWireProcessor.new(proto_wallet)
18
+ # transceiver = BSV::Wallet::WalletWireTransceiver.new(processor)
19
+ class WalletWireProcessor
20
+ include WalletWire
21
+
22
+ # @param wallet [Interface::BRC100] any BRC-100 wallet implementation
23
+ def initialize(wallet)
24
+ @wallet = wallet
25
+ end
26
+
27
+ # Process a binary request frame and return a binary result frame.
28
+ #
29
+ # @param request_bytes [String] binary request frame
30
+ # @return [String] binary result frame (success or error)
31
+ def transmit_to_wallet(request_bytes)
32
+ req = Wire::Frame.read_request(request_bytes)
33
+ call_byte = req[:call]
34
+
35
+ method_name = Wire::Calls::CALL_TO_METHOD.fetch(call_byte) do
36
+ raise Error.new("unknown call byte: #{call_byte}", code: 1)
37
+ end
38
+
39
+ serialize_result = Serializer::SERIALIZE_RESULT.fetch(call_byte) do
40
+ raise Error.new("no result serialiser for call #{call_byte}", code: 1)
41
+ end
42
+
43
+ args = Serializer::DESERIALIZE_ARGS.fetch(call_byte) do
44
+ raise Error.new("no args deserialiser for call #{call_byte}", code: 1)
45
+ end.call(req[:params])
46
+
47
+ args[:originator] = req[:originator] unless req[:originator].empty?
48
+
49
+ result = @wallet.public_send(method_name, **args)
50
+ payload = serialize_result.call(result)
51
+ Wire::Frame.write_result(payload: payload)
52
+ rescue NotImplementedError => e
53
+ Wire::Frame.write_error(error: UnsupportedActionError.new(e.message.to_s))
54
+ rescue Error => e
55
+ Wire::Frame.write_error(error: e)
56
+ rescue StandardError => e
57
+ Wire::Frame.write_error(error: Error.new(e.message.to_s, code: 1))
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ # Client-side BRC-103 transceiver.
6
+ #
7
+ # Implements every {Interface::BRC100} method by serialising the call
8
+ # arguments to a binary request frame, transmitting it over a {WalletWire},
9
+ # unframing the result, and deserialising the payload back to a Ruby hash.
10
+ #
11
+ # @example In-process loopback (testing / same-process use)
12
+ # proto = BSV::Wallet::ProtoWallet.new(key)
13
+ # processor = BSV::Wallet::WalletWireProcessor.new(proto)
14
+ # client = BSV::Wallet::WalletWireTransceiver.new(processor)
15
+ # client.get_public_key(identity_key: true)
16
+ # #=> { public_key: "02..." }
17
+ #
18
+ # @example Over HTTP
19
+ # wire = BSV::Wallet::Substrates::HTTPWalletWire.new(base_url: 'https://wallet.example')
20
+ # client = BSV::Wallet::WalletWireTransceiver.new(wire)
21
+ #
22
+ # Thread safety: each call is independent. The wire transport is the
23
+ # synchronisation boundary — concurrent calls are serialised only if the
24
+ # underlying wire implementation serialises them.
25
+ class WalletWireTransceiver
26
+ include Interface::BRC100
27
+
28
+ # @param wire [#transmit_to_wallet] any object that includes {WalletWire}
29
+ def initialize(wire)
30
+ @wire = wire
31
+ end
32
+
33
+ # Generate the 28 BRC-100 methods via metaprogramming.
34
+ #
35
+ # For each call byte → method name mapping in CALL_TO_METHOD, define a
36
+ # method that:
37
+ # 1. Extracts originator from kwargs (default '').
38
+ # 2. Serialises the args via the SERIALIZE_ARGS dispatch table.
39
+ # 3. Frames and transmits the binary request.
40
+ # 4. Unframes the result (raises on error frame).
41
+ # 5. Deserialises the payload via DESERIALIZE_RESULT.
42
+ Wire::Calls::CALL_TO_METHOD.each do |call_byte, method_name|
43
+ define_method(method_name) do |**args|
44
+ _dispatch(call_byte, args)
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def _dispatch(call_byte, args)
51
+ originator = args[:originator].to_s
52
+ Wire::Validation.originator_domain!('originator', originator) unless originator.empty?
53
+ params = Serializer::SERIALIZE_ARGS.fetch(call_byte).call(args)
54
+ frame = Wire::Frame.write_request(call: call_byte, originator: originator, params: params)
55
+ reply = @wire.transmit_to_wallet(frame)
56
+ payload = Wire::Frame.read_result(reply)
57
+ Serializer::DESERIALIZE_RESULT.fetch(call_byte).call(payload)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ module Wire
6
+ # BRC-103 call byte constants and dispatch tables.
7
+ #
8
+ # Each constant maps to the corresponding Go SDK CallXxx constant in
9
+ # go-sdk/wallet/substrates/wallet_wire_calls.go. The CALL_TO_METHOD
10
+ # table maps each byte to the actual Ruby method name on Interface::BRC100.
11
+ #
12
+ # Note: call byte 23 maps to :authenticated? (predicate suffix preserved),
13
+ # not :is_authenticated, to match the existing Interface::BRC100 definition.
14
+ module Calls
15
+ CREATE_ACTION = 1
16
+ SIGN_ACTION = 2
17
+ ABORT_ACTION = 3
18
+ LIST_ACTIONS = 4
19
+ INTERNALIZE_ACTION = 5
20
+ LIST_OUTPUTS = 6
21
+ RELINQUISH_OUTPUT = 7
22
+ GET_PUBLIC_KEY = 8
23
+ REVEAL_COUNTERPARTY_KEY_LINKAGE = 9
24
+ REVEAL_SPECIFIC_KEY_LINKAGE = 10
25
+ ENCRYPT = 11
26
+ DECRYPT = 12
27
+ CREATE_HMAC = 13
28
+ VERIFY_HMAC = 14
29
+ CREATE_SIGNATURE = 15
30
+ VERIFY_SIGNATURE = 16
31
+ ACQUIRE_CERTIFICATE = 17
32
+ LIST_CERTIFICATES = 18
33
+ PROVE_CERTIFICATE = 19
34
+ RELINQUISH_CERTIFICATE = 20
35
+ DISCOVER_BY_IDENTITY_KEY = 21
36
+ DISCOVER_BY_ATTRIBUTES = 22
37
+ IS_AUTHENTICATED = 23
38
+ WAIT_FOR_AUTHENTICATION = 24
39
+ GET_HEIGHT = 25
40
+ GET_HEADER_FOR_HEIGHT = 26
41
+ GET_NETWORK = 27
42
+ GET_VERSION = 28
43
+
44
+ CALL_TO_METHOD = {
45
+ CREATE_ACTION => :create_action,
46
+ SIGN_ACTION => :sign_action,
47
+ ABORT_ACTION => :abort_action,
48
+ LIST_ACTIONS => :list_actions,
49
+ INTERNALIZE_ACTION => :internalize_action,
50
+ LIST_OUTPUTS => :list_outputs,
51
+ RELINQUISH_OUTPUT => :relinquish_output,
52
+ GET_PUBLIC_KEY => :get_public_key,
53
+ REVEAL_COUNTERPARTY_KEY_LINKAGE => :reveal_counterparty_key_linkage,
54
+ REVEAL_SPECIFIC_KEY_LINKAGE => :reveal_specific_key_linkage,
55
+ ENCRYPT => :encrypt,
56
+ DECRYPT => :decrypt,
57
+ CREATE_HMAC => :create_hmac,
58
+ VERIFY_HMAC => :verify_hmac,
59
+ CREATE_SIGNATURE => :create_signature,
60
+ VERIFY_SIGNATURE => :verify_signature,
61
+ ACQUIRE_CERTIFICATE => :acquire_certificate,
62
+ LIST_CERTIFICATES => :list_certificates,
63
+ PROVE_CERTIFICATE => :prove_certificate,
64
+ RELINQUISH_CERTIFICATE => :relinquish_certificate,
65
+ DISCOVER_BY_IDENTITY_KEY => :discover_by_identity_key,
66
+ DISCOVER_BY_ATTRIBUTES => :discover_by_attributes,
67
+ IS_AUTHENTICATED => :authenticated?,
68
+ WAIT_FOR_AUTHENTICATION => :wait_for_authentication,
69
+ GET_HEIGHT => :get_height,
70
+ GET_HEADER_FOR_HEIGHT => :get_header_for_height,
71
+ GET_NETWORK => :get_network,
72
+ GET_VERSION => :get_version
73
+ }.freeze
74
+
75
+ METHOD_TO_CALL = CALL_TO_METHOD.invert.freeze
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ module Wire
6
+ # BRC-103 request and result frame codec.
7
+ #
8
+ # Port of go-sdk/wallet/serializer/frame.go. Two frame types:
9
+ #
10
+ # Request frame (client → wallet):
11
+ # [1 byte: call][1 byte: originator_len][originator_len bytes: UTF-8][remaining: params]
12
+ #
13
+ # Result frame (wallet → client):
14
+ # [1 byte: error_code]
15
+ # On success (0x00): [remaining bytes: payload]
16
+ # On error (non-zero):
17
+ # [VarInt: message_len][message bytes]
18
+ # [VarInt: stack_len][stack bytes]
19
+ module Frame
20
+ # Maximum originator byte length enforced at write time.
21
+ # Matches the BRC-100 +OriginatorDomainNameStringUnder250Bytes+ branded
22
+ # type used by +Wire::Validation.originator_domain!+.
23
+ MAX_ORIGINATOR_BYTES = 250
24
+
25
+ module_function
26
+
27
+ # Encode a request frame.
28
+ #
29
+ # @param call [Integer] call byte (1..28)
30
+ # @param originator [String] originator domain (0..250 bytes UTF-8)
31
+ # @param params [String, nil] binary params payload
32
+ # @return [String] binary frame (ASCII-8BIT encoding)
33
+ # @raise [ArgumentError] if originator exceeds 250 bytes
34
+ def write_request(call:, originator:, params: nil)
35
+ originator_bytes = originator.to_s.b
36
+ if originator_bytes.bytesize > MAX_ORIGINATOR_BYTES
37
+ raise ArgumentError,
38
+ "originator must be at most #{MAX_ORIGINATOR_BYTES} bytes, " \
39
+ "got #{originator_bytes.bytesize}"
40
+ end
41
+
42
+ buf = String.new(encoding: 'BINARY')
43
+ buf << [call, originator_bytes.bytesize].pack('CC')
44
+ buf << originator_bytes
45
+ buf << params.b if params && !params.empty?
46
+ buf
47
+ end
48
+
49
+ # Decode a request frame.
50
+ #
51
+ # @param bytes [String] binary frame
52
+ # @return [Hash] { call: Integer, originator: String, params: String }
53
+ # @raise [ArgumentError] if the frame is truncated or malformed
54
+ def read_request(bytes)
55
+ data = bytes.b
56
+ raise ArgumentError, 'frame too short: need at least 2 bytes' if data.bytesize < 2
57
+
58
+ call = data.getbyte(0)
59
+ originator_len = data.getbyte(1)
60
+
61
+ if data.bytesize < 2 + originator_len
62
+ raise ArgumentError,
63
+ "frame truncated: need #{2 + originator_len} bytes for originator, " \
64
+ "got #{data.bytesize}"
65
+ end
66
+
67
+ originator = data.byteslice(2, originator_len).force_encoding('UTF-8')
68
+ raise ArgumentError, 'frame originator is not valid UTF-8' unless originator.valid_encoding?
69
+
70
+ params = data.byteslice(2 + originator_len, data.bytesize - 2 - originator_len) || ''.b
71
+
72
+ { call: call, originator: originator, params: params }
73
+ end
74
+
75
+ # Encode a success result frame.
76
+ #
77
+ # @param payload [String, nil] binary payload
78
+ # @return [String] binary frame
79
+ def write_result(payload: nil)
80
+ buf = String.new(encoding: 'BINARY')
81
+ buf << "\x00"
82
+ buf << payload.b if payload && !payload.empty?
83
+ buf
84
+ end
85
+
86
+ # Encode an error result frame.
87
+ #
88
+ # @param error [BSV::Wallet::Error] the error to encode
89
+ # @return [String] binary frame
90
+ def write_error(error:)
91
+ wire = error.to_wire
92
+ msg_bytes = wire[:message].to_s.b
93
+ stack_bytes = wire[:stack].to_s.b
94
+
95
+ buf = String.new(encoding: 'BINARY')
96
+ buf << [wire[:code]].pack('C')
97
+ buf << encode_varint(msg_bytes.bytesize)
98
+ buf << msg_bytes
99
+ buf << encode_varint(stack_bytes.bytesize)
100
+ buf << stack_bytes
101
+ buf
102
+ end
103
+
104
+ # Decode a result frame.
105
+ #
106
+ # @param bytes [String] binary frame
107
+ # @return [String] binary payload on success
108
+ # @raise [BSV::Wallet::Error] the appropriate subclass on error
109
+ # @raise [ArgumentError] if the frame is truncated or malformed
110
+ def read_result(bytes)
111
+ data = bytes.b
112
+ raise ArgumentError, 'result frame is empty' if data.empty?
113
+
114
+ code = data.getbyte(0)
115
+
116
+ return data.byteslice(1, data.bytesize - 1) || ''.b if code.zero?
117
+
118
+ offset = 1
119
+ msg_len, vi = decode_varint(data, offset)
120
+ offset += vi
121
+
122
+ raise ArgumentError, 'result frame truncated: message' if data.bytesize < offset + msg_len
123
+
124
+ message = data.byteslice(offset, msg_len).force_encoding('UTF-8')
125
+ raise ArgumentError, 'result frame error message is not valid UTF-8' unless message.valid_encoding?
126
+
127
+ offset += msg_len
128
+
129
+ stack_len, vi = decode_varint(data, offset)
130
+ offset += vi
131
+
132
+ raise ArgumentError, 'result frame truncated: stack' if data.bytesize < offset + stack_len
133
+
134
+ stack = data.byteslice(offset, stack_len).force_encoding('UTF-8')
135
+ raise ArgumentError, 'result frame stack is not valid UTF-8' unless stack.valid_encoding?
136
+
137
+ raise BSV::Wallet.error_from_wire(code, message, stack)
138
+ end
139
+
140
+ # @param n [Integer] unsigned integer
141
+ # @return [String] Bitcoin varint encoding
142
+ def encode_varint(n)
143
+ if n < 0xFD
144
+ [n].pack('C')
145
+ elsif n <= 0xFFFF
146
+ [0xFD, n].pack('Cv')
147
+ elsif n <= 0xFFFFFFFF
148
+ [0xFE, n].pack('CV')
149
+ else
150
+ [0xFF, n].pack('CQ<')
151
+ end
152
+ end
153
+
154
+ # @param data [String] binary data
155
+ # @param offset [Integer] byte offset
156
+ # @return [Array(Integer, Integer)] decoded value, bytes consumed
157
+ def decode_varint(data, offset = 0)
158
+ raise ArgumentError, "varint: need 1 byte at #{offset}" if offset >= data.bytesize
159
+
160
+ first = data.getbyte(offset)
161
+ case first
162
+ when 0..0xFC
163
+ [first, 1]
164
+ when 0xFD
165
+ raise ArgumentError, "varint: need 3 bytes at #{offset}" if data.bytesize < offset + 3
166
+
167
+ [data.byteslice(offset + 1, 2).unpack1('v'), 3]
168
+ when 0xFE
169
+ raise ArgumentError, "varint: need 5 bytes at #{offset}" if data.bytesize < offset + 5
170
+
171
+ [data.byteslice(offset + 1, 4).unpack1('V'), 5]
172
+ when 0xFF
173
+ raise ArgumentError, "varint: need 9 bytes at #{offset}" if data.bytesize < offset + 9
174
+
175
+ [data.byteslice(offset + 1, 8).unpack1('Q<'), 9]
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end