bsv-sdk 0.19.1 → 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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +89 -0
  3. data/README.md +2 -2
  4. data/lib/bsv/auth/transport.rb +1 -1
  5. data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +5 -3
  6. data/lib/bsv/network/protocol.rb +4 -5
  7. data/lib/bsv/network/protocols/arc.rb +4 -30
  8. data/lib/bsv/network/protocols/arcade.rb +163 -0
  9. data/lib/bsv/network/protocols/chaintracks.rb +6 -3
  10. data/lib/bsv/network/protocols/jungle_bus.rb +6 -0
  11. data/lib/bsv/network/protocols.rb +1 -0
  12. data/lib/bsv/network/provider.rb +7 -9
  13. data/lib/bsv/network/providers/gorilla_pool.rb +18 -18
  14. data/lib/bsv/network/util.rb +44 -0
  15. data/lib/bsv/network.rb +1 -0
  16. data/lib/bsv/overlay/lookup_resolver.rb +0 -1
  17. data/lib/bsv/overlay/topic_broadcaster.rb +0 -1
  18. data/lib/bsv/primitives/curve.rb +1 -11
  19. data/lib/bsv/primitives/ecies.rb +1 -8
  20. data/lib/bsv/primitives/hex.rb +1 -1
  21. data/lib/bsv/script/script.rb +1 -1
  22. data/lib/bsv/transaction/beef.rb +0 -2
  23. data/lib/bsv/transaction/chain_tracker.rb +74 -13
  24. data/lib/bsv/transaction/chain_trackers/whats_on_chain.rb +3 -3
  25. data/lib/bsv/transaction/chain_trackers.rb +0 -10
  26. data/lib/bsv/transaction/fee_models/live_policy.rb +10 -8
  27. data/lib/bsv/transaction/merkle_path.rb +0 -2
  28. data/lib/bsv/version.rb +1 -1
  29. data/lib/bsv/wallet/errors.rb +65 -21
  30. data/lib/bsv/wallet/proto_wallet/validators.rb +7 -49
  31. data/lib/bsv/wallet/proto_wallet.rb +15 -8
  32. data/lib/bsv/wallet/serializer/abort_action.rb +38 -0
  33. data/lib/bsv/wallet/serializer/acquire_certificate.rb +171 -0
  34. data/lib/bsv/wallet/serializer/certificate.rb +184 -0
  35. data/lib/bsv/wallet/serializer/common.rb +207 -0
  36. data/lib/bsv/wallet/serializer/create_action_args.rb +259 -0
  37. data/lib/bsv/wallet/serializer/create_action_result.rb +85 -0
  38. data/lib/bsv/wallet/serializer/create_hmac.rb +67 -0
  39. data/lib/bsv/wallet/serializer/create_signature.rb +90 -0
  40. data/lib/bsv/wallet/serializer/decrypt.rb +60 -0
  41. data/lib/bsv/wallet/serializer/discover_by_attributes.rb +61 -0
  42. data/lib/bsv/wallet/serializer/discover_by_identity_key.rb +49 -0
  43. data/lib/bsv/wallet/serializer/discover_certificates_result.rb +39 -0
  44. data/lib/bsv/wallet/serializer/encrypt.rb +60 -0
  45. data/lib/bsv/wallet/serializer/get_header_for_height.rb +71 -0
  46. data/lib/bsv/wallet/serializer/get_height.rb +46 -0
  47. data/lib/bsv/wallet/serializer/get_network.rb +65 -0
  48. data/lib/bsv/wallet/serializer/get_public_key.rb +86 -0
  49. data/lib/bsv/wallet/serializer/get_version.rb +44 -0
  50. data/lib/bsv/wallet/serializer/internalize_action.rb +151 -0
  51. data/lib/bsv/wallet/serializer/list_actions.rb +348 -0
  52. data/lib/bsv/wallet/serializer/list_certificates.rb +124 -0
  53. data/lib/bsv/wallet/serializer/list_outputs.rb +167 -0
  54. data/lib/bsv/wallet/serializer/prove_certificate.rb +146 -0
  55. data/lib/bsv/wallet/serializer/relinquish_certificate.rb +56 -0
  56. data/lib/bsv/wallet/serializer/relinquish_output.rb +44 -0
  57. data/lib/bsv/wallet/serializer/reveal_counterparty_key_linkage.rb +108 -0
  58. data/lib/bsv/wallet/serializer/reveal_specific_key_linkage.rb +116 -0
  59. data/lib/bsv/wallet/serializer/sign_action_args.rb +94 -0
  60. data/lib/bsv/wallet/serializer/sign_action_result.rb +49 -0
  61. data/lib/bsv/wallet/serializer/status.rb +85 -0
  62. data/lib/bsv/wallet/serializer/verify_hmac.rb +67 -0
  63. data/lib/bsv/wallet/serializer/verify_signature.rb +101 -0
  64. data/lib/bsv/wallet/serializer.rb +180 -0
  65. data/lib/bsv/wallet/substrates/http_wallet_json.rb +129 -0
  66. data/lib/bsv/wallet/substrates/http_wallet_wire.rb +99 -0
  67. data/lib/bsv/wallet/wallet_wire.rb +20 -0
  68. data/lib/bsv/wallet/wallet_wire_processor.rb +61 -0
  69. data/lib/bsv/wallet/wallet_wire_transceiver.rb +61 -0
  70. data/lib/bsv/wallet/wire/calls.rb +79 -0
  71. data/lib/bsv/wallet/wire/frame.rb +181 -0
  72. data/lib/bsv/wallet/wire/reader_writer.rb +402 -0
  73. data/lib/bsv/wallet/wire/validation.rb +213 -0
  74. data/lib/bsv/wallet/wire.rb +13 -0
  75. data/lib/bsv/wallet.rb +17 -0
  76. metadata +47 -3
  77. data/lib/bsv/transaction/chain_trackers/chaintracks.rb +0 -83
