bsv-sdk 0.20.0 → 0.23.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 +106 -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 +27 -4
- data/lib/bsv/network/protocols/jungle_bus.rb +34 -0
- data/lib/bsv/network/protocols/woc_rest.rb +28 -1
- 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 +66 -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,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
module Wire
|
|
6
|
+
# BRC-103 request and result frame codec.
|
|
7
|
+
#
|
|
8
|
+
# Port of go-sdk/wallet/serializer/frame.go. Two frame types:
|
|
9
|
+
#
|
|
10
|
+
# Request frame (client → wallet):
|
|
11
|
+
# [1 byte: call][1 byte: originator_len][originator_len bytes: UTF-8][remaining: params]
|
|
12
|
+
#
|
|
13
|
+
# Result frame (wallet → client):
|
|
14
|
+
# [1 byte: error_code]
|
|
15
|
+
# On success (0x00): [remaining bytes: payload]
|
|
16
|
+
# On error (non-zero):
|
|
17
|
+
# [VarInt: message_len][message bytes]
|
|
18
|
+
# [VarInt: stack_len][stack bytes]
|
|
19
|
+
module Frame
|
|
20
|
+
# Maximum originator byte length enforced at write time.
|
|
21
|
+
# Matches the BRC-100 +OriginatorDomainNameStringUnder250Bytes+ branded
|
|
22
|
+
# type used by +Wire::Validation.originator_domain!+.
|
|
23
|
+
MAX_ORIGINATOR_BYTES = 250
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# Encode a request frame.
|
|
28
|
+
#
|
|
29
|
+
# @param call [Integer] call byte (1..28)
|
|
30
|
+
# @param originator [String] originator domain (0..250 bytes UTF-8)
|
|
31
|
+
# @param params [String, nil] binary params payload
|
|
32
|
+
# @return [String] binary frame (ASCII-8BIT encoding)
|
|
33
|
+
# @raise [ArgumentError] if originator exceeds 250 bytes
|
|
34
|
+
def write_request(call:, originator:, params: nil)
|
|
35
|
+
originator_bytes = originator.to_s.b
|
|
36
|
+
if originator_bytes.bytesize > MAX_ORIGINATOR_BYTES
|
|
37
|
+
raise ArgumentError,
|
|
38
|
+
"originator must be at most #{MAX_ORIGINATOR_BYTES} bytes, " \
|
|
39
|
+
"got #{originator_bytes.bytesize}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
buf = String.new(encoding: 'BINARY')
|
|
43
|
+
buf << [call, originator_bytes.bytesize].pack('CC')
|
|
44
|
+
buf << originator_bytes
|
|
45
|
+
buf << params.b if params && !params.empty?
|
|
46
|
+
buf
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Decode a request frame.
|
|
50
|
+
#
|
|
51
|
+
# @param bytes [String] binary frame
|
|
52
|
+
# @return [Hash] { call: Integer, originator: String, params: String }
|
|
53
|
+
# @raise [ArgumentError] if the frame is truncated or malformed
|
|
54
|
+
def read_request(bytes)
|
|
55
|
+
data = bytes.b
|
|
56
|
+
raise ArgumentError, 'frame too short: need at least 2 bytes' if data.bytesize < 2
|
|
57
|
+
|
|
58
|
+
call = data.getbyte(0)
|
|
59
|
+
originator_len = data.getbyte(1)
|
|
60
|
+
|
|
61
|
+
if data.bytesize < 2 + originator_len
|
|
62
|
+
raise ArgumentError,
|
|
63
|
+
"frame truncated: need #{2 + originator_len} bytes for originator, " \
|
|
64
|
+
"got #{data.bytesize}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
originator = data.byteslice(2, originator_len).force_encoding('UTF-8')
|
|
68
|
+
raise ArgumentError, 'frame originator is not valid UTF-8' unless originator.valid_encoding?
|
|
69
|
+
|
|
70
|
+
params = data.byteslice(2 + originator_len, data.bytesize - 2 - originator_len) || ''.b
|
|
71
|
+
|
|
72
|
+
{ call: call, originator: originator, params: params }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Encode a success result frame.
|
|
76
|
+
#
|
|
77
|
+
# @param payload [String, nil] binary payload
|
|
78
|
+
# @return [String] binary frame
|
|
79
|
+
def write_result(payload: nil)
|
|
80
|
+
buf = String.new(encoding: 'BINARY')
|
|
81
|
+
buf << "\x00"
|
|
82
|
+
buf << payload.b if payload && !payload.empty?
|
|
83
|
+
buf
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Encode an error result frame.
|
|
87
|
+
#
|
|
88
|
+
# @param error [BSV::Wallet::Error] the error to encode
|
|
89
|
+
# @return [String] binary frame
|
|
90
|
+
def write_error(error:)
|
|
91
|
+
wire = error.to_wire
|
|
92
|
+
msg_bytes = wire[:message].to_s.b
|
|
93
|
+
stack_bytes = wire[:stack].to_s.b
|
|
94
|
+
|
|
95
|
+
buf = String.new(encoding: 'BINARY')
|
|
96
|
+
buf << [wire[:code]].pack('C')
|
|
97
|
+
buf << encode_varint(msg_bytes.bytesize)
|
|
98
|
+
buf << msg_bytes
|
|
99
|
+
buf << encode_varint(stack_bytes.bytesize)
|
|
100
|
+
buf << stack_bytes
|
|
101
|
+
buf
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Decode a result frame.
|
|
105
|
+
#
|
|
106
|
+
# @param bytes [String] binary frame
|
|
107
|
+
# @return [String] binary payload on success
|
|
108
|
+
# @raise [BSV::Wallet::Error] the appropriate subclass on error
|
|
109
|
+
# @raise [ArgumentError] if the frame is truncated or malformed
|
|
110
|
+
def read_result(bytes)
|
|
111
|
+
data = bytes.b
|
|
112
|
+
raise ArgumentError, 'result frame is empty' if data.empty?
|
|
113
|
+
|
|
114
|
+
code = data.getbyte(0)
|
|
115
|
+
|
|
116
|
+
return data.byteslice(1, data.bytesize - 1) || ''.b if code.zero?
|
|
117
|
+
|
|
118
|
+
offset = 1
|
|
119
|
+
msg_len, vi = decode_varint(data, offset)
|
|
120
|
+
offset += vi
|
|
121
|
+
|
|
122
|
+
raise ArgumentError, 'result frame truncated: message' if data.bytesize < offset + msg_len
|
|
123
|
+
|
|
124
|
+
message = data.byteslice(offset, msg_len).force_encoding('UTF-8')
|
|
125
|
+
raise ArgumentError, 'result frame error message is not valid UTF-8' unless message.valid_encoding?
|
|
126
|
+
|
|
127
|
+
offset += msg_len
|
|
128
|
+
|
|
129
|
+
stack_len, vi = decode_varint(data, offset)
|
|
130
|
+
offset += vi
|
|
131
|
+
|
|
132
|
+
raise ArgumentError, 'result frame truncated: stack' if data.bytesize < offset + stack_len
|
|
133
|
+
|
|
134
|
+
stack = data.byteslice(offset, stack_len).force_encoding('UTF-8')
|
|
135
|
+
raise ArgumentError, 'result frame stack is not valid UTF-8' unless stack.valid_encoding?
|
|
136
|
+
|
|
137
|
+
raise BSV::Wallet.error_from_wire(code, message, stack)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @param n [Integer] unsigned integer
|
|
141
|
+
# @return [String] Bitcoin varint encoding
|
|
142
|
+
def encode_varint(n)
|
|
143
|
+
if n < 0xFD
|
|
144
|
+
[n].pack('C')
|
|
145
|
+
elsif n <= 0xFFFF
|
|
146
|
+
[0xFD, n].pack('Cv')
|
|
147
|
+
elsif n <= 0xFFFFFFFF
|
|
148
|
+
[0xFE, n].pack('CV')
|
|
149
|
+
else
|
|
150
|
+
[0xFF, n].pack('CQ<')
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# @param data [String] binary data
|
|
155
|
+
# @param offset [Integer] byte offset
|
|
156
|
+
# @return [Array(Integer, Integer)] decoded value, bytes consumed
|
|
157
|
+
def decode_varint(data, offset = 0)
|
|
158
|
+
raise ArgumentError, "varint: need 1 byte at #{offset}" if offset >= data.bytesize
|
|
159
|
+
|
|
160
|
+
first = data.getbyte(offset)
|
|
161
|
+
case first
|
|
162
|
+
when 0..0xFC
|
|
163
|
+
[first, 1]
|
|
164
|
+
when 0xFD
|
|
165
|
+
raise ArgumentError, "varint: need 3 bytes at #{offset}" if data.bytesize < offset + 3
|
|
166
|
+
|
|
167
|
+
[data.byteslice(offset + 1, 2).unpack1('v'), 3]
|
|
168
|
+
when 0xFE
|
|
169
|
+
raise ArgumentError, "varint: need 5 bytes at #{offset}" if data.bytesize < offset + 5
|
|
170
|
+
|
|
171
|
+
[data.byteslice(offset + 1, 4).unpack1('V'), 5]
|
|
172
|
+
when 0xFF
|
|
173
|
+
raise ArgumentError, "varint: need 9 bytes at #{offset}" if data.bytesize < offset + 9
|
|
174
|
+
|
|
175
|
+
[data.byteslice(offset + 1, 8).unpack1('Q<'), 9]
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -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
|