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
@@ -16,7 +16,7 @@ module BSV
16
16
  #
17
17
  # @example
18
18
  # model = BSV::Transaction::FeeModels::LivePolicy.new(
19
- # arc_url: 'https://arcade.gorillapool.io',
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
- # Returns a LivePolicy with sensible defaults (GorillaPool ARC,
38
- # 100 sat/kB fallback, 5-minute cache).
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 ARC API key
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
- provider = BSV::Network::Providers::GorillaPool.mainnet
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://arcade.gorillapool.io')
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BSV
4
- VERSION = '0.20.0'
4
+ VERSION = '0.22.0'
5
5
  end
@@ -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 = 1)
10
+ def initialize(message = nil, code: 1, stack: '')
11
11
  @code = code
12
- super(message)
12
+ @wallet_stack = stack
13
+ super(message || '')
13
14
  end
14
- end
15
15
 
16
- # Raised when a required parameter is missing or invalid.
17
- class InvalidParameterError < Error
18
- attr_reader :parameter
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
- def initialize(parameter, must_be = 'valid')
21
- @parameter = parameter
22
- super("the #{parameter} parameter must be #{must_be}", 6)
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
- # Raised when an HMAC fails to verify.
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
- # Raised when a signature fails to verify.
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
- # Raised when an operation is not supported by this wallet implementation.
41
- class UnsupportedActionError < Error
42
- def initialize(method_name = 'this method')
43
- super("#{method_name} is not supported by this wallet implementation", 2)
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
- # Provides the subset of validators required by KeyDeriver and ProtoWallet.
9
- # Raises +InvalidParameterError+ for any invalid input.
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
- unless protocol_id.is_a?(Array) && protocol_id.length == 2
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
- raise InvalidParameterError.new('key_id', 'a String') unless key_id.is_a?(String)
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
- return if %w[self anyone].include?(counterparty)
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
- raise InvalidParameterError.new(name, 'a String') unless value.is_a?(String)
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