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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +82 -0
- data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +5 -3
- data/lib/bsv/network/protocols/arc.rb +4 -30
- data/lib/bsv/network/protocols/arcade.rb +163 -0
- data/lib/bsv/network/protocols/chaintracks.rb +6 -3
- data/lib/bsv/network/protocols/jungle_bus.rb +6 -0
- data/lib/bsv/network/protocols.rb +1 -0
- data/lib/bsv/network/providers/gorilla_pool.rb +18 -18
- data/lib/bsv/network/util.rb +44 -0
- data/lib/bsv/network.rb +1 -0
- data/lib/bsv/transaction/chain_tracker.rb +74 -13
- data/lib/bsv/transaction/chain_trackers.rb +0 -10
- data/lib/bsv/transaction/fee_models/live_policy.rb +10 -8
- data/lib/bsv/version.rb +1 -1
- data/lib/bsv/wallet/errors.rb +65 -21
- data/lib/bsv/wallet/proto_wallet/validators.rb +7 -49
- data/lib/bsv/wallet/proto_wallet.rb +14 -1
- data/lib/bsv/wallet/serializer/abort_action.rb +38 -0
- data/lib/bsv/wallet/serializer/acquire_certificate.rb +171 -0
- data/lib/bsv/wallet/serializer/certificate.rb +184 -0
- data/lib/bsv/wallet/serializer/common.rb +207 -0
- data/lib/bsv/wallet/serializer/create_action_args.rb +259 -0
- data/lib/bsv/wallet/serializer/create_action_result.rb +85 -0
- data/lib/bsv/wallet/serializer/create_hmac.rb +67 -0
- data/lib/bsv/wallet/serializer/create_signature.rb +90 -0
- data/lib/bsv/wallet/serializer/decrypt.rb +60 -0
- data/lib/bsv/wallet/serializer/discover_by_attributes.rb +61 -0
- data/lib/bsv/wallet/serializer/discover_by_identity_key.rb +49 -0
- data/lib/bsv/wallet/serializer/discover_certificates_result.rb +39 -0
- data/lib/bsv/wallet/serializer/encrypt.rb +60 -0
- data/lib/bsv/wallet/serializer/get_header_for_height.rb +71 -0
- data/lib/bsv/wallet/serializer/get_height.rb +46 -0
- data/lib/bsv/wallet/serializer/get_network.rb +65 -0
- data/lib/bsv/wallet/serializer/get_public_key.rb +86 -0
- data/lib/bsv/wallet/serializer/get_version.rb +44 -0
- data/lib/bsv/wallet/serializer/internalize_action.rb +151 -0
- data/lib/bsv/wallet/serializer/list_actions.rb +348 -0
- data/lib/bsv/wallet/serializer/list_certificates.rb +124 -0
- data/lib/bsv/wallet/serializer/list_outputs.rb +167 -0
- data/lib/bsv/wallet/serializer/prove_certificate.rb +146 -0
- data/lib/bsv/wallet/serializer/relinquish_certificate.rb +56 -0
- data/lib/bsv/wallet/serializer/relinquish_output.rb +44 -0
- data/lib/bsv/wallet/serializer/reveal_counterparty_key_linkage.rb +108 -0
- data/lib/bsv/wallet/serializer/reveal_specific_key_linkage.rb +116 -0
- data/lib/bsv/wallet/serializer/sign_action_args.rb +94 -0
- data/lib/bsv/wallet/serializer/sign_action_result.rb +49 -0
- data/lib/bsv/wallet/serializer/status.rb +85 -0
- data/lib/bsv/wallet/serializer/verify_hmac.rb +67 -0
- data/lib/bsv/wallet/serializer/verify_signature.rb +101 -0
- data/lib/bsv/wallet/serializer.rb +180 -0
- data/lib/bsv/wallet/substrates/http_wallet_json.rb +129 -0
- data/lib/bsv/wallet/substrates/http_wallet_wire.rb +99 -0
- data/lib/bsv/wallet/wallet_wire.rb +20 -0
- data/lib/bsv/wallet/wallet_wire_processor.rb +61 -0
- data/lib/bsv/wallet/wallet_wire_transceiver.rb +61 -0
- data/lib/bsv/wallet/wire/calls.rb +79 -0
- data/lib/bsv/wallet/wire/frame.rb +181 -0
- data/lib/bsv/wallet/wire/reader_writer.rb +402 -0
- data/lib/bsv/wallet/wire/validation.rb +213 -0
- data/lib/bsv/wallet/wire.rb +13 -0
- data/lib/bsv/wallet.rb +17 -0
- metadata +46 -2
- 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
|