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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +89 -0
- data/README.md +2 -2
- data/lib/bsv/auth/transport.rb +1 -1
- data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +5 -3
- data/lib/bsv/network/protocol.rb +4 -5
- 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/provider.rb +7 -9
- 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/overlay/lookup_resolver.rb +0 -1
- data/lib/bsv/overlay/topic_broadcaster.rb +0 -1
- data/lib/bsv/primitives/curve.rb +1 -11
- data/lib/bsv/primitives/ecies.rb +1 -8
- data/lib/bsv/primitives/hex.rb +1 -1
- data/lib/bsv/script/script.rb +1 -1
- data/lib/bsv/transaction/beef.rb +0 -2
- data/lib/bsv/transaction/chain_tracker.rb +74 -13
- data/lib/bsv/transaction/chain_trackers/whats_on_chain.rb +3 -3
- data/lib/bsv/transaction/chain_trackers.rb +0 -10
- data/lib/bsv/transaction/fee_models/live_policy.rb +10 -8
- data/lib/bsv/transaction/merkle_path.rb +0 -2
- 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 +15 -8
- 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 +47 -3
- 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
|