Module: BSV::Wallet::Serializer::Common

Defined in:
lib/bsv/wallet/serializer/common.rb

Overview

Shared binary encoding helpers for BRC-103 per-call serializers.

Port of the shared helpers in go-sdk/wallet/serializer/serializer.go. All methods operate on BSV::Wallet::Wire::Writer / Reader instances.

Constant Summary collapse

COUNTERPARTY_SELF =

0x0B

11
COUNTERPARTY_ANYONE =

0x0C

12
PUBKEY_SIZE =

compressed secp256k1 public key

33
SEND_WITH_STATUS_UNPROVEN =

Send-with result status codes (Go status.go).

1
SEND_WITH_STATUS_SENDING =
2
SEND_WITH_STATUS_FAILED =
3
SEND_WITH_STATUS_CODES =
{
  unproven: SEND_WITH_STATUS_UNPROVEN,
  sending: SEND_WITH_STATUS_SENDING,
  failed: SEND_WITH_STATUS_FAILED
}.freeze
SEND_WITH_CODE_STATUSES =
SEND_WITH_STATUS_CODES.invert.freeze

Class Method Summary collapse

Class Method Details

.decode_outpoints(bytes) ⇒ Array<String>?

Decode a list of outpoints from binary (encoded as encode_outpoints).

Parameters:

  • bytes (String, nil)

    binary data

Returns:

  • (Array<String>, nil)

    array of “txid_hex.vout” strings



145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/bsv/wallet/serializer/common.rb', line 145

def decode_outpoints(bytes)
  return nil if bytes.nil? || bytes.b.empty?

  r = Wire::Reader.new(bytes)
  count = r.read_varint
  return nil if count == 0xFFFF_FFFF_FFFF_FFFF

  count.times.map do
    txid_hex = r.read_bytes(32).unpack1('H*')
    vout = r.read_varint
    "#{txid_hex}.#{vout}"
  end
end

.encode_outpoints(outpoints) ⇒ String?

Encode a list of outpoints (varint count + 32-byte wire-order txid + varint vout each). Returns nil bytes for nil input.

Parameters:

  • outpoints (Array<String>, nil)

    array of “txid_hex.vout” strings

Returns:

  • (String, nil)

    binary or nil



129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/bsv/wallet/serializer/common.rb', line 129

def encode_outpoints(outpoints)
  return nil if outpoints.nil?

  w = Wire::Writer.new
  w.write_varint(outpoints.length)
  outpoints.each do |op|
    txid_hex, vout = op.split('.')
    w.write_bytes([txid_hex].pack('H*'))
    w.write_varint(vout.to_i)
  end
  w.buf
end

.read_counterparty(reader) ⇒ Object



61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/bsv/wallet/serializer/common.rb', line 61

def read_counterparty(reader)
  first = reader.read_byte
  case first
  when COUNTERPARTY_SELF
    'self'
  when COUNTERPARTY_ANYONE
    'anyone'
  else
    rest = reader.read_bytes(PUBKEY_SIZE - 1)
    ([first].pack('C') + rest).unpack1('H*')
  end
end


111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/bsv/wallet/serializer/common.rb', line 111

def read_key_related_params(reader)
  protocol_id       = read_protocol(reader)
  key_id            = reader.read_str_with_varint_len
  counterparty      = read_counterparty(reader)
  privileged, reason = read_privileged_params(reader)
  {
    protocol_id: protocol_id,
    key_id: key_id,
    counterparty: counterparty,
    privileged: privileged,
    privileged_reason: reason
  }
end

.read_privileged_params(reader) ⇒ Object



88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/bsv/wallet/serializer/common.rb', line 88

def read_privileged_params(reader)
  privileged = reader.read_optional_bool
  # 0xFF as the leading byte is the nil-reason sentinel. Otherwise the
  # bytes start a Bitcoin varint length prefix (which can be 0xFD or
  # 0xFE for reasons >= 253 bytes; 0xFF would only collide if the
  # reason exceeded 4 GiB, which the protocol does not permit).
  if reader.peek_byte == 0xFF
    reader.read_byte
    [privileged, nil]
  else
    [privileged, reader.read_str_with_varint_len]
  end
end

.read_protocol(reader) ⇒ Object



38
39
40
41
42
# File 'lib/bsv/wallet/serializer/common.rb', line 38

def read_protocol(reader)
  level = reader.read_byte
  name  = reader.read_str_with_varint_len
  [level, name]
