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
|
@@ -16,7 +16,7 @@ module BSV
|
|
|
16
16
|
#
|
|
17
17
|
# @example
|
|
18
18
|
# model = BSV::Transaction::FeeModels::LivePolicy.new(
|
|
19
|
-
# arc_url: 'https://
|
|
19
|
+
# arc_url: 'https://arc.taal.com',
|
|
20
20
|
# fallback_rate: 50
|
|
21
21
|
# )
|
|
22
22
|
# fee = model.compute_fee(transaction)
|
|
@@ -34,18 +34,20 @@ module BSV
|
|
|
34
34
|
|
|
35
35
|
DEFAULT_FALLBACK_RATE = 100
|
|
36
36
|
|
|
37
|
-
#
|
|
38
|
-
#
|
|
37
|
+
# TAAL still runs a public ARC instance that serves /v1/policy.
|
|
38
|
+
# Full policy access may require a TAAL API key.
|
|
39
|
+
TAAL_ARC_URL = 'https://arc.taal.com'
|
|
40
|
+
|
|
41
|
+
# Returns a LivePolicy using TAAL ARC for fee policy, 100 sat/kB fallback,
|
|
42
|
+
# and a 5-minute cache.
|
|
39
43
|
#
|
|
40
|
-
# @param api_key [String, nil] optional
|
|
44
|
+
# @param api_key [String, nil] optional TAAL API key for authenticated policy access
|
|
41
45
|
# @return [LivePolicy]
|
|
42
46
|
def self.default(api_key: nil)
|
|
43
|
-
|
|
44
|
-
arc_protocol = provider.protocol_for(:broadcast)
|
|
45
|
-
new(arc_url: arc_protocol.base_url, fallback_rate: DEFAULT_FALLBACK_RATE, api_key: api_key)
|
|
47
|
+
new(arc_url: TAAL_ARC_URL, fallback_rate: DEFAULT_FALLBACK_RATE, api_key: api_key)
|
|
46
48
|
end
|
|
47
49
|
|
|
48
|
-
# @param arc_url [String] ARC base URL (e.g. 'https://
|
|
50
|
+
# @param arc_url [String] ARC base URL (e.g. 'https://arc.taal.com')
|
|
49
51
|
# @param fallback_rate [Integer] sat/kB to use when fetch fails (default: 100)
|
|
50
52
|
# @param cache_ttl [Integer] seconds to cache a fetched rate (default: 300)
|
|
51
53
|
# @param api_key [String, nil] optional Bearer token for ARC authentication
|
data/lib/bsv/version.rb
CHANGED
data/lib/bsv/wallet/errors.rb
CHANGED
|
@@ -3,44 +3,88 @@
|
|
|
3
3
|
module BSV
|
|
4
4
|
module Wallet
|
|
5
5
|
# Base error for all wallet operations. Carries a machine-readable code
|
|
6
|
-
# per the BRC-100 error structure.
|
|
6
|
+
# per the BRC-100 error structure and a wire-protocol stack string.
|
|
7
7
|
class Error < StandardError
|
|
8
|
-
attr_reader :code
|
|
8
|
+
attr_reader :code, :wallet_stack
|
|
9
9
|
|
|
10
|
-
def initialize(message, code
|
|
10
|
+
def initialize(message = nil, code: 1, stack: '')
|
|
11
11
|
@code = code
|
|
12
|
-
|
|
12
|
+
@wallet_stack = stack
|
|
13
|
+
super(message || '')
|
|
13
14
|
end
|
|
14
|
-
end
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
# Serialise to a hash suitable for embedding in a wire result frame.
|
|
17
|
+
# Never leaks Ruby's internal backtrace — only the wallet_stack string.
|
|
18
|
+
def to_wire
|
|
19
|
+
{ code: code, message: message.to_s, stack: wallet_stack.to_s }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
# Code 2 — operation not supported by this wallet implementation.
|
|
24
|
+
class UnsupportedActionError < Error
|
|
25
|
+
def initialize(message = 'this method is not supported by this wallet implementation', stack: '')
|
|
26
|
+
super(message, code: 2, stack: stack)
|
|
23
27
|
end
|
|
24
28
|
end
|
|
25
29
|
|
|
26
|
-
#
|
|
30
|
+
# Code 3 — HMAC verification failed.
|
|
27
31
|
class InvalidHmacError < Error
|
|
28
|
-
def initialize(message = 'the provided HMAC is invalid')
|
|
29
|
-
super(message, 3)
|
|
32
|
+
def initialize(message = 'the provided HMAC is invalid', stack: '')
|
|
33
|
+
super(message, code: 3, stack: stack)
|
|
30
34
|
end
|
|
31
35
|
end
|
|
32
36
|
|
|
33
|
-
#
|
|
37
|
+
# Code 4 — signature verification failed.
|
|
34
38
|
class InvalidSignatureError < Error
|
|
35
|
-
def initialize(message = 'the provided signature is invalid')
|
|
36
|
-
super(message, 4)
|
|
39
|
+
def initialize(message = 'the provided signature is invalid', stack: '')
|
|
40
|
+
super(message, code: 4, stack: stack)
|
|
37
41
|
end
|
|
38
42
|
end
|
|
39
43
|
|
|
40
|
-
#
|
|
41
|
-
class
|
|
42
|
-
def initialize(
|
|
43
|
-
super(
|
|
44
|
+
# Code 5 — wallet has insufficient funds for the requested operation.
|
|
45
|
+
class InsufficientFundsError < Error
|
|
46
|
+
def initialize(message = 'insufficient funds', stack: '')
|
|
47
|
+
super(message, code: 5, stack: stack)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Code 6 — a required parameter is missing or invalid.
|
|
52
|
+
#
|
|
53
|
+
# Two calling conventions:
|
|
54
|
+
# InvalidParameterError.new('pubkey', 'a hex string') # raises "the pubkey parameter must be ..."
|
|
55
|
+
# InvalidParameterError.new('raw message') # wire-rehydration path
|
|
56
|
+
class InvalidParameterError < Error
|
|
57
|
+
attr_reader :parameter
|
|
58
|
+
|
|
59
|
+
def initialize(parameter, must_be = nil, stack: '')
|
|
60
|
+
@parameter = must_be ? parameter : nil
|
|
61
|
+
message = must_be ? "the #{parameter} parameter must be #{must_be}" : parameter
|
|
62
|
+
super(message, code: 6, stack: stack)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Code 7 — actions require review before they can be processed.
|
|
67
|
+
class ReviewActionsError < Error
|
|
68
|
+
def initialize(message = 'actions require review', stack: '')
|
|
69
|
+
super(message, code: 7, stack: stack)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Rehydrate a wire error frame into the appropriate subclass.
|
|
74
|
+
#
|
|
75
|
+
# @param code [Integer] error code byte from the result frame
|
|
76
|
+
# @param message [String] error message from the frame
|
|
77
|
+
# @param stack [String] stack trace from the frame (may be empty)
|
|
78
|
+
# @return [Error] an instance of the matching subclass
|
|
79
|
+
def self.error_from_wire(code, message, stack = '')
|
|
80
|
+
case code
|
|
81
|
+
when 2 then UnsupportedActionError.new(message, stack: stack)
|
|
82
|
+
when 3 then InvalidHmacError.new(message, stack: stack)
|
|
83
|
+
when 4 then InvalidSignatureError.new(message, stack: stack)
|
|
84
|
+
when 5 then InsufficientFundsError.new(message, stack: stack)
|
|
85
|
+
when 6 then InvalidParameterError.new(message, nil, stack: stack)
|
|
86
|
+
when 7 then ReviewActionsError.new(message, stack: stack)
|
|
87
|
+
else Error.new(message, code: code, stack: stack)
|
|
44
88
|
end
|
|
45
89
|
end
|
|
46
90
|
end
|
|
@@ -5,68 +5,26 @@ module BSV
|
|
|
5
5
|
class ProtoWallet
|
|
6
6
|
# Validation helpers for BRC-100 wallet method parameters.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# Delegates to Wire::Validation — the single source of truth for all
|
|
9
|
+
# BRC-100 parameter validation. This module is retained for backwards
|
|
10
|
+
# compatibility with ProtoWallet's internal call sites.
|
|
10
11
|
module Validators
|
|
11
12
|
module_function
|
|
12
13
|
|
|
13
|
-
# Validates a BRC-43 protocol ID.
|
|
14
|
-
#
|
|
15
|
-
# Must be an Array of [Integer(0-2), String(5-400 chars)]. The name is
|
|
16
|
-
# normalized (stripped and downcased) before length/content checks.
|
|
17
|
-
#
|
|
18
|
-
# @param protocol_id [Object] the value to validate
|
|
19
|
-
# @raise [InvalidParameterError]
|
|
20
14
|
def validate_protocol_id!(protocol_id)
|
|
21
|
-
|
|
22
|
-
raise InvalidParameterError.new('protocol_id', 'an Array of [security_level, protocol_name]')
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
level, name = protocol_id
|
|
26
|
-
raise InvalidParameterError.new('protocol_id security level', '0, 1, or 2') unless [0, 1, 2].include?(level)
|
|
27
|
-
raise InvalidParameterError.new('protocol_id name', 'a String') unless name.is_a?(String)
|
|
28
|
-
|
|
29
|
-
name = name.strip.downcase
|
|
30
|
-
max_length = name.start_with?('specific linkage revelation') ? 430 : 400
|
|
31
|
-
raise InvalidParameterError.new('protocol_id name', "between 5 and #{max_length} characters") if name.length < 5 || name.length > max_length
|
|
32
|
-
|
|
33
|
-
raise InvalidParameterError.new('protocol_id name', 'lowercase letters, numbers, and spaces only') unless name.match?(/\A[a-z0-9 ]+\z/)
|
|
34
|
-
|
|
35
|
-
raise InvalidParameterError.new('protocol_id name', 'free of consecutive spaces') if name.include?(' ')
|
|
15
|
+
Wire::Validation.wallet_protocol!('protocol_id', protocol_id)
|
|
36
16
|
end
|
|
37
17
|
|
|
38
|
-
# Validates a BRC-43 key ID.
|
|
39
|
-
#
|
|
40
|
-
# Must be a non-empty String of at most 800 bytes.
|
|
41
|
-
#
|
|
42
|
-
# @param key_id [Object] the value to validate
|
|
43
|
-
# @raise [InvalidParameterError]
|
|
44
18
|
def validate_key_id!(key_id)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
byte_length = key_id.bytesize
|
|
48
|
-
raise InvalidParameterError.new('key_id', 'between 1 and 800 bytes') if byte_length < 1 || byte_length > 800
|
|
19
|
+
Wire::Validation.key_id_string_1_to_800!('key_id', key_id)
|
|
49
20
|
end
|
|
50
21
|
|
|
51
|
-
# Validates a counterparty: 'self', 'anyone', or a 66-char hex pubkey.
|
|
52
|
-
#
|
|
53
|
-
# @param counterparty [Object] the value to validate
|
|
54
|
-
# @raise [InvalidParameterError]
|
|
55
22
|
def validate_counterparty!(counterparty)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
validate_pub_key_hex!(counterparty, 'counterparty')
|
|
23
|
+
Wire::Validation.wallet_counterparty!('counterparty', counterparty)
|
|
59
24
|
end
|
|
60
25
|
|
|
61
|
-
# Validates a compressed public key in hex form (66 chars, 02/03/04 prefix).
|
|
62
|
-
#
|
|
63
|
-
# @param value [Object] the value to validate
|
|
64
|
-
# @param name [String] parameter name for error messages
|
|
65
|
-
# @raise [InvalidParameterError]
|
|
66
26
|
def validate_pub_key_hex!(value, name = 'public_key')
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
raise InvalidParameterError.new(name, 'a 66-character hex string (compressed public key)') unless value.match?(/\A[0-9a-f]{66}\z/)
|
|
27
|
+
Wire::Validation.pub_key_hex!(name, value)
|
|
70
28
|
end
|
|
71
29
|
end
|
|
72
30
|
end
|
|
@@ -226,6 +226,8 @@ module BSV
|
|
|
226
226
|
def reveal_counterparty_key_linkage(counterparty:, verifier:,
|
|
227
227
|
privileged: false, privileged_reason: nil,
|
|
228
228
|
originator: nil)
|
|
229
|
+
counterparty = normalise_pubkey_hex(counterparty)
|
|
230
|
+
verifier = normalise_pubkey_hex(verifier)
|
|
229
231
|
raise InvalidParameterError.new('counterparty', 'a specific public key hex, not "anyone"') if counterparty == 'anyone'
|
|
230
232
|
|
|
231
233
|
Validators.validate_pub_key_hex!(verifier, 'verifier')
|
|
@@ -281,6 +283,8 @@ module BSV
|
|
|
281
283
|
def reveal_specific_key_linkage(counterparty:, verifier:, protocol_id:, key_id:,
|
|
282
284
|
privileged: false, privileged_reason: nil,
|
|
283
285
|
originator: nil)
|
|
286
|
+
counterparty = normalise_pubkey_hex(counterparty)
|
|
287
|
+
verifier = normalise_pubkey_hex(verifier)
|
|
284
288
|
raise InvalidParameterError.new('counterparty', 'a specific public key hex, not "anyone"') if counterparty == 'anyone'
|
|
285
289
|
|
|
286
290
|
Validators.validate_pub_key_hex!(verifier, 'verifier')
|
|
@@ -340,7 +344,16 @@ module BSV
|
|
|
340
344
|
end
|
|
341
345
|
|
|
342
346
|
def bytes_to_string(bytes)
|
|
343
|
-
bytes.pack('C*')
|
|
347
|
+
bytes.is_a?(String) ? bytes.b : bytes.pack('C*')
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Normalise a public key argument to a 66-character compressed hex string.
|
|
351
|
+
# Accepts either a 33-byte binary string or a 66-character hex string.
|
|
352
|
+
def normalise_pubkey_hex(value)
|
|
353
|
+
return value if value.is_a?(String) && value.length == 66
|
|
354
|
+
return value.unpack1('H*') if value.is_a?(String) && value.bytesize == 33
|
|
355
|
+
|
|
356
|
+
value
|
|
344
357
|
end
|
|
345
358
|
|
|
346
359
|
def string_to_bytes(str)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
module Serializer
|
|
6
|
+
# BRC-103 wire codec for the +abort_action+ call (call byte 3).
|
|
7
|
+
#
|
|
8
|
+
# Args wire layout:
|
|
9
|
+
# [remaining bytes: reference (raw binary)]
|
|
10
|
+
#
|
|
11
|
+
# Result wire layout:
|
|
12
|
+
# [empty — success is implicit from the frame error byte]
|
|
13
|
+
module AbortAction
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def serialize_args(args)
|
|
17
|
+
ref = args[:reference]
|
|
18
|
+
return ''.b if ref.nil? || ref.empty?
|
|
19
|
+
|
|
20
|
+
ref.b
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def deserialize_args(bytes)
|
|
24
|
+
ref = bytes.b
|
|
25
|
+
{ reference: ref.empty? ? nil : ref }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def serialize_result(_result)
|
|
29
|
+
''.b
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def deserialize_result(_bytes)
|
|
33
|
+
{ aborted: true }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
|
|
5
|
+
module BSV
|
|
6
|
+
module Wallet
|
|
7
|
+
module Serializer
|
|
8
|
+
# BRC-103 wire codec for the +acquire_certificate+ call (call byte 17).
|
|
9
|
+
#
|
|
10
|
+
# Args wire layout:
|
|
11
|
+
# [32 bytes: type]
|
|
12
|
+
# [33 bytes: certifier pubkey]
|
|
13
|
+
# [varint: field_count] per field: [varint-str key][varint-str value]
|
|
14
|
+
# [privileged params]
|
|
15
|
+
# [1 byte: acquisition_protocol] 1=direct, 2=issuance
|
|
16
|
+
# If direct:
|
|
17
|
+
# [32 bytes: serial_number]
|
|
18
|
+
# [36 bytes: revocation_outpoint]
|
|
19
|
+
# [varint: sig_len][sig bytes]
|
|
20
|
+
# [keyring_revealer: 0x0B or 33-byte pubkey]
|
|
21
|
+
# [varint: keyring_count] per entry: [varint-str key][varint-int base64_value]
|
|
22
|
+
# If issuance:
|
|
23
|
+
# [varint-str: certifier_url]
|
|
24
|
+
#
|
|
25
|
+
# Result wire layout:
|
|
26
|
+
# [inline Certificate bytes (type+serial+subject+certifier+outpoint+fields+sig)]
|
|
27
|
+
module AcquireCertificate
|
|
28
|
+
ACQUISITION_DIRECT = 1
|
|
29
|
+
ACQUISITION_ISSUANCE = 2
|
|
30
|
+
KEYRING_REVEALER_CERTIFIER = 11 # 0x0B — matches Go keyRingRevealerCertifier
|
|
31
|
+
|
|
32
|
+
CERT_TYPE_SIZE = 32
|
|
33
|
+
SERIAL_SIZE = 32
|
|
34
|
+
PUBKEY_SIZE = 33
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
def serialize_args(args)
|
|
39
|
+
w = Wire::Writer.new
|
|
40
|
+
|
|
41
|
+
type_bytes = Base64.strict_decode64(args[:type].to_s)
|
|
42
|
+
w.write_bytes(type_bytes.ljust(CERT_TYPE_SIZE, "\x00").byteslice(0, CERT_TYPE_SIZE))
|
|
43
|
+
w.write_bytes([args[:certifier].to_s].pack('H*'))
|
|
44
|
+
|
|
45
|
+
fields = args[:fields] || {}
|
|
46
|
+
w.write_varint(fields.length)
|
|
47
|
+
fields.keys.sort.each do |k|
|
|
48
|
+
w.write_str_with_varint_len(k)
|
|
49
|
+
w.write_str_with_varint_len(fields[k].to_s)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
Common.write_privileged_params(w, args[:privileged], args[:privileged_reason])
|
|
53
|
+
|
|
54
|
+
case args[:acquisition_protocol]
|
|
55
|
+
when :direct
|
|
56
|
+
w.write_byte(ACQUISITION_DIRECT)
|
|
57
|
+
serial_bytes = Base64.strict_decode64(args[:serial_number].to_s)
|
|
58
|
+
w.write_bytes(serial_bytes.ljust(SERIAL_SIZE, "\x00").byteslice(0, SERIAL_SIZE))
|
|
59
|
+
|
|
60
|
+
outpoint_str = args[:revocation_outpoint].to_s
|
|
61
|
+
txid_hex, vout = outpoint_str.split('.', 2)
|
|
62
|
+
w.write_outpoint(txid_hex.to_s, vout.to_i)
|
|
63
|
+
|
|
64
|
+
sig = args[:signature]
|
|
65
|
+
if sig && !sig.to_s.empty?
|
|
66
|
+
sig_bytes = [sig.to_s].pack('H*')
|
|
67
|
+
w.write_int_bytes(sig_bytes)
|
|
68
|
+
else
|
|
69
|
+
w.write_varint(0)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
revealer = args[:keyring_revealer]
|
|
73
|
+
if revealer == :certifier || (revealer.is_a?(Hash) && revealer[:certifier])
|
|
74
|
+
w.write_byte(KEYRING_REVEALER_CERTIFIER)
|
|
75
|
+
else
|
|
76
|
+
pubkey_hex = revealer.is_a?(Hash) ? revealer[:pub_key].to_s : revealer.to_s
|
|
77
|
+
w.write_bytes([pubkey_hex].pack('H*'))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
keyring = args[:keyring_for_subject] || {}
|
|
81
|
+
w.write_varint(keyring.length)
|
|
82
|
+
keyring.keys.sort.each do |k|
|
|
83
|
+
w.write_str_with_varint_len(k)
|
|
84
|
+
w.write_int_from_base64(keyring[k].to_s)
|
|
85
|
+
end
|
|
86
|
+
when :issuance
|
|
87
|
+
w.write_byte(ACQUISITION_ISSUANCE)
|
|
88
|
+
w.write_str_with_varint_len(args[:certifier_url].to_s)
|
|
89
|
+
else
|
|
90
|
+
raise ArgumentError, "invalid acquisition_protocol: #{args[:acquisition_protocol].inspect}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
w.buf
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def deserialize_args(bytes)
|
|
97
|
+
r = Wire::Reader.new(bytes)
|
|
98
|
+
|
|
99
|
+
type_raw = r.read_bytes(CERT_TYPE_SIZE)
|
|
100
|
+
certifier = r.read_bytes(PUBKEY_SIZE).unpack1('H*')
|
|
101
|
+
|
|
102
|
+
field_count = r.read_varint
|
|
103
|
+
fields = {}
|
|
104
|
+
field_count.times do
|
|
105
|
+
k = r.read_str_with_varint_len
|
|
106
|
+
v = r.read_str_with_varint_len
|
|
107
|
+
fields[k] = v
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
privileged, privileged_reason = Common.read_privileged_params(r)
|
|
111
|
+
|
|
112
|
+
protocol_byte = r.read_byte
|
|
113
|
+
acquisition_protocol = case protocol_byte
|
|
114
|
+
when ACQUISITION_DIRECT then :direct
|
|
115
|
+
when ACQUISITION_ISSUANCE then :issuance
|
|
116
|
+
else raise ArgumentError, "invalid acquisition protocol byte: #{protocol_byte}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
result = {
|
|
120
|
+
type: Base64.strict_encode64(type_raw),
|
|
121
|
+
certifier: certifier,
|
|
122
|
+
fields: fields,
|
|
123
|
+
privileged: privileged,
|
|
124
|
+
privileged_reason: privileged_reason,
|
|
125
|
+
acquisition_protocol: acquisition_protocol
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if acquisition_protocol == :direct
|
|
129
|
+
serial_raw = r.read_bytes(SERIAL_SIZE)
|
|
130
|
+
result[:serial_number] = Base64.strict_encode64(serial_raw)
|
|
131
|
+
|
|
132
|
+
outpoint_data = r.read_outpoint
|
|
133
|
+
result[:revocation_outpoint] = "#{outpoint_data[:txid_hex]}.#{outpoint_data[:vout]}"
|
|
134
|
+
|
|
135
|
+
sig_len = r.read_varint
|
|
136
|
+
result[:signature] = sig_len.positive? ? r.read_bytes(sig_len).unpack1('H*') : nil
|
|
137
|
+
|
|
138
|
+
revealer_byte = r.read_byte
|
|
139
|
+
if revealer_byte == KEYRING_REVEALER_CERTIFIER
|
|
140
|
+
result[:keyring_revealer] = { certifier: true }
|
|
141
|
+
else
|
|
142
|
+
rest = r.read_bytes(PUBKEY_SIZE - 1)
|
|
143
|
+
result[:keyring_revealer] = { pub_key: ([revealer_byte].pack('C') + rest).unpack1('H*') }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
keyring_count = r.read_varint
|
|
147
|
+
keyring = {}
|
|
148
|
+
keyring_count.times do
|
|
149
|
+
k = r.read_str_with_varint_len
|
|
150
|
+
keyring[k] = r.read_base64_int
|
|
151
|
+
end
|
|
152
|
+
result[:keyring_for_subject] = keyring
|
|
153
|
+
else
|
|
154
|
+
result[:certifier_url] = r.read_str_with_varint_len
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
result
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def serialize_result(result)
|
|
161
|
+
Certificate.serialize_certificate(result[:certificate] || result, include_signature: true)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def deserialize_result(bytes)
|
|
165
|
+
cert = Certificate.deserialize_certificate(bytes)
|
|
166
|
+
{ certificate: cert }
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
|
|
5
|
+
module BSV
|
|
6
|
+
module Wallet
|
|
7
|
+
module Serializer
|
|
8
|
+
# Shared BRC-103 wire codec for Certificate and IdentityCertificate.
|
|
9
|
+
#
|
|
10
|
+
# Certificate wire layout (matches go-sdk serializeCertificate):
|
|
11
|
+
# [32 bytes: type (raw bytes decoded from Base64)]
|
|
12
|
+
# [32 bytes: serial_number (raw bytes decoded from Base64)]
|
|
13
|
+
# [33 bytes: subject compressed pubkey]
|
|
14
|
+
# [33 bytes: certifier compressed pubkey]
|
|
15
|
+
# [32 bytes + varint: revocation_outpoint (display-order txid + varint vout)]
|
|
16
|
+
# [varint: field_count]
|
|
17
|
+
# per field: [varint-len name][varint-len value]
|
|
18
|
+
# [remaining: DER signature bytes (absent if no signature)]
|
|
19
|
+
#
|
|
20
|
+
# IdentityCertificate additionally appends:
|
|
21
|
+
# [varint-int: serialised Certificate bytes (int-prefixed)]
|
|
22
|
+
# [varint-str: certifier_info.name]
|
|
23
|
+
# [varint-str: certifier_info.icon_url]
|
|
24
|
+
# [varint-str: certifier_info.description]
|
|
25
|
+
# [1 byte: certifier_info.trust]
|
|
26
|
+
# [varint: keyring_count] per entry: [varint-str key][varint-int raw_bytes]
|
|
27
|
+
# [varint: decrypted_fields_count] per entry: [varint-str key][varint-str value]
|
|
28
|
+
module Certificate
|
|
29
|
+
CERT_TYPE_SIZE = 32
|
|
30
|
+
SERIAL_SIZE = 32
|
|
31
|
+
PUBKEY_SIZE = 33
|
|
32
|
+
|
|
33
|
+
# NULL outpoint used when revocation_outpoint is nil.
|
|
34
|
+
NULL_TXID_HEX = '00' * 32
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
# Serialise a certificate Hash to binary.
|
|
39
|
+
#
|
|
40
|
+
# @param cert [Hash] with keys: :type (Base64), :serial_number (Base64),
|
|
41
|
+
# :subject (hex pubkey), :certifier (hex pubkey),
|
|
42
|
+
# :revocation_outpoint (String "txid.vout" or nil),
|
|
43
|
+
# :fields (Hash<String,String>), :signature (hex bytes or nil)
|
|
44
|
+
# @param include_signature [Boolean] whether to append signature bytes
|
|
45
|
+
# @return [String] binary
|
|
46
|
+
def serialize_certificate(cert, include_signature: true)
|
|
47
|
+
w = Wire::Writer.new
|
|
48
|
+
|
|
49
|
+
type_bytes = Base64.strict_decode64(cert[:type].to_s)
|
|
50
|
+
serial_bytes = Base64.strict_decode64(cert[:serial_number].to_s)
|
|
51
|
+
|
|
52
|
+
w.write_bytes(type_bytes.ljust(CERT_TYPE_SIZE, "\x00").byteslice(0, CERT_TYPE_SIZE))
|
|
53
|
+
w.write_bytes(serial_bytes.ljust(SERIAL_SIZE, "\x00").byteslice(0, SERIAL_SIZE))
|
|
54
|
+
w.write_bytes([cert[:subject].to_s].pack('H*'))
|
|
55
|
+
w.write_bytes([cert[:certifier].to_s].pack('H*'))
|
|
56
|
+
|
|
57
|
+
outpoint_str = cert[:revocation_outpoint].to_s
|
|
58
|
+
if outpoint_str.empty? || outpoint_str == '.'
|
|
59
|
+
w.write_outpoint(NULL_TXID_HEX, 0)
|
|
60
|
+
else
|
|
61
|
+
txid_hex, vout = outpoint_str.split('.', 2)
|
|
62
|
+
w.write_outpoint(txid_hex.to_s, vout.to_i)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
fields = cert[:fields] || {}
|
|
66
|
+
w.write_varint(fields.length)
|
|
67
|
+
fields.keys.sort.each do |name|
|
|
68
|
+
w.write_str_with_varint_len(name)
|
|
69
|
+
w.write_str_with_varint_len(fields[name].to_s)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
w.write_bytes([cert[:signature].to_s].pack('H*')) if include_signature && cert[:signature] && !cert[:signature].empty?
|
|
73
|
+
|
|
74
|
+
w.buf
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Deserialise a certificate from binary.
|
|
78
|
+
#
|
|
79
|
+
# @param bytes [String] binary
|
|
80
|
+
# @return [Hash]
|
|
81
|
+
def deserialize_certificate(bytes)
|
|
82
|
+
r = Wire::Reader.new(bytes)
|
|
83
|
+
type_raw = r.read_bytes(CERT_TYPE_SIZE)
|
|
84
|
+
serial_raw = r.read_bytes(SERIAL_SIZE)
|
|
85
|
+
subject = r.read_bytes(PUBKEY_SIZE).unpack1('H*')
|
|
86
|
+
certifier = r.read_bytes(PUBKEY_SIZE).unpack1('H*')
|
|
87
|
+
|
|
88
|
+
outpoint_data = r.read_outpoint
|
|
89
|
+
revocation_outpoint = "#{outpoint_data[:txid_hex]}.#{outpoint_data[:vout]}"
|
|
90
|
+
|
|
91
|
+
field_count = r.read_varint
|
|
92
|
+
fields = {}
|
|
93
|
+
field_count.times do
|
|
94
|
+
name = r.read_str_with_varint_len
|
|
95
|
+
value = r.read_str_with_varint_len
|
|
96
|
+
fields[name] = value
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
sig_bytes = r.read_remaining
|
|
100
|
+
signature = sig_bytes.empty? ? nil : sig_bytes.unpack1('H*')
|
|
101
|
+
|
|
102
|
+
{
|
|
103
|
+
type: Base64.strict_encode64(type_raw),
|
|
104
|
+
serial_number: Base64.strict_encode64(serial_raw),
|
|
105
|
+
subject: subject,
|
|
106
|
+
certifier: certifier,
|
|
107
|
+
revocation_outpoint: revocation_outpoint,
|
|
108
|
+
fields: fields,
|
|
109
|
+
signature: signature
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Serialise an IdentityCertificate (used by discover_* result).
|
|
114
|
+
#
|
|
115
|
+
# @param cert [Hash] all Certificate fields plus:
|
|
116
|
+
# :certifier_info ({ name:, icon_url:, description:, trust: })
|
|
117
|
+
# :publicly_revealed_keyring (Hash<String,Base64>)
|
|
118
|
+
# :decrypted_fields (Hash<String,String>)
|
|
119
|
+
def serialize_identity_certificate(cert)
|
|
120
|
+
w = Wire::Writer.new
|
|
121
|
+
|
|
122
|
+
cert_bytes = serialize_certificate(cert, include_signature: true)
|
|
123
|
+
w.write_int_bytes(cert_bytes)
|
|
124
|
+
|
|
125
|
+
info = cert[:certifier_info] || {}
|
|
126
|
+
w.write_str_with_varint_len(info[:name].to_s)
|
|
127
|
+
w.write_str_with_varint_len(info[:icon_url].to_s)
|
|
128
|
+
w.write_str_with_varint_len(info[:description].to_s)
|
|
129
|
+
w.write_byte((info[:trust] || 0).to_i & 0xFF)
|
|
130
|
+
|
|
131
|
+
keyring = cert[:publicly_revealed_keyring] || {}
|
|
132
|
+
w.write_varint(keyring.length)
|
|
133
|
+
keyring.keys.sort.each do |k|
|
|
134
|
+
w.write_str_with_varint_len(k)
|
|
135
|
+
w.write_int_from_base64(keyring[k].to_s)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
dec_fields = cert[:decrypted_fields] || {}
|
|
139
|
+
w.write_varint(dec_fields.length)
|
|
140
|
+
dec_fields.keys.sort.each do |k|
|
|
141
|
+
w.write_str_with_varint_len(k)
|
|
142
|
+
w.write_str_with_varint_len(dec_fields[k].to_s)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
w.buf
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Deserialise an IdentityCertificate from a Reader (reads inline, not length-prefixed).
|
|
149
|
+
#
|
|
150
|
+
# @param reader [Wire::Reader]
|
|
151
|
+
# @return [Hash]
|
|
152
|
+
def deserialize_identity_certificate(reader)
|
|
153
|
+
cert_bytes = reader.read_int_bytes
|
|
154
|
+
cert = deserialize_certificate(cert_bytes)
|
|
155
|
+
|
|
156
|
+
cert[:certifier_info] = {
|
|
157
|
+
name: reader.read_str_with_varint_len,
|
|
158
|
+
icon_url: reader.read_str_with_varint_len,
|
|
159
|
+
description: reader.read_str_with_varint_len,
|
|
160
|
+
trust: reader.read_byte
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
keyring_len = reader.read_varint
|
|
164
|
+
keyring = {}
|
|
165
|
+
keyring_len.times do
|
|
166
|
+
k = reader.read_str_with_varint_len
|
|
167
|
+
keyring[k] = reader.read_base64_int
|
|
168
|
+
end
|
|
169
|
+
cert[:publicly_revealed_keyring] = keyring
|
|
170
|
+
|
|
171
|
+
dec_len = reader.read_varint
|
|
172
|
+
dec_fields = {}
|
|
173
|
+
dec_len.times do
|
|
174
|
+
k = reader.read_str_with_varint_len
|
|
175
|
+
dec_fields[k] = reader.read_str_with_varint_len
|
|
176
|
+
end
|
|
177
|
+
cert[:decrypted_fields] = dec_fields
|
|
178
|
+
|
|
179
|
+
cert
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|