@@ -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
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ module Serializer
6
+ # Shared binary encoding helpers for BRC-103 per-call serializers.
7
+ #
8
+ # Port of the shared helpers in go-sdk/wallet/serializer/serializer.go.
9
+ # All methods operate on BSV::Wallet::Wire::Writer / Reader instances.
10
+ module Common
11
+ COUNTERPARTY_SELF = 11 # 0x0B
12
+ COUNTERPARTY_ANYONE = 12 # 0x0C
13
+ PUBKEY_SIZE = 33 # compressed secp256k1 public key
14
+
15
+ module_function
16
+
17
+ # Coerce a byte payload to a binary String (ASCII-8BIT encoding).
18
+ #
19
+ # Accepts either an Array<Integer> (as returned by ProtoWallet) or a
20
+ # String. Serialisers use this so they remain compatible with both the
21
+ # in-process wallet interface (Arrays) and the wire interface (Strings).
22
+ def to_binary(bytes)
23
+ return ''.b if bytes.nil?
24
+ return bytes.pack('C*').b if bytes.is_a?(Array)
25
+
26
+ bytes.b
27
+ end
28
+
29
+ # Encode a BRC-43 protocol ID: [security_level (0-2), protocol_name].
30
+ #
31
+ # Wire format: 1-byte security level + varint-len string.
32
+ def write_protocol(writer, protocol_id)
33
+ level, name = protocol_id
34
+ writer.write_byte(level.to_i)
35
+ writer.write_str_with_varint_len(name.to_s)
36
+ end
37
+
38
+ def read_protocol(reader)
39
+ level = reader.read_byte
40
+ name = reader.read_str_with_varint_len
41
+ [level, name]
42
+ end
43
+
44
+ # Encode a counterparty: 'self' | 'anyone' | 66-char hex compressed pubkey.
45
+ #
46
+ # Wire format:
47
+ # 0x0B — self
48
+ # 0x0C — anyone
49
+ # 02/03 <32 bytes> — specific pubkey (first byte is the prefix of the 33-byte key)
50
+ def write_counterparty(writer, counterparty)
51
+ case counterparty
52
+ when 'self'
53
+ writer.write_byte(COUNTERPARTY_SELF)
54
+ when 'anyone'
55
+ writer.write_byte(COUNTERPARTY_ANYONE)
56
+ else
57
+ writer.write_bytes([counterparty].pack('H*'))
58
+ end
59
+ end
60
+
61
+ def read_counterparty(reader)
62
+ first = reader.read_byte
63
+ case first
64
+ when COUNTERPARTY_SELF
65
+ 'self'
66
+ when COUNTERPARTY_ANYONE
67
+ 'anyone'
68
+ else
69
+ rest = reader.read_bytes(PUBKEY_SIZE - 1)
70
+ ([first].pack('C') + rest).unpack1('H*')
71
+ end
72
+ end
73
+
74
+ # Encode privileged flag + privileged_reason.
75
+ #
76
+ # Wire format: optional_bool (0xFF=nil, 0x00=false, 0x01=true) + reason as varint-len string,
77
+ # or 0xFF if reason is nil/empty (NegativeOneByte sentinel).
78
+ def write_privileged_params(writer, privileged, privileged_reason)
79
+ writer.write_optional_bool(privileged)
80
+ reason = privileged_reason.to_s
81
+ if reason.empty?
82
+ writer.write_byte(0xFF)
83
+ else
84
+ writer.write_str_with_varint_len(reason)
85
+ end
86
+ end
87
+
88
+ def read_privileged_params(reader)
89
+ privileged = reader.read_optional_bool
90
+ # 0xFF as the leading byte is the nil-reason sentinel. Otherwise the
91
+ # bytes start a Bitcoin varint length prefix (which can be 0xFD or
92
+ # 0xFE for reasons >= 253 bytes; 0xFF would only collide if the
93
+ # reason exceeded 4 GiB, which the protocol does not permit).
94
+ if reader.peek_byte == 0xFF
95
+ reader.read_byte
96
+ [privileged, nil]
97
+ else
98
+ [privileged, reader.read_str_with_varint_len]
99
+ end
100
+ end
101
+
102
+ # Encode key-related params: protocol + key_id + counterparty + privileged params.
103
+ def write_key_related_params(writer, protocol_id:, key_id:, counterparty:,
104
+ privileged: nil, privileged_reason: nil)
105
+ write_protocol(writer, protocol_id)
106
+ writer.write_str_with_varint_len(key_id.to_s)
107
+ write_counterparty(writer, counterparty)
108
+ write_privileged_params(writer, privileged, privileged_reason)
109
+ end
110
+
111
+ def read_key_related_params(reader)
112
+ protocol_id = read_protocol(reader)
113
+ key_id = reader.read_str_with_varint_len
114
+ counterparty = read_counterparty(reader)
115
+ privileged, reason = read_privileged_params(reader)
116
+ {
117
+ protocol_id: protocol_id,
118
+ key_id: key_id,
119
+ counterparty: counterparty,
120
+ privileged: privileged,
121
+ privileged_reason: reason
122
+ }
123
+ end
124
+
125
+ # Encode a list of outpoints (varint count + 32-byte wire-order txid + varint vout each).
126
+ # Returns nil bytes for nil input.
127
+ # @param outpoints [Array<String>, nil] array of "txid_hex.vout" strings
128
+ # @return [String, nil] binary or nil
129
+ def encode_outpoints(outpoints)
130
+ return nil if outpoints.nil?
131
+
132
+ w = Wire::Writer.new
133
+ w.write_varint(outpoints.length)
134
+ outpoints.each do |op|
135
+ txid_hex, vout = op.split('.')
136
+ w.write_bytes([txid_hex].pack('H*'))
137
+ w.write_varint(vout.to_i)
138
+ end
139
+ w.buf
140
+ end
141
+
142
+ # Decode a list of outpoints from binary (encoded as encode_outpoints).
143
+ # @param bytes [String, nil] binary data
144
+ # @return [Array<String>, nil] array of "txid_hex.vout" strings
145
+ def decode_outpoints(bytes)
146
+ return nil if bytes.nil? || bytes.b.empty?
147
+
148
+ r = Wire::Reader.new(bytes)
149
+ count = r.read_varint
150
+ return nil if count == 0xFFFF_FFFF_FFFF_FFFF
151
+
152
+ count.times.map do
153
+ txid_hex = r.read_bytes(32).unpack1('H*')
154
+ vout = r.read_varint
155
+ "#{txid_hex}.#{vout}"
156
+ end
157
+ end
158
+
159
+ # Send-with result status codes (Go status.go).
160
+ SEND_WITH_STATUS_UNPROVEN = 1
161
+ SEND_WITH_STATUS_SENDING = 2
162
+ SEND_WITH_STATUS_FAILED = 3
163
+
164
+ SEND_WITH_STATUS_CODES = {
165
+ unproven: SEND_WITH_STATUS_UNPROVEN,
166
+ sending: SEND_WITH_STATUS_SENDING,
167
+ failed: SEND_WITH_STATUS_FAILED
168
+ }.freeze
169
+
170
+ SEND_WITH_CODE_STATUSES = SEND_WITH_STATUS_CODES.invert.freeze
171
+
172
+ # Write a send_with_results array: varint count + txid (32 bytes) + status byte each.
173
+ # nil or empty → writes 0 count.
174
+ # @param writer [Wire::Writer]
175
+ # @param results [Array<Hash>, nil] array of { txid: String, status: Symbol }
176
+ def write_send_with_results(writer, results)
177
+ arr = results || []
178
+ writer.write_varint(arr.length)
179
+ arr.each do |res|
180
+ writer.write_bytes([res[:txid]].pack('H*'))
181
+ code = SEND_WITH_STATUS_CODES.fetch(res[:status]) do
182
+ raise ArgumentError, "invalid send_with status: #{res[:status]}"
183
+ end
184
+ writer.write_byte(code)
185
+ end
186
+ end
187
+
188
+ # Read a send_with_results array.
189
+ # @param reader [Wire::Reader]
190
+ # @return [Array<Hash>, nil]
191
+ def read_send_with_results(reader)
192
+ count = reader.read_varint
193
+ return nil if count.zero?
194
+
195
+ count.times.map do
196
+ txid_hex = reader.read_bytes(32).unpack1('H*')
197
+ code = reader.read_byte
198
+ status = SEND_WITH_CODE_STATUSES.fetch(code) do
199
+ raise ArgumentError, "invalid send_with status code: #{code}"
200
+ end
201
+ { txid: txid_hex, status: status }
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end