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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +5 -3
  4. data/lib/bsv/network/protocols/arc.rb +4 -30
  5. data/lib/bsv/network/protocols/arcade.rb +163 -0
  6. data/lib/bsv/network/protocols/chaintracks.rb +6 -3
  7. data/lib/bsv/network/protocols/jungle_bus.rb +6 -0
  8. data/lib/bsv/network/protocols.rb +1 -0
  9. data/lib/bsv/network/providers/gorilla_pool.rb +18 -18
  10. data/lib/bsv/network/util.rb +44 -0
  11. data/lib/bsv/network.rb +1 -0
  12. data/lib/bsv/transaction/chain_tracker.rb +74 -13
  13. data/lib/bsv/transaction/chain_trackers.rb +0 -10
  14. data/lib/bsv/transaction/fee_models/live_policy.rb +10 -8
  15. data/lib/bsv/version.rb +1 -1
  16. data/lib/bsv/wallet/errors.rb +65 -21
  17. data/lib/bsv/wallet/proto_wallet/validators.rb +7 -49
  18. data/lib/bsv/wallet/proto_wallet.rb +14 -1
  19. data/lib/bsv/wallet/serializer/abort_action.rb +38 -0
  20. data/lib/bsv/wallet/serializer/acquire_certificate.rb +171 -0
  21. data/lib/bsv/wallet/serializer/certificate.rb +184 -0
  22. data/lib/bsv/wallet/serializer/common.rb +207 -0
  23. data/lib/bsv/wallet/serializer/create_action_args.rb +259 -0
  24. data/lib/bsv/wallet/serializer/create_action_result.rb +85 -0
  25. data/lib/bsv/wallet/serializer/create_hmac.rb +67 -0
  26. data/lib/bsv/wallet/serializer/create_signature.rb +90 -0
  27. data/lib/bsv/wallet/serializer/decrypt.rb +60 -0
  28. data/lib/bsv/wallet/serializer/discover_by_attributes.rb +61 -0
  29. data/lib/bsv/wallet/serializer/discover_by_identity_key.rb +49 -0
  30. data/lib/bsv/wallet/serializer/discover_certificates_result.rb +39 -0
  31. data/lib/bsv/wallet/serializer/encrypt.rb +60 -0
  32. data/lib/bsv/wallet/serializer/get_header_for_height.rb +71 -0
  33. data/lib/bsv/wallet/serializer/get_height.rb +46 -0
  34. data/lib/bsv/wallet/serializer/get_network.rb +65 -0
  35. data/lib/bsv/wallet/serializer/get_public_key.rb +86 -0
  36. data/lib/bsv/wallet/serializer/get_version.rb +44 -0
  37. data/lib/bsv/wallet/serializer/internalize_action.rb +151 -0
  38. data/lib/bsv/wallet/serializer/list_actions.rb +348 -0
  39. data/lib/bsv/wallet/serializer/list_certificates.rb +124 -0
  40. data/lib/bsv/wallet/serializer/list_outputs.rb +167 -0
  41. data/lib/bsv/wallet/serializer/prove_certificate.rb +146 -0
  42. data/lib/bsv/wallet/serializer/relinquish_certificate.rb +56 -0
  43. data/lib/bsv/wallet/serializer/relinquish_output.rb +44 -0
  44. data/lib/bsv/wallet/serializer/reveal_counterparty_key_linkage.rb +108 -0
  45. data/lib/bsv/wallet/serializer/reveal_specific_key_linkage.rb +116 -0
  46. data/lib/bsv/wallet/serializer/sign_action_args.rb +94 -0
  47. data/lib/bsv/wallet/serializer/sign_action_result.rb +49 -0
  48. data/lib/bsv/wallet/serializer/status.rb +85 -0
  49. data/lib/bsv/wallet/serializer/verify_hmac.rb +67 -0
  50. data/lib/bsv/wallet/serializer/verify_signature.rb +101 -0
  51. data/lib/bsv/wallet/serializer.rb +180 -0
  52. data/lib/bsv/wallet/substrates/http_wallet_json.rb +129 -0
  53. data/lib/bsv/wallet/substrates/http_wallet_wire.rb +99 -0
  54. data/lib/bsv/wallet/wallet_wire.rb +20 -0
  55. data/lib/bsv/wallet/wallet_wire_processor.rb +61 -0
  56. data/lib/bsv/wallet/wallet_wire_transceiver.rb +61 -0
  57. data/lib/bsv/wallet/wire/calls.rb +79 -0
  58. data/lib/bsv/wallet/wire/frame.rb +181 -0
  59. data/lib/bsv/wallet/wire/reader_writer.rb +402 -0
  60. data/lib/bsv/wallet/wire/validation.rb +213 -0
  61. data/lib/bsv/wallet/wire.rb +13 -0
  62. data/lib/bsv/wallet.rb +17 -0
  63. metadata +46 -2
  64. data/lib/bsv/transaction/chain_trackers/chaintracks.rb +0 -83
