Module: BSV::Wallet::Wire::Frame

Defined in:
lib/bsv/wallet/wire/frame.rb

Overview

BRC-103 request and result frame codec.

Port of go-sdk/wallet/serializer/frame.go. Two frame types:

Request frame (client → wallet):

[1 byte: call][1 byte: originator_len][originator_len bytes: UTF-8][remaining: params]

Result frame (wallet → client):

[1 byte: error_code]
On success (0x00): [remaining bytes: payload]
On error (non-zero):
  [VarInt: message_len][message bytes]
  [VarInt: stack_len][stack bytes]

Constant Summary collapse

MAX_ORIGINATOR_BYTES =

Maximum originator byte length enforced at write time. Matches the BRC-100 OriginatorDomainNameStringUnder250Bytes branded type used by Wire::Validation.originator_domain!.

250

Class Method Summary collapse

Class Method Details

.decode_varint(data, offset = 0) ⇒ Array(Integer, Integer)

Returns decoded value, bytes consumed.

Parameters:

  • data (String)

    binary data

  • offset (Integer) (defaults to: 0)

    byte offset

Returns:

  • (Array(Integer, Integer))

    decoded value, bytes consumed

Raises:

  • (ArgumentError)


157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/bsv/wallet/wire/frame.rb', line 157

def decode_varint(data, offset = 0)
  raise ArgumentError, "varint: need 1 byte at #{offset}" if offset >= data.bytesize

  first = data.getbyte(offset)
  case first
  when 0..0xFC
    [first, 1]
  when 0xFD
    raise ArgumentError, "varint: need 3 bytes at #{offset}" if data.bytesize < offset + 3

    [data.byteslice(offset + 1, 2).unpack1('v'), 3]
  when 0xFE
    raise ArgumentError, "varint: need 5 bytes at #{offset}" if data.bytesize < offset + 5

    [data.byteslice(offset + 1, 4).unpack1('V'), 5]
  when 0xFF
    raise ArgumentError, "varint: need 9 bytes at #{offset}" if data.bytesize < offset + 9

    [data.byteslice(offset + 1, 8).unpack1('Q<'), 9]
  end
end

.encode_varint(n) ⇒ String

Returns Bitcoin varint encoding.

Parameters:

  • n (Integer)

    unsigned integer

Returns:

  • (String)

    Bitcoin varint encoding



142
143
144
145
146
147
148
149
150
151
152
# File 'lib/bsv/wallet/wire/frame.rb', line 142

def encode_varint(n)
  if n < 0xFD
    [n].pack('C')
  elsif n <= 0xFFFF
    [0xFD, n].pack('Cv')
  elsif n <= 0xFFFFFFFF
    [0xFE, n].pack('CV')
  else
    [0xFF, n].pack('CQ<')
  end
end

.read_request(bytes) ⇒ Hash

Decode a request frame.

Parameters:

  • bytes (String)

    binary frame

Returns:

  • (Hash)

    { call: Integer, originator: String, params: String }

Raises:

  • (ArgumentError)

    if the frame is truncated or malformed



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/bsv/wallet/wire/frame.rb', line 54

def read_request(bytes)
  data = bytes.b
  raise ArgumentError, 'frame too short: need at least 2 bytes' if data.bytesize < 2

  call = data.getbyte(0)
  originator_len = data.getbyte(1)

  if data.bytesize < 2 + originator_len
    raise ArgumentError,
          "frame truncated: need #{2 + originator_len} bytes for originator, " \
          "got #{data.bytesize}"
  end

  originator = data.byteslice(2, originator_len).force_encoding('UTF-8')
  raise ArgumentError, 'frame originator is not valid UTF-8' unless originator.valid_encoding?

  params = data.byteslice(2 + originator_len, data.bytesize - 2 - originator_len) || ''.b

  { call: call, originator: originator, params: params }
end

.read_result(bytes) ⇒ String

Decode a result frame.

Parameters:

  • bytes (String)

    binary frame

Returns:

  • (String)

    binary payload on success

Raises:

  • (BSV::Wallet::Error)

    the appropriate subclass on error

  • (ArgumentError)

    if the frame is truncated or malformed



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/bsv/wallet/wire/frame.rb', line 110

def read_result(bytes)
  data = bytes.b
  raise ArgumentError, 'result frame is empty' if data.empty?

  code = data.getbyte(0)

  return data.byteslice(1, data.bytesize - 1) || ''.b if code.zero?

  offset = 1
  msg_len, vi = decode_varint(data, offset)
  offset += vi

  raise ArgumentError, 'result frame truncated: message' if data.bytesize < offset + msg_len

  message = data.byteslice(offset, msg_len).force_encoding('UTF-8')
  raise ArgumentError, 'result frame error message is not valid UTF-8' unless message.valid_encoding?

  offset += msg_len

  stack_len, vi = decode_varint(data, offset)
  offset += vi

  raise ArgumentError, 'result frame truncated: stack' if data.bytesize < offset + stack_len

  stack = data.byteslice(offset, stack_len).force_encoding('UTF-8')
  raise ArgumentError, 'result frame stack is not valid UTF-8' unless stack.valid_encoding?

  raise BSV::Wallet.error_from_wire(code, message, stack)
end

.write_error(error:) ⇒ String

Encode an error result frame.

Parameters:

Returns:

  • (String)

    binary frame



90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/bsv/wallet/wire/frame.rb', line 90

def write_error(error:)
  wire = error.to_wire
  msg_bytes = wire[:message].to_s.b
  stack_bytes = wire[:stack].to_s.b

  buf = String.new(encoding: 'BINARY')
  buf << [wire[:code]].pack('C')
  buf << encode_varint(msg_bytes.bytesize)
  buf << msg_bytes
  buf << encode_varint(stack_bytes.bytesize)
  buf << stack_bytes
  buf
end

.write_request(call:, originator:, params: nil) ⇒ String

Encode a request frame.

Parameters:

  • call (Integer)

    call byte (1..28)

  • originator (String)

    originator domain (0..250 bytes UTF-8)

  • params (String, nil) (defaults to: nil)

    binary params payload

Returns:

  • (String)

    binary frame (ASCII-8BIT encoding)

Raises:

  • (ArgumentError)

    if originator exceeds 250 bytes



34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/bsv/wallet/wire/frame.rb', line 34

def write_request(call:, originator:, params: nil)
  originator_bytes = originator.to_s.b
  if originator_bytes.bytesize > MAX_ORIGINATOR_BYTES
    raise ArgumentError,
          "originator must be at most #{MAX_ORIGINATOR_BYTES} bytes, " \
          "got #{originator_bytes.bytesize}"
  end

  buf = String.new(encoding: 'BINARY')
  buf << [call, originator_bytes.bytesize].pack('CC')
  buf << originator_bytes
  buf << params.b if params && !params.empty?
  buf
end

.write_result(payload: nil) ⇒ String

Encode a success result frame.

Parameters:

  • payload (String, nil) (defaults to: nil)

    binary payload

Returns:

  • (String)

    binary frame



79
80
81
82
83
84
# File 'lib/bsv/wallet/wire/frame.rb', line 79

def write_result(payload: nil)
  buf = String.new(encoding: 'BINARY')
  buf << "\x00"
  buf << payload.b if payload && !payload.empty?
  buf
end