end

.read_send_with_results(reader) ⇒ Array<Hash>?

Read a send_with_results array.

Parameters:

Returns:

  • (Array<Hash>, nil)


191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/bsv/wallet/serializer/common.rb', line 191

def read_send_with_results(reader)
  count = reader.read_varint
  return nil if count.zero?

  count.times.map do
    txid_hex = reader.read_bytes(32).unpack1('H*')
    code = reader.read_byte
    status = SEND_WITH_CODE_STATUSES.fetch(code) do
      raise ArgumentError, "invalid send_with status code: #{code}"
    end
    { txid: txid_hex, status: status }
  end
end

.to_binary(bytes) ⇒ Object

Coerce a byte payload to a binary String (ASCII-8BIT encoding).

Accepts either an Array<Integer> (as returned by ProtoWallet) or a String. Serialisers use this so they remain compatible with both the in-process wallet interface (Arrays) and the wire interface (Strings).



22
23
24
25
26
27
# File 'lib/bsv/wallet/serializer/common.rb', line 22

def to_binary(bytes)
  return ''.b if bytes.nil?
  return bytes.pack('C*').b if bytes.is_a?(Array)

  bytes.b
end

.write_counterparty(writer, counterparty) ⇒ Object

Encode a counterparty: ‘self’ | ‘anyone’ | 66-char hex compressed pubkey.

Wire format:

0x0B — self
0x0C — anyone
02/03 <32 bytes> — specific pubkey (first byte is the prefix of the 33-byte key)


50
51
52
53
54
55
56
57
58
59
# File 'lib/bsv/wallet/serializer/common.rb', line 50

def write_counterparty(writer, counterparty)
  case counterparty
  when 'self'
    writer.write_byte(COUNTERPARTY_SELF)
  when 'anyone'
    writer.write_byte(COUNTERPARTY_ANYONE)
  else
    writer.write_bytes([counterparty].pack('H*'))
  end
end

Encode key-related params: protocol + key_id + counterparty + privileged params.



103
104
105
106
107
108
109
# File 'lib/bsv/wallet/serializer/common.rb', line 103

def write_key_related_params(writer, protocol_id:, key_id:, counterparty:,
                             privileged: nil, privileged_reason: nil)
  write_protocol(writer, protocol_id)
  writer.write_str_with_varint_len(key_id.to_s)
  write_counterparty(writer, counterparty)
  write_privileged_params(writer, privileged, privileged_reason)
end

.write_privileged_params(writer, privileged, privileged_reason) ⇒ Object

Encode privileged flag + privileged_reason.

Wire format: optional_bool (0xFF=nil, 0x00=false, 0x01=true) + reason as varint-len string, or 0xFF if reason is nil/empty (NegativeOneByte sentinel).



78
79
80
81
82
83
84
85
86
# File 'lib/bsv/wallet/serializer/common.rb', line 78

def write_privileged_params(writer, privileged, privileged_reason)
  writer.write_optional_bool(privileged)
  reason = privileged_reason.to_s
  if reason.empty?
    writer.write_byte(0xFF)
  else
    writer.write_str_with_varint_len(reason)
  end
end

.write_protocol(writer, protocol_id) ⇒ Object

Encode a BRC-43 protocol ID: [security_level (0-2), protocol_name].

Wire format: 1-byte security level + varint-len string.



32
33
34
35
36
# File 'lib/bsv/wallet/serializer/common.rb', line 32

def write_protocol(writer, protocol_id)
  level, name = protocol_id
  writer.write_byte(level.to_i)
  writer.write_str_with_varint_len(name.to_s)
end

.write_send_with_results(writer, results) ⇒ Object

Write a send_with_results array: varint count + txid (32 bytes) + status byte each. nil or empty → writes 0 count.

Parameters:

  • writer (Wire::Writer)
  • results (Array<Hash>, nil)

    array of { txid: String, status: Symbol }



176
177
178
179
180
181
182
183
184
185
186
# File 'lib/bsv/wallet/serializer/common.rb', line 176

def write_send_with_results(writer, results)
  arr = results || []
  writer.write_varint(arr.length)
  arr.each do |res|
    writer.write_bytes([res[:txid]].pack('H*'))
    code = SEND_WITH_STATUS_CODES.fetch(res[:status]) do
      raise ArgumentError, "invalid send_with status: #{res[:status]}"
    end
    writer.write_byte(code)
  end
end