@@ -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
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ module Serializer
6
+ # BRC-103 serialiser for create_action args (call byte 1).
7
+ #
8
+ # Wire layout (port of go-sdk/wallet/serializer/create_action_args.go):
9
+ # [string] description
10
+ # [optional_bytes] input_beef (NegativeOne = nil)
11
+ # [inputs] NegativeOne | varint_count + input_record…
12
+ # [outputs] NegativeOne | varint_count + output_record…
13
+ # [optional_uint32] lock_time
14
+ # [optional_uint32] version
15
+ # [string_slice] labels
16
+ # [options_block] 0x00 = absent | 0x01 + fields
17
+ #
18
+ # Input record:
19
+ # [32 bytes] outpoint txid (wire order)
20
+ # [varint] outpoint vout
21
+ # [int_bytes or NegativeOne + varint] unlocking_script or length placeholder
22
+ # [string] input_description
23
+ # [optional_uint32] sequence_number
24
+ #
25
+ # Output record:
26
+ # [int_bytes] locking_script
27
+ # [varint] satoshis
28
+ # [string] output_description
29
+ # [optional_string] basket
30
+ # [optional_string] custom_instructions
31
+ # [string_slice] tags
32
+ #
33
+ # Options block (after 0x01 presence byte):
34
+ # [optional_bool] sign_and_process
35
+ # [optional_bool] accept_delayed_broadcast
36
+ # [0x01 or 0xFF] trust_self (1=known, 0xFF=absent)
37
+ # [txid_slice] known_txids
38
+ # [optional_bool] return_txid_only
39
+ # [optional_bool] no_send
40
+ # [optional_bytes] no_send_change (encoded outpoints, NegativeOne = nil)
41
+ # [txid_slice] send_with
42
+ # [optional_bool] randomize_outputs
43
+ module CreateActionArgs
44
+ TRUST_SELF_KNOWN = 1
45
+
46
+ module_function
47
+
48
+ # @param args [Hash]
49
+ # @return [String] binary
50
+ def serialize(args)
51
+ w = Wire::Writer.new
52
+
53
+ w.write_string(args[:description].to_s)
54
+ input_beef = args[:input_beef]
55
+ if input_beef.nil? || input_beef.b.empty?
56
+ w.write_negative_one
57
+ else
58
+ w.write_int_bytes(input_beef)
59
+ end
60
+
61
+ serialize_inputs(w, args[:inputs])
62
+ serialize_outputs(w, args[:outputs])
63
+
64
+ w.write_optional_uint32(args[:lock_time])
65
+ w.write_optional_uint32(args[:version])
66
+ w.write_string_slice(args[:labels])
67
+
68
+ serialize_options(w, args[:options])
69
+
70
+ w.buf
71
+ end
72
+
73
+ # @param bytes [String] binary
74
+ # @return [Hash]
75
+ def deserialize(bytes)
76
+ raise ArgumentError, 'empty message' if bytes.b.empty?
77
+
78
+ r = Wire::Reader.new(bytes)
79
+
80
+ description = r.read_string
81
+ input_beef = r.read_int_bytes
82
+ input_beef = nil if input_beef.empty?
83
+
84
+ inputs = deserialize_inputs(r)
85
+ outputs = deserialize_outputs(r)
86
+
87
+ lock_time = r.read_optional_uint32
88
+ version = r.read_optional_uint32
89
+ labels = r.read_string_slice
90
+
91
+ options = deserialize_options(r)
92
+
93
+ result = { description: description }
94
+ result[:input_beef] = input_beef unless input_beef.nil?
95
+ result[:inputs] = inputs unless inputs.nil?
96
+ result[:outputs] = outputs unless outputs.nil?
97
+ result[:lock_time] = lock_time unless lock_time.nil?
98
+ result[:version] = version unless version.nil?
99
+ result[:labels] = labels unless labels.nil?
100
+ result[:options] = options unless options.nil?
101
+ result
102
+ end
103
+
104
+ def serialize_inputs(writer, inputs)
105
+ if inputs.nil?
106
+ writer.write_negative_one
107
+ return
108
+ end
109
+
110
+ writer.write_varint(inputs.length)
111
+ inputs.each do |inp|
112
+ txid_hex, vout = inp[:outpoint].split('.')
113
+ writer.write_bytes([txid_hex].pack('H*'))
114
+ writer.write_varint(vout.to_i)
115
+
116
+ script = inp[:unlocking_script]
117
+ if script && !script.b.empty?
118
+ writer.write_int_bytes(script)
119
+ else
120
+ writer.write_negative_one
121
+ writer.write_varint(inp[:unlocking_script_length].to_i)
122
+ end
123
+
124
+ writer.write_string(inp[:input_description].to_s)
125
+ writer.write_optional_uint32(inp[:sequence_number])
126
+ end
127
+ end
128
+
129
+ def serialize_outputs(writer, outputs)
130
+ if outputs.nil?
131
+ writer.write_negative_one
132
+ return
133
+ end
134
+
135
+ writer.write_varint(outputs.length)
136
+ outputs.each do |out|
137
+ writer.write_int_bytes(out[:locking_script])
138
+ writer.write_varint(out[:satoshis].to_i)
139
+ writer.write_string(out[:output_description].to_s)
140
+ writer.write_optional_string(out[:basket])
141
+ writer.write_optional_string(out[:custom_instructions])
142
+ writer.write_string_slice(out[:tags])
143
+ end
144
+ end
145
+
146
+ def serialize_options(writer, opts)
147
+ if opts.nil?
148
+ writer.write_byte(0)
149
+ return
150
+ end
151
+
152
+ writer.write_byte(1)
153
+ writer.write_optional_bool(opts[:sign_and_process])
154
+ writer.write_optional_bool(opts[:accept_delayed_broadcast])
155
+
156
+ if opts[:trust_self] == :known
157
+ writer.write_byte(TRUST_SELF_KNOWN)
158
+ else
159
+ writer.write_byte(0xFF)
160
+ end
161
+
162
+ writer.write_txid_slice(opts[:known_txids])
163
+ writer.write_optional_bool(opts[:return_txid_only])
164
+ writer.write_optional_bool(opts[:no_send])
165
+
166
+ no_send_change_bytes = Common.encode_outpoints(opts[:no_send_change])
167
+ writer.write_int_bytes(no_send_change_bytes)
168
+
169
+ writer.write_txid_slice(opts[:send_with])
170
+ writer.write_optional_bool(opts[:randomize_outputs])
171
+ end
172
+
173
+ def deserialize_inputs(reader)
174
+ count = reader.read_varint
175
+ return nil if count == 0xFFFF_FFFF_FFFF_FFFF
176
+
177
+ count.times.map do
178
+ txid_hex = reader.read_bytes(32).unpack1('H*')
179
+ vout = reader.read_varint
180
+ outpoint = "#{txid_hex}.#{vout}"
181
+
182
+ script_len = reader.read_varint
183
+ inp = { outpoint: outpoint }
184
+
185
+ if script_len == 0xFFFF_FFFF_FFFF_FFFF
186
+ length = reader.read_varint
187
+ inp[:unlocking_script_length] = length
188
+ else
189
+ script = reader.read_bytes(script_len)
190
+ inp[:unlocking_script] = script
191
+ inp[:unlocking_script_length] = script_len
192
+ end
193
+
194
+ inp[:input_description] = reader.read_string
195
+ seq = reader.read_optional_uint32
196
+ inp[:sequence_number] = seq unless seq.nil?
197
+ inp
198
+ end
199
+ end
200
+
201
+ def deserialize_outputs(reader)
202
+ count = reader.read_varint
203
+ return nil if count == 0xFFFF_FFFF_FFFF_FFFF
204
+
205
+ count.times.map do
206
+ script = reader.read_int_bytes
207
+ satoshis = reader.read_varint
208
+ out_desc = reader.read_string
209
+ basket = reader.read_optional_string
210
+ custom = reader.read_optional_string
211
+ tags = reader.read_string_slice
212
+
213
+ out = { locking_script: script, satoshis: satoshis, output_description: out_desc }
214
+ out[:basket] = basket unless basket.nil?
215
+ out[:custom_instructions] = custom unless custom.nil?
216
+ out[:tags] = tags unless tags.nil?
217
+ out
218
+ end
219
+ end
220
+
221
+ def deserialize_options(reader)
222
+ return nil if reader.read_byte != 1
223
+
224
+ opts = {}
225
+
226
+ sign_and_process = reader.read_optional_bool
227
+ accept_delayed_broadcast = reader.read_optional_bool
228
+
229
+ trust_byte = reader.read_byte
230
+ trust_self = trust_byte == TRUST_SELF_KNOWN ? :known : nil
231
+
232
+ known_txids = reader.read_txid_slice
233
+ return_txid_only = reader.read_optional_bool
234
+ no_send = reader.read_optional_bool
235
+
236
+ no_send_change_bytes = reader.read_int_bytes
237
+ no_send_change = Common.decode_outpoints(no_send_change_bytes.empty? ? nil : no_send_change_bytes)
238
+
239
+ send_with = reader.read_txid_slice
240
+ randomize_outputs = reader.read_optional_bool
241
+
242
+ opts[:sign_and_process] = sign_and_process unless sign_and_process.nil?
243
+ opts[:accept_delayed_broadcast] = accept_delayed_broadcast unless accept_delayed_broadcast.nil?
244
+ opts[:trust_self] = trust_self unless trust_self.nil?
245
+ opts[:known_txids] = known_txids unless known_txids.nil?
246
+ opts[:return_txid_only] = return_txid_only unless return_txid_only.nil?
247
+ opts[:no_send] = no_send unless no_send.nil?
248
+ opts[:no_send_change] = no_send_change unless no_send_change.nil?
249
+ opts[:send_with] = send_with unless send_with.nil?
250
+ opts[:randomize_outputs] = randomize_outputs unless randomize_outputs.nil?
251
+ opts
252
+ end
253
+
254
+ private_class_method :serialize_inputs, :serialize_outputs, :serialize_options,
255
+ :deserialize_inputs, :deserialize_outputs, :deserialize_options
256
+ end
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ module Serializer
6
+ # BRC-103 serialiser for create_action result (call byte 1).
7
+ #
8
+ # Wire layout (port of go-sdk/wallet/serializer/create_action_result.go):
9
+ # [1 byte] 0x00 success sentinel (frame-level, already checked by Frame)
10
+ # [flag + 32 bytes] txid (wire-order) with flag byte: 0=absent, 1=present
11
+ # [flag + int_bytes] tx (BEEF bytes) with flag byte: 0=absent, 1=present + varint_len
12
+ # [optional_bytes] no_send_change encoded outpoints (NegativeOne = nil)
13
+ # [send_with_results] varint count + txid + status_byte each
14
+ # [1 byte flag] signable_transaction: 0=absent, 1=present
15
+ # If signable_transaction present:
16
+ # [int_bytes] tx (BEEF bytes)
17
+ # [int_bytes] reference bytes
18
+ module CreateActionResult
19
+ module_function
20
+
21
+ # @param result [Hash]
22
+ # @return [String] binary
23
+ def serialize(result)
24
+ w = Wire::Writer.new
25
+ w.write_byte(0) # success sentinel
26
+
27
+ txid_bytes = result[:txid] ? [result[:txid]].pack('H*').reverse : nil
28
+ w.write_optional_bytes_with_flag(txid_bytes, fixed_size: 32)
29
+ w.write_optional_bytes_with_flag(result[:tx])
30
+
31
+ no_send_change_bytes = Common.encode_outpoints(result[:no_send_change])
32
+ w.write_int_bytes(no_send_change_bytes)
33
+
34
+ Common.write_send_with_results(w, result[:send_with_results])
35
+
36
+ signable = result[:signable_transaction]
37
+ if signable
38
+ w.write_byte(1)
39
+ w.write_int_bytes(signable[:tx])
40
+ w.write_int_bytes(signable[:reference])
41
+ else
42
+ w.write_byte(0)
43
+ end
44
+
45
+ w.buf
46
+ end
47
+
48
+ # @param bytes [String] binary
49
+ # @return [Hash]
50
+ def deserialize(bytes)
51
+ raise ArgumentError, 'empty response data' if bytes.b.empty?
52
+
53
+ r = Wire::Reader.new(bytes)
54
+
55
+ status = r.read_byte
56
+ raise ArgumentError, "response indicates failure: #{status}" unless status.zero?
57
+
58
+ txid_raw = r.read_optional_bytes_with_flag(fixed_size: 32)
59
+ txid_hex = txid_raw&.reverse&.unpack1('H*')
60
+ tx = r.read_optional_bytes_with_flag
61
+
62
+ no_send_change_bytes = r.read_int_bytes
63
+ no_send_change = Common.decode_outpoints(no_send_change_bytes.empty? ? nil : no_send_change_bytes)
64
+
65
+ send_with_results = Common.read_send_with_results(r)
66
+
67
+ signable_flag = r.read_byte
68
+ signable_transaction = if signable_flag == 1
69
+ tx_bytes = r.read_int_bytes
70
+ ref_bytes = r.read_int_bytes
71
+ { tx: tx_bytes, reference: ref_bytes }
72
+ end
73
+
74
+ result = {}
75
+ result[:txid] = txid_hex unless txid_hex.nil?
76
+ result[:tx] = tx unless tx.nil?
77
+ result[:no_send_change] = no_send_change unless no_send_change.nil?
78
+ result[:send_with_results] = send_with_results unless send_with_results.nil?
79
+ result[:signable_transaction] = signable_transaction unless signable_transaction.nil?
80
+ result
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ module Serializer
6
+ # BRC-103 wire codec for the +create_hmac+ call (call byte 13).
7
+ #
8
+ # Port of go-sdk/wallet/serializer/create_hmac.go.
9
+ module CreateHmac
10
+ HMAC_SIZE = 32
11
+
12
+ # Args wire layout:
13
+ # [key-related params]
14
+ # [VarInt data_len][data bytes]
15
+ # [optional_bool seek_permission]
16
+ module Args
17
+ module_function
18
+
19
+ def serialize(args)
20
+ w = BSV::Wallet::Wire::Writer.new
21
+ Common.write_key_related_params(
22
+ w,
23
+ protocol_id: args[:protocol_id],
24
+ key_id: args[:key_id],
25
+ counterparty: args[:counterparty],
26
+ privileged: args[:privileged],
27
+ privileged_reason: args[:privileged_reason]
28
+ )
29
+ data = Common.to_binary(args[:data])
30
+ w.write_varint(data.bytesize)
31
+ w.write_bytes(data)
32
+ w.write_optional_bool(args[:seek_permission])
33
+ w.buf
34
+ end
35
+
36
+ def deserialize(bytes)
37
+ r = BSV::Wallet::Wire::Reader.new(bytes)
38
+ params = Common.read_key_related_params(r)
39
+ len = r.read_varint
40
+ data = r.read_bytes(len)
41
+ seek_permission = r.read_optional_bool
42
+ params.merge(data: data, seek_permission: seek_permission)
43
+ end
44
+ end
45
+
46
+ # Result wire layout:
47
+ # [32 bytes: HMAC-SHA-256]
48
+ module Result
49
+ module_function
50
+
51
+ def serialize(result)
52
+ hmac = Common.to_binary(result[:hmac])
53
+ raise ArgumentError, "HMAC must be #{HMAC_SIZE} bytes, got #{hmac.bytesize}" unless hmac.bytesize == HMAC_SIZE
54
+
55
+ hmac
56
+ end
57
+
58
+ def deserialize(bytes)
59
+ raise ArgumentError, "HMAC result too short: #{bytes.bytesize}" if bytes.bytesize < HMAC_SIZE
60
+
61
+ { hmac: bytes.byteslice(0, HMAC_SIZE) }
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end