bsv-sdk 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +82 -0
- data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +5 -3
- data/lib/bsv/network/protocols/arc.rb +4 -30
- data/lib/bsv/network/protocols/arcade.rb +163 -0
- data/lib/bsv/network/protocols/chaintracks.rb +6 -3
- data/lib/bsv/network/protocols/jungle_bus.rb +6 -0
- data/lib/bsv/network/protocols.rb +1 -0
- data/lib/bsv/network/providers/gorilla_pool.rb +18 -18
- data/lib/bsv/network/util.rb +44 -0
- data/lib/bsv/network.rb +1 -0
- data/lib/bsv/transaction/chain_tracker.rb +74 -13
- data/lib/bsv/transaction/chain_trackers.rb +0 -10
- data/lib/bsv/transaction/fee_models/live_policy.rb +10 -8
- data/lib/bsv/version.rb +1 -1
- data/lib/bsv/wallet/errors.rb +65 -21
- data/lib/bsv/wallet/proto_wallet/validators.rb +7 -49
- data/lib/bsv/wallet/proto_wallet.rb +14 -1
- data/lib/bsv/wallet/serializer/abort_action.rb +38 -0
- data/lib/bsv/wallet/serializer/acquire_certificate.rb +171 -0
- data/lib/bsv/wallet/serializer/certificate.rb +184 -0
- data/lib/bsv/wallet/serializer/common.rb +207 -0
- data/lib/bsv/wallet/serializer/create_action_args.rb +259 -0
- data/lib/bsv/wallet/serializer/create_action_result.rb +85 -0
- data/lib/bsv/wallet/serializer/create_hmac.rb +67 -0
- data/lib/bsv/wallet/serializer/create_signature.rb +90 -0
- data/lib/bsv/wallet/serializer/decrypt.rb +60 -0
- data/lib/bsv/wallet/serializer/discover_by_attributes.rb +61 -0
- data/lib/bsv/wallet/serializer/discover_by_identity_key.rb +49 -0
- data/lib/bsv/wallet/serializer/discover_certificates_result.rb +39 -0
- data/lib/bsv/wallet/serializer/encrypt.rb +60 -0
- data/lib/bsv/wallet/serializer/get_header_for_height.rb +71 -0
- data/lib/bsv/wallet/serializer/get_height.rb +46 -0
- data/lib/bsv/wallet/serializer/get_network.rb +65 -0
- data/lib/bsv/wallet/serializer/get_public_key.rb +86 -0
- data/lib/bsv/wallet/serializer/get_version.rb +44 -0
- data/lib/bsv/wallet/serializer/internalize_action.rb +151 -0
- data/lib/bsv/wallet/serializer/list_actions.rb +348 -0
- data/lib/bsv/wallet/serializer/list_certificates.rb +124 -0
- data/lib/bsv/wallet/serializer/list_outputs.rb +167 -0
- data/lib/bsv/wallet/serializer/prove_certificate.rb +146 -0
- data/lib/bsv/wallet/serializer/relinquish_certificate.rb +56 -0
- data/lib/bsv/wallet/serializer/relinquish_output.rb +44 -0
- data/lib/bsv/wallet/serializer/reveal_counterparty_key_linkage.rb +108 -0
- data/lib/bsv/wallet/serializer/reveal_specific_key_linkage.rb +116 -0
- data/lib/bsv/wallet/serializer/sign_action_args.rb +94 -0
- data/lib/bsv/wallet/serializer/sign_action_result.rb +49 -0
- data/lib/bsv/wallet/serializer/status.rb +85 -0
- data/lib/bsv/wallet/serializer/verify_hmac.rb +67 -0
- data/lib/bsv/wallet/serializer/verify_signature.rb +101 -0
- data/lib/bsv/wallet/serializer.rb +180 -0
- data/lib/bsv/wallet/substrates/http_wallet_json.rb +129 -0
- data/lib/bsv/wallet/substrates/http_wallet_wire.rb +99 -0
- data/lib/bsv/wallet/wallet_wire.rb +20 -0
- data/lib/bsv/wallet/wallet_wire_processor.rb +61 -0
- data/lib/bsv/wallet/wallet_wire_transceiver.rb +61 -0
- data/lib/bsv/wallet/wire/calls.rb +79 -0
- data/lib/bsv/wallet/wire/frame.rb +181 -0
- data/lib/bsv/wallet/wire/reader_writer.rb +402 -0
- data/lib/bsv/wallet/wire/validation.rb +213 -0
- data/lib/bsv/wallet/wire.rb +13 -0
- data/lib/bsv/wallet.rb +17 -0
- metadata +46 -2
- data/lib/bsv/transaction/chain_trackers/chaintracks.rb +0 -83
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
|
|
5
|
+
module BSV
|
|
6
|
+
module Wallet
|
|
7
|
+
module Wire
|
|
8
|
+
# Binary read/write helpers with BRC-103 idioms.
|
|
9
|
+
#
|
|
10
|
+
# Thin wrappers over StringIO providing the specific encodings used across
|
|
11
|
+
# BRC-103 wire frames: varint strings, optional bools, satoshi LE uint64,
|
|
12
|
+
# and outpoints. Reuses BSV::Transaction::VarInt for the varint codec.
|
|
13
|
+
|
|
14
|
+
# Writer accumulates bytes into a binary string buffer.
|
|
15
|
+
class Writer
|
|
16
|
+
attr_reader :buf
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
@buf = String.new(encoding: 'BINARY')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Write a raw byte.
|
|
23
|
+
def write_byte(byte)
|
|
24
|
+
@buf << [byte].pack('C')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Write raw bytes (binary string).
|
|
28
|
+
def write_bytes(bytes)
|
|
29
|
+
@buf << bytes.b
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Write a Bitcoin varint.
|
|
33
|
+
def write_varint(n)
|
|
34
|
+
@buf << BSV::Transaction::VarInt.encode(n)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Write a UTF-8 string prefixed by its byte length as a varint.
|
|
38
|
+
def write_str_with_varint_len(str)
|
|
39
|
+
bytes = str.to_s.b
|
|
40
|
+
write_varint(bytes.bytesize)
|
|
41
|
+
write_bytes(bytes)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Write an optional boolean as a single byte (Go/BRC-103 convention).
|
|
45
|
+
# nil → 0xFF, false → 0x00, true → 0x01
|
|
46
|
+
def write_optional_bool(value)
|
|
47
|
+
byte = case value
|
|
48
|
+
when nil then 0xFF
|
|
49
|
+
when false then 0x00
|
|
50
|
+
else 0x01
|
|
51
|
+
end
|
|
52
|
+
write_byte(byte)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Write a satoshi amount as 8-byte little-endian uint64.
|
|
56
|
+
def write_satoshis(n)
|
|
57
|
+
@buf << [n].pack('Q<')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Write an outpoint: 32-byte display-order txid followed by varint vout.
|
|
61
|
+
#
|
|
62
|
+
# Go encodeOutpoint calls WriteBytesReverse(Txid[:]) which writes the
|
|
63
|
+
# wire-order (chainhash) bytes reversed — i.e. display order on the wire.
|
|
64
|
+
# The vout is a varint, not a fixed 4-byte LE integer.
|
|
65
|
+
#
|
|
66
|
+
# @param txid_hex [String] 64-char display-order hex txid
|
|
67
|
+
# @param vout [Integer] output index
|
|
68
|
+
def write_outpoint(txid_hex, vout)
|
|
69
|
+
BSV::Primitives::Hex.validate_dtxid_hex!(txid_hex)
|
|
70
|
+
@buf << [txid_hex].pack('H*')
|
|
71
|
+
write_varint(vout)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Write the NegativeOne sentinel (MaxUint64 as a 9-byte varint: 9 × 0xFF).
|
|
75
|
+
# Used as a nil sentinel for optional fields in Go-compatible encoding.
|
|
76
|
+
def write_negative_one
|
|
77
|
+
write_varint(0xFFFF_FFFF_FFFF_FFFF)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Write a varint length-prefixed byte array (WriteIntBytes in Go).
|
|
81
|
+
# @param bytes [String, nil] binary string; nil or empty → write varint 0
|
|
82
|
+
def write_int_bytes(bytes)
|
|
83
|
+
raw = bytes ? bytes.b : ''.b
|
|
84
|
+
write_varint(raw.bytesize)
|
|
85
|
+
write_bytes(raw)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Write an optional uint32: nil → NegativeOne; else varint.
|
|
89
|
+
# @param n [Integer, nil]
|
|
90
|
+
def write_optional_uint32(n)
|
|
91
|
+
if n.nil?
|
|
92
|
+
write_negative_one
|
|
93
|
+
else
|
|
94
|
+
write_varint(n)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Write an optional string: nil or empty → NegativeOne; else varint len + bytes.
|
|
99
|
+
# Matches Go WriteOptionalString.
|
|
100
|
+
# @param str [String, nil]
|
|
101
|
+
def write_optional_string(str)
|
|
102
|
+
if str.nil? || str.empty?
|
|
103
|
+
write_negative_one
|
|
104
|
+
else
|
|
105
|
+
write_str_with_varint_len(str)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Write an array of strings: nil → NegativeOne; else varint count + each as optional string.
|
|
110
|
+
# Matches Go WriteStringSlice.
|
|
111
|
+
# @param arr [Array<String>, nil]
|
|
112
|
+
def write_string_slice(arr)
|
|
113
|
+
if arr.nil?
|
|
114
|
+
write_negative_one
|
|
115
|
+
else
|
|
116
|
+
write_varint(arr.length)
|
|
117
|
+
arr.each { |s| write_optional_string(s) }
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Write a string map (Hash<String,String>) sorted by key.
|
|
122
|
+
# Matches Go WriteStringMap.
|
|
123
|
+
# @param map [Hash, nil]
|
|
124
|
+
def write_string_map(map)
|
|
125
|
+
m = map || {}
|
|
126
|
+
write_varint(m.length)
|
|
127
|
+
m.keys.sort.each do |k|
|
|
128
|
+
write_str_with_varint_len(k)
|
|
129
|
+
write_str_with_varint_len(m[k])
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Write a binary value encoded as a Base64 string on the wire.
|
|
134
|
+
# Decodes the Base64 string and writes the raw bytes prefixed by varint length.
|
|
135
|
+
# @param base64_str [String] standard Base64-encoded string
|
|
136
|
+
def write_int_from_base64(base64_str)
|
|
137
|
+
raw = Base64.strict_decode64(base64_str)
|
|
138
|
+
write_int_bytes(raw)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Write the privileged flag and reason (Go encodePrivilegedParams).
|
|
142
|
+
# privileged: nil → NegativeOneByte (0xFF), false → 0x00, true → 0x01.
|
|
143
|
+
# reason: nil or empty → NegativeOneByte; else varint len + bytes.
|
|
144
|
+
def write_privileged_params(privileged, reason)
|
|
145
|
+
# Go OptionalBool: nil=0xFF, false=0x00, true=0x01
|
|
146
|
+
byte = case privileged
|
|
147
|
+
when nil then 0xFF
|
|
148
|
+
when false then 0x00
|
|
149
|
+
else 0x01
|
|
150
|
+
end
|
|
151
|
+
write_byte(byte)
|
|
152
|
+
if reason.nil? || reason.empty?
|
|
153
|
+
write_byte(0xFF)
|
|
154
|
+
else
|
|
155
|
+
write_str_with_varint_len(reason)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Write a txid slice (array of 32-byte txids in wire order).
|
|
160
|
+
# nil → NegativeOne; else varint count + 32 raw bytes per txid.
|
|
161
|
+
# Txids are passed as 64-char display-order hex; written as-is (no byte reversal)
|
|
162
|
+
# to match Go WriteTxidSlice which writes txID[:] (wire-order bytes directly).
|
|
163
|
+
# @param txids [Array<String>, nil] 64-char display-order hex strings
|
|
164
|
+
def write_txid_slice(txids)
|
|
165
|
+
if txids.nil?
|
|
166
|
+
write_negative_one
|
|
167
|
+
else
|
|
168
|
+
write_varint(txids.length)
|
|
169
|
+
txids.each { |hex| write_bytes([hex].pack('H*')) }
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Write optional bytes with a 1-byte flag prefix (Go BytesOptionWithFlag).
|
|
174
|
+
# nil/empty → 0x00; else → 0x01 + varint_len + bytes (or fixed_size bytes).
|
|
175
|
+
# @param bytes [String, nil] binary data
|
|
176
|
+
# @param fixed_size [Integer, nil] if set, omit the varint length prefix
|
|
177
|
+
def write_optional_bytes_with_flag(bytes, fixed_size: nil)
|
|
178
|
+
raw = bytes ? bytes.b : ''.b
|
|
179
|
+
if raw.empty?
|
|
180
|
+
write_byte(0)
|
|
181
|
+
else
|
|
182
|
+
write_byte(1)
|
|
183
|
+
if fixed_size
|
|
184
|
+
write_bytes(raw)
|
|
185
|
+
else
|
|
186
|
+
write_int_bytes(raw)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Write a varint-len string (always present, 0-length for nil/empty).
|
|
192
|
+
# Matches Go WriteString which always writes the length prefix.
|
|
193
|
+
# @param str [String, nil]
|
|
194
|
+
def write_string(str)
|
|
195
|
+
write_str_with_varint_len(str.to_s)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Reader reads sequentially from a binary string.
|
|
200
|
+
class Reader
|
|
201
|
+
# @param data [String] binary data
|
|
202
|
+
def initialize(data)
|
|
203
|
+
@data = data.b
|
|
204
|
+
@pos = 0
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Read a single byte.
|
|
208
|
+
# @return [Integer]
|
|
209
|
+
def read_byte
|
|
210
|
+
raise ArgumentError, 'unexpected end of data reading byte' if @pos >= @data.bytesize
|
|
211
|
+
|
|
212
|
+
byte = @data.getbyte(@pos)
|
|
213
|
+
@pos += 1
|
|
214
|
+
byte
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Look at the next byte without consuming it.
|
|
218
|
+
# @return [Integer]
|
|
219
|
+
def peek_byte
|
|
220
|
+
raise ArgumentError, 'unexpected end of data peeking byte' if @pos >= @data.bytesize
|
|
221
|
+
|
|
222
|
+
@data.getbyte(@pos)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Read +n+ raw bytes.
|
|
226
|
+
# @return [String] binary string
|
|
227
|
+
def read_bytes(n)
|
|
228
|
+
raise ArgumentError, "need #{n} bytes at offset #{@pos}, got #{remaining}" if remaining < n
|
|
229
|
+
|
|
230
|
+
slice = @data.byteslice(@pos, n)
|
|
231
|
+
@pos += n
|
|
232
|
+
slice
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Read a Bitcoin varint.
|
|
236
|
+
# @return [Integer]
|
|
237
|
+
def read_varint
|
|
238
|
+
value, consumed = BSV::Transaction::VarInt.decode(@data, @pos)
|
|
239
|
+
@pos += consumed
|
|
240
|
+
value
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Read a varint-prefixed UTF-8 string.
|
|
244
|
+
# @return [String]
|
|
245
|
+
# @raise [ArgumentError] if the bytes are not valid UTF-8
|
|
246
|
+
def read_str_with_varint_len
|
|
247
|
+
len = read_varint
|
|
248
|
+
str = read_bytes(len).force_encoding('UTF-8')
|
|
249
|
+
raise ArgumentError, 'varint-prefixed string is not valid UTF-8' unless str.valid_encoding?
|
|
250
|
+
|
|
251
|
+
str
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Read an optional boolean byte (Go/BRC-103 convention).
|
|
255
|
+
# 0xFF → nil, 0x00 → false, 0x01 → true
|
|
256
|
+
# @return [Boolean, nil]
|
|
257
|
+
def read_optional_bool
|
|
258
|
+
byte = read_byte
|
|
259
|
+
return nil if byte == 0xFF
|
|
260
|
+
|
|
261
|
+
byte == 0x01
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Read 8-byte little-endian uint64 satoshi amount.
|
|
265
|
+
# @return [Integer]
|
|
266
|
+
def read_satoshis
|
|
267
|
+
read_bytes(8).unpack1('Q<')
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Read an outpoint: 32-byte display-order txid + varint vout.
|
|
271
|
+
# @return [Hash] { txid_hex: String, vout: Integer }
|
|
272
|
+
def read_outpoint
|
|
273
|
+
txid_bytes = read_bytes(32)
|
|
274
|
+
vout = read_varint
|
|
275
|
+
{ txid_hex: txid_bytes.unpack1('H*'), vout: vout }
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Remaining bytes.
|
|
279
|
+
def remaining
|
|
280
|
+
@data.bytesize - @pos
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Read all remaining bytes.
|
|
284
|
+
# @return [String] binary string
|
|
285
|
+
def read_remaining
|
|
286
|
+
slice = @data.byteslice(@pos, @data.bytesize - @pos) || ''.b
|
|
287
|
+
@pos = @data.bytesize
|
|
288
|
+
slice
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Whether the next varint is the NegativeOne sentinel (0xFFFF…).
|
|
292
|
+
# Peeks at the next byte without consuming it.
|
|
293
|
+
def next_negative_one?
|
|
294
|
+
@pos < @data.bytesize && @data.getbyte(@pos) == 0xFF
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Read a varint-length-prefixed byte array (ReadIntBytes in Go).
|
|
298
|
+
# @return [String] binary string
|
|
299
|
+
def read_int_bytes
|
|
300
|
+
len = read_varint
|
|
301
|
+
return ''.b if len == 0xFFFF_FFFF_FFFF_FFFF || len.zero?
|
|
302
|
+
|
|
303
|
+
read_bytes(len)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Read an optional uint32: NegativeOne sentinel → nil; else varint → Integer.
|
|
307
|
+
# @return [Integer, nil]
|
|
308
|
+
def read_optional_uint32
|
|
309
|
+
val = read_varint
|
|
310
|
+
val == 0xFFFF_FFFF_FFFF_FFFF ? nil : val
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Read an optional string: NegativeOne sentinel → nil; else varint len + bytes.
|
|
314
|
+
# @return [String, nil]
|
|
315
|
+
def read_optional_string
|
|
316
|
+
val = read_varint
|
|
317
|
+
return nil if val == 0xFFFF_FFFF_FFFF_FFFF
|
|
318
|
+
|
|
319
|
+
read_bytes(val).force_encoding('UTF-8')
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Read an array of strings encoded as varint count + each optional string.
|
|
323
|
+
# NegativeOne sentinel count → nil.
|
|
324
|
+
# @return [Array<String>, nil]
|
|
325
|
+
def read_string_slice
|
|
326
|
+
count = read_varint
|
|
327
|
+
return nil if count == 0xFFFF_FFFF_FFFF_FFFF
|
|
328
|
+
|
|
329
|
+
count.times.map { read_optional_string || '' }
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Read a string map: varint count + key/value pairs (each varint-len-prefixed).
|
|
333
|
+
# @return [Hash<String,String>]
|
|
334
|
+
def read_string_map
|
|
335
|
+
count = read_varint
|
|
336
|
+
count.times.each_with_object({}) do |_, h|
|
|
337
|
+
k = read_str_with_varint_len
|
|
338
|
+
v = read_str_with_varint_len
|
|
339
|
+
h[k] = v
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Read a binary value and return it Base64-encoded.
|
|
344
|
+
# @return [String] Base64-encoded string
|
|
345
|
+
def read_base64_int
|
|
346
|
+
raw = read_int_bytes
|
|
347
|
+
Base64.strict_encode64(raw)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Read privileged params (Go decodePrivilegedParams).
|
|
351
|
+
# @return [Array(Boolean|nil, String|nil)]
|
|
352
|
+
def read_privileged_params
|
|
353
|
+
privileged = read_optional_bool
|
|
354
|
+
first_byte = read_byte
|
|
355
|
+
if first_byte == 0xFF
|
|
356
|
+
[privileged, nil]
|
|
357
|
+
else
|
|
358
|
+
# Back up one byte and read as varint-prefixed string
|
|
359
|
+
@pos -= 1
|
|
360
|
+
reason = read_str_with_varint_len
|
|
361
|
+
[privileged, reason]
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Read a txid slice: NegativeOne → nil; else varint count + 32 bytes per txid.
|
|
366
|
+
# Go stores txids in wire order (txID[:]) — returned as hex without reversal.
|
|
367
|
+
# @return [Array<String>, nil]
|
|
368
|
+
def read_txid_slice
|
|
369
|
+
count = read_varint
|
|
370
|
+
return nil if count == 0xFFFF_FFFF_FFFF_FFFF
|
|
371
|
+
|
|
372
|
+
count.times.map { read_bytes(32).unpack1('H*') }
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Read optional bytes with a 1-byte flag prefix (Go BytesOptionWithFlag).
|
|
376
|
+
# 0x00 → nil; 0x01 → read varint_len + bytes (or fixed_size bytes).
|
|
377
|
+
# @param fixed_size [Integer, nil] if set, read exactly this many bytes (no varint)
|
|
378
|
+
# @return [String, nil] binary string or nil
|
|
379
|
+
def read_optional_bytes_with_flag(fixed_size: nil)
|
|
380
|
+
flag = read_byte
|
|
381
|
+
return nil if flag.zero?
|
|
382
|
+
|
|
383
|
+
if fixed_size
|
|
384
|
+
read_bytes(fixed_size)
|
|
385
|
+
else
|
|
386
|
+
read_int_bytes
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Read a varint-len string (always present, 0-length = empty string).
|
|
391
|
+
# Matches Go ReadString which returns "" for length 0 or NegativeOne.
|
|
392
|
+
# @return [String]
|
|
393
|
+
def read_string
|
|
394
|
+
len = read_varint
|
|
395
|
+
return '' if len.zero? || len == 0xFFFF_FFFF_FFFF_FFFF
|
|
396
|
+
|
|
397
|
+
read_bytes(len).force_encoding('UTF-8')
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
end
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
module Wire
|
|
6
|
+
# Branded-type validators for BRC-100 parameters.
|
|
7
|
+
#
|
|
8
|
+
# Each method raises InvalidParameterError on failure and returns nil
|
|
9
|
+
# on success. Port of ts-sdk/src/wallet/validationHelpers.ts.
|
|
10
|
+
#
|
|
11
|
+
# The existing ProtoWallet::Validators module delegates here so there
|
|
12
|
+
# is a single source of truth for all parameter validation logic.
|
|
13
|
+
module Validation
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
MAX_SATOSHIS = 21_000_000 * (10**8)
|
|
17
|
+
|
|
18
|
+
# Validates that +value+ is a hex string (even length, hex chars only).
|
|
19
|
+
#
|
|
20
|
+
# @param name [String] parameter name for error messages
|
|
21
|
+
# @param value [Object] the value to validate
|
|
22
|
+
# @param length [Integer, nil] expected number of hex chars (nil = any)
|
|
23
|
+
def hex_string!(name, value, length: nil)
|
|
24
|
+
raise InvalidParameterError.new(name, 'a String') unless value.is_a?(String)
|
|
25
|
+
|
|
26
|
+
raise InvalidParameterError.new(name, 'a hex string (characters 0-9, a-f, A-F only)') unless value.match?(/\A[0-9a-fA-F]*\z/)
|
|
27
|
+
|
|
28
|
+
raise InvalidParameterError.new(name, 'a hex string with even number of characters') if value.length.odd?
|
|
29
|
+
|
|
30
|
+
return unless length && value.length != length
|
|
31
|
+
|
|
32
|
+
raise InvalidParameterError.new(name, "a #{length}-character hex string, got #{value.length}")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Validates that +value+ is a Base64-encoded string.
|
|
36
|
+
#
|
|
37
|
+
# @param name [String] parameter name for error messages
|
|
38
|
+
# @param value [Object] the value to validate
|
|
39
|
+
def base64_string!(name, value)
|
|
40
|
+
raise InvalidParameterError.new(name, 'a String') unless value.is_a?(String)
|
|
41
|
+
|
|
42
|
+
return if value.match?(%r{\A[A-Za-z0-9+/]*={0,2}\z})
|
|
43
|
+
|
|
44
|
+
raise InvalidParameterError.new(name, 'a valid Base64 string')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Validates an outpoint string in the form "<64-hex-txid>.<vout>".
|
|
48
|
+
#
|
|
49
|
+
# @param name [String] parameter name for error messages
|
|
50
|
+
# @param value [Object] the value to validate
|
|
51
|
+
def outpoint_string!(name, value)
|
|
52
|
+
raise InvalidParameterError.new(name, 'a String') unless value.is_a?(String)
|
|
53
|
+
|
|
54
|
+
parts = value.split('.', 2)
|
|
55
|
+
raise InvalidParameterError.new(name, 'an outpoint string in the format <64-hex-txid>.<vout>') unless parts.length == 2
|
|
56
|
+
|
|
57
|
+
txid_hex, vout_str = parts
|
|
58
|
+
|
|
59
|
+
raise InvalidParameterError.new(name, 'an outpoint with a 64-character hex transaction ID') unless txid_hex.match?(/\A[0-9a-fA-F]{64}\z/)
|
|
60
|
+
|
|
61
|
+
raise InvalidParameterError.new(name, 'an outpoint with a non-negative integer output index') unless vout_str.match?(/\A\d+\z/)
|
|
62
|
+
|
|
63
|
+
vout = vout_str.to_i
|
|
64
|
+
return unless vout > 0xFFFFFFFF
|
|
65
|
+
|
|
66
|
+
raise InvalidParameterError.new(name, 'an outpoint with a vout that fits in a uint32 (0..4294967295)')
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Validates a compressed public key in hex form (66 chars, 02/03 prefix).
|
|
70
|
+
#
|
|
71
|
+
# @param name [String] parameter name for error messages
|
|
72
|
+
# @param value [Object] the value to validate
|
|
73
|
+
def pub_key_hex!(name, value)
|
|
74
|
+
raise InvalidParameterError.new(name, 'a String') unless value.is_a?(String)
|
|
75
|
+
|
|
76
|
+
return if value.match?(/\A0[23][0-9a-fA-F]{64}\z/)
|
|
77
|
+
|
|
78
|
+
raise InvalidParameterError.new(name, 'a 66-character compressed public key hex string (02 or 03 prefix)')
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Validates a description string (5..50 printable characters).
|
|
82
|
+
#
|
|
83
|
+
# @param name [String] parameter name for error messages
|
|
84
|
+
# @param value [Object] the value to validate
|
|
85
|
+
def description_5_to_50!(name, value)
|
|
86
|
+
raise InvalidParameterError.new(name, 'a String') unless value.is_a?(String)
|
|
87
|
+
|
|
88
|
+
len = value.length
|
|
89
|
+
raise InvalidParameterError.new(name, 'between 5 and 50 characters') if len < 5 || len > 50
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Validates a label string (1..150 characters, no spaces).
|
|
93
|
+
#
|
|
94
|
+
# @param name [String] parameter name for error messages
|
|
95
|
+
# @param value [Object] the value to validate
|
|
96
|
+
def label_string!(name, value)
|
|
97
|
+
raise InvalidParameterError.new(name, 'a String') unless value.is_a?(String)
|
|
98
|
+
|
|
99
|
+
len = value.length
|
|
100
|
+
raise InvalidParameterError.new(name, 'between 1 and 150 characters') if len < 1 || len > 150
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Validates a basket name (1..300 characters).
|
|
104
|
+
#
|
|
105
|
+
# @param name [String] parameter name for error messages
|
|
106
|
+
# @param value [Object] the value to validate
|
|
107
|
+
def basket_string!(name, value)
|
|
108
|
+
raise InvalidParameterError.new(name, 'a String') unless value.is_a?(String)
|
|
109
|
+
|
|
110
|
+
len = value.length
|
|
111
|
+
raise InvalidParameterError.new(name, 'between 1 and 300 characters') if len < 1 || len > 300
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Validates an originator domain (1..250 bytes UTF-8).
|
|
115
|
+
# The originator must fit in a single-byte length field in the wire frame.
|
|
116
|
+
#
|
|
117
|
+
# @param name [String] parameter name for error messages
|
|
118
|
+
# @param value [Object] the value to validate
|
|
119
|
+
def originator_domain!(name, value)
|
|
120
|
+
raise InvalidParameterError.new(name, 'a String') unless value.is_a?(String)
|
|
121
|
+
|
|
122
|
+
byte_len = value.bytesize
|
|
123
|
+
raise InvalidParameterError.new(name, 'at most 250 bytes') if byte_len > 250
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Validates a satoshi amount (0..21_000_000 * 10^8).
|
|
127
|
+
#
|
|
128
|
+
# @param name [String] parameter name for error messages
|
|
129
|
+
# @param value [Object] the value to validate
|
|
130
|
+
def satoshi_value!(name, value)
|
|
131
|
+
raise InvalidParameterError.new(name, 'a non-negative integer') unless value.is_a?(Integer)
|
|
132
|
+
|
|
133
|
+
return unless value.negative? || value > MAX_SATOSHIS
|
|
134
|
+
|
|
135
|
+
raise InvalidParameterError.new(name, "between 0 and #{MAX_SATOSHIS} satoshis")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Validates a non-negative integer.
|
|
139
|
+
#
|
|
140
|
+
# @param name [String] parameter name for error messages
|
|
141
|
+
# @param value [Object] the value to validate
|
|
142
|
+
def positive_integer_or_zero!(name, value)
|
|
143
|
+
return if value.is_a?(Integer) && !value.negative?
|
|
144
|
+
|
|
145
|
+
raise InvalidParameterError.new(name, 'a non-negative integer')
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Validates a BRC-43 protocol name string (5..400 chars, lowercase).
|
|
149
|
+
#
|
|
150
|
+
# @param name [String] parameter name for error messages
|
|
151
|
+
# @param value [Object] the value to validate
|
|
152
|
+
def protocol_string_5_to_400!(name, value)
|
|
153
|
+
raise InvalidParameterError.new(name, 'a String') unless value.is_a?(String)
|
|
154
|
+
|
|
155
|
+
normalized = value.strip.downcase
|
|
156
|
+
max_length = normalized.start_with?('specific linkage revelation') ? 430 : 400
|
|
157
|
+
|
|
158
|
+
raise InvalidParameterError.new(name, "between 5 and #{max_length} characters") if normalized.length < 5 || normalized.length > max_length
|
|
159
|
+
|
|
160
|
+
raise InvalidParameterError.new(name, 'lowercase letters, numbers, and spaces only') unless normalized.match?(/\A[a-z0-9 ]+\z/)
|
|
161
|
+
|
|
162
|
+
return unless normalized.include?(' ')
|
|
163
|
+
|
|
164
|
+
raise InvalidParameterError.new(name, 'free of consecutive spaces')
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Validates a BRC-43 key ID string (1..800 bytes).
|
|
168
|
+
#
|
|
169
|
+
# @param name [String] parameter name for error messages
|
|
170
|
+
# @param value [Object] the value to validate
|
|
171
|
+
def key_id_string_1_to_800!(name, value)
|
|
172
|
+
raise InvalidParameterError.new(name, 'a String') unless value.is_a?(String)
|
|
173
|
+
|
|
174
|
+
byte_length = value.bytesize
|
|
175
|
+
raise InvalidParameterError.new(name, 'between 1 and 800 bytes') if byte_length < 1 || byte_length > 800
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Validates a wallet counterparty: 'self', 'anyone', or a 66-char hex pubkey.
|
|
179
|
+
#
|
|
180
|
+
# @param name [String] parameter name for error messages
|
|
181
|
+
# @param value [Object] the value to validate
|
|
182
|
+
def wallet_counterparty!(name, value)
|
|
183
|
+
return if %w[self anyone].include?(value)
|
|
184
|
+
|
|
185
|
+
pub_key_hex!(name, value)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Validates a BRC-43 protocol ID: [security_level (0-2), protocol_name (5-400 chars)].
|
|
189
|
+
#
|
|
190
|
+
# @param name [String] parameter name for error messages
|
|
191
|
+
# @param value [Object] the value to validate
|
|
192
|
+
def wallet_protocol!(name, value)
|
|
193
|
+
raise InvalidParameterError.new(name, 'an Array of [security_level, protocol_name]') unless value.is_a?(Array) && value.length == 2
|
|
194
|
+
|
|
195
|
+
level, proto_name = value
|
|
196
|
+
raise InvalidParameterError.new(name, 'a security level of 0, 1, or 2') unless [0, 1, 2].include?(level)
|
|
197
|
+
|
|
198
|
+
protocol_string_5_to_400!("#{name} protocol name", proto_name)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Validates a certificate acquisition protocol: 'direct' or 'issuance'.
|
|
202
|
+
#
|
|
203
|
+
# @param name [String] parameter name for error messages
|
|
204
|
+
# @param value [Object] the value to validate
|
|
205
|
+
def acquisition_protocol!(name, value)
|
|
206
|
+
return if %w[direct issuance].include?(value)
|
|
207
|
+
|
|
208
|
+
raise InvalidParameterError.new(name, "'direct' or 'issuance'")
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
module Wire
|
|
6
|
+
autoload :Frame, 'bsv/wallet/wire/frame'
|
|
7
|
+
autoload :Calls, 'bsv/wallet/wire/calls'
|
|
8
|
+
autoload :Validation, 'bsv/wallet/wire/validation'
|
|
9
|
+
autoload :Writer, 'bsv/wallet/wire/reader_writer'
|
|
10
|
+
autoload :Reader, 'bsv/wallet/wire/reader_writer'
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/bsv/wallet.rb
CHANGED
|
@@ -8,9 +8,26 @@ module BSV
|
|
|
8
8
|
require_relative 'wallet/errors'
|
|
9
9
|
require_relative 'wallet/interface'
|
|
10
10
|
|
|
11
|
+
# Wire layer — BRC-103 frame codec, call enum, validators, reader/writer.
|
|
12
|
+
require_relative 'wallet/wire'
|
|
13
|
+
|
|
14
|
+
# Per-call BRC-103 serialisers and dispatch tables.
|
|
15
|
+
autoload :Serializer, 'bsv/wallet/serializer'
|
|
16
|
+
|
|
11
17
|
# ProtoWallet — minimal crypto-only BRC-100 implementation.
|
|
12
18
|
# Its internals (KeyDeriver, Validators) are scoped under ProtoWallet::
|
|
13
19
|
# to avoid collision with bsv-wallet's own KeyDeriver.
|
|
14
20
|
require_relative 'wallet/proto_wallet'
|
|
21
|
+
|
|
22
|
+
# BRC-103 transport layer.
|
|
23
|
+
autoload :WalletWire, 'bsv/wallet/wallet_wire'
|
|
24
|
+
autoload :WalletWireTransceiver, 'bsv/wallet/wallet_wire_transceiver'
|
|
25
|
+
autoload :WalletWireProcessor, 'bsv/wallet/wallet_wire_processor'
|
|
26
|
+
|
|
27
|
+
# BRC-103 transport substrates.
|
|
28
|
+
module Substrates
|
|
29
|
+
autoload :HTTPWalletWire, 'bsv/wallet/substrates/http_wallet_wire'
|
|
30
|
+
autoload :HTTPWalletJSON, 'bsv/wallet/substrates/http_wallet_json'
|
|
31
|
+
end
|
|
15
32
|
end
|
|
16
33
|
end
|