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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +106 -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 +27 -4
  7. data/lib/bsv/network/protocols/jungle_bus.rb +34 -0
  8. data/lib/bsv/network/protocols/woc_rest.rb +28 -1
  9. data/lib/bsv/network/protocols.rb +1 -0
  10. data/lib/bsv/network/providers/gorilla_pool.rb +18 -18
  11. data/lib/bsv/network/util.rb +44 -0
  12. data/lib/bsv/network.rb +1 -0
  13. data/lib/bsv/transaction/chain_tracker.rb +66 -13
  14. data/lib/bsv/transaction/chain_trackers.rb +0 -10
  15. data/lib/bsv/transaction/fee_models/live_policy.rb +10 -8
  16. data/lib/bsv/version.rb +1 -1
  17. data/lib/bsv/wallet/errors.rb +65 -21
  18. data/lib/bsv/wallet/proto_wallet/validators.rb +7 -49
  19. data/lib/bsv/wallet/proto_wallet.rb +14 -1
  20. data/lib/bsv/wallet/serializer/abort_action.rb +38 -0
  21. data/lib/bsv/wallet/serializer/acquire_certificate.rb +171 -0
  22. data/lib/bsv/wallet/serializer/certificate.rb +184 -0
  23. data/lib/bsv/wallet/serializer/common.rb +207 -0
  24. data/lib/bsv/wallet/serializer/create_action_args.rb +259 -0
  25. data/lib/bsv/wallet/serializer/create_action_result.rb +85 -0
  26. data/lib/bsv/wallet/serializer/create_hmac.rb +67 -0
  27. data/lib/bsv/wallet/serializer/create_signature.rb +90 -0
  28. data/lib/bsv/wallet/serializer/decrypt.rb +60 -0
  29. data/lib/bsv/wallet/serializer/discover_by_attributes.rb +61 -0
  30. data/lib/bsv/wallet/serializer/discover_by_identity_key.rb +49 -0
  31. data/lib/bsv/wallet/serializer/discover_certificates_result.rb +39 -0
  32. data/lib/bsv/wallet/serializer/encrypt.rb +60 -0
  33. data/lib/bsv/wallet/serializer/get_header_for_height.rb +71 -0
  34. data/lib/bsv/wallet/serializer/get_height.rb +46 -0
  35. data/lib/bsv/wallet/serializer/get_network.rb +65 -0
  36. data/lib/bsv/wallet/serializer/get_public_key.rb +86 -0
  37. data/lib/bsv/wallet/serializer/get_version.rb +44 -0
  38. data/lib/bsv/wallet/serializer/internalize_action.rb +151 -0
  39. data/lib/bsv/wallet/serializer/list_actions.rb +348 -0
  40. data/lib/bsv/wallet/serializer/list_certificates.rb +124 -0
  41. data/lib/bsv/wallet/serializer/list_outputs.rb +167 -0
  42. data/lib/bsv/wallet/serializer/prove_certificate.rb +146 -0
  43. data/lib/bsv/wallet/serializer/relinquish_certificate.rb +56 -0
  44. data/lib/bsv/wallet/serializer/relinquish_output.rb +44 -0
  45. data/lib/bsv/wallet/serializer/reveal_counterparty_key_linkage.rb +108 -0
  46. data/lib/bsv/wallet/serializer/reveal_specific_key_linkage.rb +116 -0
  47. data/lib/bsv/wallet/serializer/sign_action_args.rb +94 -0
  48. data/lib/bsv/wallet/serializer/sign_action_result.rb +49 -0
  49. data/lib/bsv/wallet/serializer/status.rb +85 -0
  50. data/lib/bsv/wallet/serializer/verify_hmac.rb +67 -0
  51. data/lib/bsv/wallet/serializer/verify_signature.rb +101 -0
  52. data/lib/bsv/wallet/serializer.rb +180 -0
  53. data/lib/bsv/wallet/substrates/http_wallet_json.rb +129 -0
  54. data/lib/bsv/wallet/substrates/http_wallet_wire.rb +99 -0
  55. data/lib/bsv/wallet/wallet_wire.rb +20 -0
  56. data/lib/bsv/wallet/wallet_wire_processor.rb +61 -0
  57. data/lib/bsv/wallet/wallet_wire_transceiver.rb +61 -0
  58. data/lib/bsv/wallet/wire/calls.rb +79 -0
  59. data/lib/bsv/wallet/wire/frame.rb +181 -0
  60. data/lib/bsv/wallet/wire/reader_writer.rb +402 -0
  61. data/lib/bsv/wallet/wire/validation.rb +213 -0
  62. data/lib/bsv/wallet/wire.rb +13 -0
  63. data/lib/bsv/wallet.rb +17 -0
  64. metadata +46 -2
  65. data/lib/bsv/transaction/chain_trackers/chaintracks.rb +0 -83
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ module Serializer
6
+ # BRC-103 wire codec for the +get_public_key+ call (call byte 8).
7
+ #
8
+ # Port of go-sdk/wallet/serializer/get_public_key.go.
9
+ module GetPublicKey
10
+ IDENTITY_KEY_FLAG = 1
11
+ PUBKEY_SIZE = 33
12
+
13
+ # Args wire layout:
14
+ # [1 byte: identity_key flag — 0=no, 1=yes]
15
+ # If 0: [key-related params][optional_bool for_self]
16
+ # If 1: [privileged params only]
17
+ # [optional_bool seek_permission]
18
+ module Args
19
+ module_function
20
+
21
+ def serialize(args)
22
+ identity_key = args[:identity_key]
23
+ w = BSV::Wallet::Wire::Writer.new
24
+
25
+ if identity_key
26
+ w.write_byte(IDENTITY_KEY_FLAG)
27
+ Common.write_privileged_params(w, args[:privileged], args[:privileged_reason])
28
+ else
29
+ w.write_byte(0)
30
+ Common.write_key_related_params(
31
+ w,
32
+ protocol_id: args[:protocol_id],
33
+ key_id: args[:key_id],
34
+ counterparty: args[:counterparty],
35
+ privileged: args[:privileged],
36
+ privileged_reason: args[:privileged_reason]
37
+ )
38
+ w.write_optional_bool(args[:for_self])
39
+ end
40
+
41
+ w.write_optional_bool(args[:seek_permission])
42
+ w.buf
43
+ end
44
+
45
+ def deserialize(bytes)
46
+ r = BSV::Wallet::Wire::Reader.new(bytes)
47
+ flag = r.read_byte
48
+
49
+ if flag == IDENTITY_KEY_FLAG
50
+ privileged, reason = Common.read_privileged_params(r)
51
+ seek_permission = r.read_optional_bool
52
+ {
53
+ identity_key: true,
54
+ privileged: privileged,
55
+ privileged_reason: reason,
56
+ seek_permission: seek_permission
57
+ }
58
+ else
59
+ params = Common.read_key_related_params(r)
60
+ for_self = r.read_optional_bool
61
+ seek_permission = r.read_optional_bool
62
+ params.merge(identity_key: false, for_self: for_self, seek_permission: seek_permission)
63
+ end
64
+ end
65
+ end
66
+
67
+ # Result wire layout:
68
+ # [33 bytes: compressed public key]
69
+ module Result
70
+ module_function
71
+
72
+ def serialize(result)
73
+ pubkey = result[:public_key] || ''.b
74
+ pubkey.bytesize == PUBKEY_SIZE ? pubkey.b : [pubkey.to_s].pack('H*')
75
+ end
76
+
77
+ def deserialize(bytes)
78
+ raise ArgumentError, "public key too short: #{bytes.bytesize}" if bytes.bytesize < PUBKEY_SIZE
79
+
80
+ { public_key: bytes.byteslice(0, PUBKEY_SIZE) }
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ module Serializer
6
+ # BRC-103 serialiser for the get_version call (call byte 28).
7
+ #
8
+ # Wire format — result only (no args payload):
9
+ # [N bytes] raw UTF-8 version string (no length prefix)
10
+ #
11
+ # Port of go-sdk/wallet/serializer/get_version.go — the Go SDK emits
12
+ # the string bytes directly, with no varint length prefix.
13
+ module GetVersion
14
+ module Args
15
+ module_function
16
+
17
+ def serialize(_args = {})
18
+ ''.b
19
+ end
20
+
21
+ def deserialize(_bytes)
22
+ {}
23
+ end
24
+ end
25
+
26
+ module Result
27
+ module_function
28
+
29
+ # @param result [Hash] { version: String }
30
+ # @return [String] binary (raw UTF-8 bytes)
31
+ def serialize(result)
32
+ result[:version].to_s.b
33
+ end
34
+
35
+ # @param bytes [String] binary
36
+ # @return [Hash] { version: String }
37
+ def deserialize(bytes)
38
+ { version: bytes.b.force_encoding('UTF-8') }
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ module Serializer
6
+ # BRC-103 serialiser for internalize_action (call byte 5).
7
+ #
8
+ # Wire layout (port of go-sdk/wallet/serializer/internalize_action.go):
9
+ # [varint + bytes] tx (BEEF) — length-prefixed raw bytes
10
+ # [varint] outputs count
11
+ # For each output:
12
+ # [varint] output_index
13
+ # [1 byte] protocol: 0x01=wallet_payment, 0x02=basket_insertion
14
+ # If wallet_payment:
15
+ # [33 bytes] sender_identity_key (compressed pubkey)
16
+ # [int_bytes] derivation_prefix
17
+ # [int_bytes] derivation_suffix
18
+ # If basket_insertion:
19
+ # [string] basket
20
+ # [optional_string] custom_instructions
21
+ # [string_slice] tags
22
+ # [string_slice] labels
23
+ # [string] description
24
+ # [optional_bool] seek_permission
25
+ #
26
+ # Result wire layout: empty (success is implicit from the frame error byte).
27
+ module InternalizeActionArgs
28
+ PROTOCOL_WALLET_PAYMENT = 1
29
+ PROTOCOL_BASKET_INSERTION = 2
30
+ PUBKEY_SIZE = 33
31
+
32
+ module_function
33
+
34
+ # @param args [Hash]
35
+ # @return [String] binary
36
+ def serialize(args)
37
+ w = Wire::Writer.new
38
+
39
+ tx = (args[:tx] || ''.b).b
40
+ w.write_varint(tx.bytesize)
41
+ w.write_bytes(tx)
42
+
43
+ outputs = args[:outputs] || []
44
+ w.write_varint(outputs.length)
45
+ outputs.each { |out| serialize_output(w, out) }
46
+
47
+ w.write_string_slice(args[:labels])
48
+ w.write_string(args[:description].to_s)
49
+ w.write_optional_bool(args[:seek_permission])
50
+
51
+ w.buf
52
+ end
53
+
54
+ # @param bytes [String] binary
55
+ # @return [Hash]
56
+ def deserialize(bytes)
57
+ r = Wire::Reader.new(bytes)
58
+
59
+ tx_len = r.read_varint
60
+ tx = r.read_bytes(tx_len)
61
+
62
+ output_count = r.read_varint
63
+ outputs = output_count.times.map { deserialize_output(r) }
64
+
65
+ labels = r.read_string_slice
66
+ description = r.read_string
67
+ seek_permission = r.read_optional_bool
68
+
69
+ result = { tx: tx, outputs: outputs, description: description }
70
+ result[:labels] = labels unless labels.nil?
71
+ result[:seek_permission] = seek_permission unless seek_permission.nil?
72
+ result
73
+ end
74
+
75
+ def serialize_output(writer, out)
76
+ writer.write_varint(out[:output_index].to_i)
77
+
78
+ case out[:protocol]
79
+ when :wallet_payment
80
+ writer.write_byte(PROTOCOL_WALLET_PAYMENT)
81
+ payment = out[:payment_remittance] || {}
82
+ writer.write_bytes([payment[:sender_identity_key]].pack('H*'))
83
+ writer.write_int_bytes(payment[:derivation_prefix])
84
+ writer.write_int_bytes(payment[:derivation_suffix])
85
+ when :basket_insertion
86
+ writer.write_byte(PROTOCOL_BASKET_INSERTION)
87
+ insertion = out[:insertion_remittance] || {}
88
+ writer.write_string(insertion[:basket].to_s)
89
+ writer.write_optional_string(insertion[:custom_instructions])
90
+ writer.write_string_slice(insertion[:tags])
91
+ else
92
+ raise ArgumentError, "unknown internalize protocol: #{out[:protocol]}"
93
+ end
94
+ end
95
+
96
+ def deserialize_output(reader)
97
+ output_index = reader.read_varint
98
+ protocol_byte = reader.read_byte
99
+
100
+ case protocol_byte
101
+ when PROTOCOL_WALLET_PAYMENT
102
+ key_bytes = reader.read_bytes(PUBKEY_SIZE)
103
+ prefix = reader.read_int_bytes
104
+ suffix = reader.read_int_bytes
105
+ {
106
+ output_index: output_index,
107
+ protocol: :wallet_payment,
108
+ payment_remittance: {
109
+ sender_identity_key: key_bytes.unpack1('H*'),
110
+ derivation_prefix: prefix,
111
+ derivation_suffix: suffix
112
+ }
113
+ }
114
+ when PROTOCOL_BASKET_INSERTION
115
+ basket = reader.read_string
116
+ custom = reader.read_optional_string
117
+ tags = reader.read_string_slice
118
+ result = {
119
+ output_index: output_index,
120
+ protocol: :basket_insertion,
121
+ insertion_remittance: { basket: basket }
122
+ }
123
+ result[:insertion_remittance][:custom_instructions] = custom unless custom.nil?
124
+ result[:insertion_remittance][:tags] = tags unless tags.nil?
125
+ result
126
+ else
127
+ raise ArgumentError, "invalid internalize protocol byte: #{protocol_byte}"
128
+ end
129
+ end
130
+
131
+ private_class_method :serialize_output, :deserialize_output
132
+ end
133
+
134
+ module InternalizeActionResult
135
+ module_function
136
+
137
+ # @param _result [Hash] ignored
138
+ # @return [String] empty binary
139
+ def serialize(_result = {})
140
+ ''.b
141
+ end
142
+
143
+ # @param _bytes [String] ignored
144
+ # @return [Hash] { accepted: true }
145
+ def deserialize(_bytes = nil)
146
+ { accepted: true }
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,348 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ module Serializer
6
+ # BRC-103 serialiser for list_actions (call byte 4).
7
+ #
8
+ # Wire layout (port of go-sdk/wallet/serializer/list_actions.go):
9
+ #
10
+ # Args:
11
+ # [string_slice] labels
12
+ # [1 byte] label_query_mode: 0x01=any, 0x02=all, 0xFF=absent
13
+ # [optional_bool] include_labels
14
+ # [optional_bool] include_inputs
15
+ # [optional_bool] include_input_source_locking_scripts
16
+ # [optional_bool] include_input_unlocking_scripts
17
+ # [optional_bool] include_outputs
18
+ # [optional_bool] include_output_locking_scripts
19
+ # [optional_uint32] limit
20
+ # [optional_uint32] offset
21
+ # [optional_bool] seek_permission
22
+ #
23
+ # Result:
24
+ # [varint] total_actions
25
+ # For each action:
26
+ # [32 bytes] txid (display order — reversed from wire storage)
27
+ # [varint] satoshis (int64 as varint)
28
+ # [1 byte] status code
29
+ # [optional_bool ptr] is_outgoing (written as optional_bool pointer in Go)
30
+ # [string] description
31
+ # [string_slice] labels
32
+ # [varint] version
33
+ # [varint] lock_time
34
+ # [inputs] NegativeOne | varint count + input_record…
35
+ # [outputs] NegativeOne | varint count + output_record…
36
+ #
37
+ # Input record:
38
+ # [36 bytes] source_outpoint (32-byte wire txid + varint vout)
39
+ # [varint] source_satoshis
40
+ # [int_bytes_optional] source_locking_script
41
+ # [int_bytes_optional] unlocking_script
42
+ # [string] input_description
43
+ # [varint] sequence_number
44
+ #
45
+ # Output record:
46
+ # [varint] output_index
47
+ # [varint] satoshis
48
+ # [int_bytes_optional] locking_script
49
+ # [optional_bool ptr] spendable
50
+ # [string] output_description
51
+ # [string] basket
52
+ # [string_slice] tags
53
+ # [optional_string] custom_instructions
54
+ module ListActionsArgs
55
+ LABEL_QUERY_MODE_ANY = 1
56
+ LABEL_QUERY_MODE_ALL = 2
57
+
58
+ module_function
59
+
60
+ # @param args [Hash]
61
+ # @return [String] binary
62
+ def serialize(args)
63
+ w = Wire::Writer.new
64
+
65
+ w.write_string_slice(args[:labels])
66
+
67
+ case args[:label_query_mode]
68
+ when :any
69
+ w.write_byte(LABEL_QUERY_MODE_ANY)
70
+ when :all
71
+ w.write_byte(LABEL_QUERY_MODE_ALL)
72
+ else
73
+ w.write_byte(0xFF)
74
+ end
75
+
76
+ w.write_optional_bool(args[:include_labels])
77
+ w.write_optional_bool(args[:include_inputs])
78
+ w.write_optional_bool(args[:include_input_source_locking_scripts])
79
+ w.write_optional_bool(args[:include_input_unlocking_scripts])
80
+ w.write_optional_bool(args[:include_outputs])
81
+ w.write_optional_bool(args[:include_output_locking_scripts])
82
+
83
+ w.write_optional_uint32(args[:limit])
84
+ w.write_optional_uint32(args[:offset])
85
+ w.write_optional_bool(args[:seek_permission])
86
+
87
+ w.buf
88
+ end
89
+
90
+ # @param bytes [String] binary
91
+ # @return [Hash]
92
+ def deserialize(bytes)
93
+ r = Wire::Reader.new(bytes)
94
+
95
+ labels = r.read_string_slice
96
+
97
+ mode_byte = r.read_byte
98
+ label_query_mode = case mode_byte
99
+ when LABEL_QUERY_MODE_ANY then :any
100
+ when LABEL_QUERY_MODE_ALL then :all
101
+ end
102
+
103
+ result = {}
104
+ result[:labels] = labels unless labels.nil?
105
+ result[:label_query_mode] = label_query_mode unless label_query_mode.nil?
106
+
107
+ v = r.read_optional_bool
108
+ result[:include_labels] = v unless v.nil?
109
+ v = r.read_optional_bool
110
+ result[:include_inputs] = v unless v.nil?
111
+ v = r.read_optional_bool
112
+ result[:include_input_source_locking_scripts] = v unless v.nil?
113
+ v = r.read_optional_bool
114
+ result[:include_input_unlocking_scripts] = v unless v.nil?
115
+ v = r.read_optional_bool
116
+ result[:include_outputs] = v unless v.nil?
117
+ v = r.read_optional_bool
118
+ result[:include_output_locking_scripts] = v unless v.nil?
119
+
120
+ lim = r.read_optional_uint32
121
+ result[:limit] = lim unless lim.nil?
122
+ off = r.read_optional_uint32
123
+ result[:offset] = off unless off.nil?
124
+
125
+ v = r.read_optional_bool
126
+ result[:seek_permission] = v unless v.nil?
127
+
128
+ result
129
+ end
130
+ end
131
+
132
+ module ListActionsResult
133
+ ACTION_STATUS_CODES = {
134
+ completed: 1,
135
+ unprocessed: 2,
136
+ sending: 3,
137
+ unproven: 4,
138
+ unsigned: 5,
139
+ no_send: 6,
140
+ non_final: 7
141
+ }.freeze
142
+
143
+ ACTION_CODE_STATUSES = ACTION_STATUS_CODES.invert.freeze
144
+
145
+ module_function
146
+
147
+ # @param result [Hash] { total_actions: Integer, actions: Array<Hash> }
148
+ # @return [String] binary
149
+ def serialize(result)
150
+ w = Wire::Writer.new
151
+
152
+ actions = result[:actions] || []
153
+ w.write_varint(actions.length)
154
+
155
+ actions.each do |action|
156
+ txid_hex = action[:txid].to_s
157
+ w.write_bytes([txid_hex].pack('H*'))
158
+ w.write_varint(action[:satoshis].to_i)
159
+
160
+ code = ACTION_STATUS_CODES.fetch(action[:status]) do
161
+ raise ArgumentError, "invalid action status: #{action[:status]}"
162
+ end
163
+ w.write_byte(code)
164
+
165
+ # is_outgoing: non-optional bool written as optional_bool pointer in Go
166
+ w.write_byte(action[:is_outgoing] ? 1 : 0)
167
+
168
+ w.write_string(action[:description].to_s)
169
+ w.write_string_slice(action[:labels])
170
+ w.write_varint(action[:version].to_i)
171
+ w.write_varint(action[:lock_time].to_i)
172
+
173
+ serialize_action_inputs(w, action[:inputs])
174
+ serialize_action_outputs(w, action[:outputs])
175
+ end
176
+
177
+ w.buf
178
+ end
179
+
180
+ # @param bytes [String] binary
181
+ # @return [Hash]
182
+ def deserialize(bytes)
183
+ r = Wire::Reader.new(bytes)
184
+
185
+ total = r.read_varint
186
+ actions = total.times.map do
187
+ txid_hex = r.read_bytes(32).unpack1('H*')
188
+ satoshis = r.read_varint
189
+ code = r.read_byte
190
+ status = ACTION_CODE_STATUSES.fetch(code) do
191
+ raise ArgumentError, "invalid action status code: #{code}"
192
+ end
193
+
194
+ is_outgoing = r.read_byte == 1
195
+ description = r.read_string
196
+ labels = r.read_string_slice
197
+ version = r.read_varint
198
+ lock_time = r.read_varint
199
+
200
+ inputs = deserialize_action_inputs(r)
201
+ outputs = deserialize_action_outputs(r)
202
+
203
+ action = {
204
+ txid: txid_hex,
205
+ satoshis: satoshis,
206
+ status: status,
207
+ is_outgoing: is_outgoing,
208
+ description: description,
209
+ version: version,
210
+ lock_time: lock_time
211
+ }
212
+ action[:labels] = labels unless labels.nil?
213
+ action[:inputs] = inputs unless inputs.nil?
214
+ action[:outputs] = outputs unless outputs.nil?
215
+ action
216
+ end
217
+
218
+ { total_actions: total, actions: actions }
219
+ end
220
+
221
+ def serialize_action_inputs(writer, inputs)
222
+ if inputs.nil? || inputs.empty?
223
+ writer.write_negative_one
224
+ return
225
+ end
226
+
227
+ writer.write_varint(inputs.length)
228
+ inputs.each do |inp|
229
+ txid_hex, vout = inp[:source_outpoint].split('.')
230
+ writer.write_bytes([txid_hex].pack('H*'))
231
+ writer.write_varint(vout.to_i)
232
+
233
+ writer.write_varint(inp[:source_satoshis].to_i)
234
+
235
+ script = inp[:source_locking_script]
236
+ if script && !script.b.empty?
237
+ writer.write_int_bytes(script)
238
+ else
239
+ writer.write_negative_one
240
+ end
241
+
242
+ unlock_script = inp[:unlocking_script]
243
+ if unlock_script && !unlock_script.b.empty?
244
+ writer.write_int_bytes(unlock_script)
245
+ else
246
+ writer.write_negative_one
247
+ end
248
+
249
+ writer.write_string(inp[:input_description].to_s)
250
+ writer.write_varint(inp[:sequence_number].to_i)
251
+ end
252
+ end
253
+
254
+ def serialize_action_outputs(writer, outputs)
255
+ if outputs.nil? || outputs.empty?
256
+ writer.write_negative_one
257
+ return
258
+ end
259
+
260
+ writer.write_varint(outputs.length)
261
+ outputs.each do |out|
262
+ writer.write_varint(out[:output_index].to_i)
263
+ writer.write_varint(out[:satoshis].to_i)
264
+
265
+ script = out[:locking_script]
266
+ if script && !script.b.empty?
267
+ writer.write_int_bytes(script)
268
+ else
269
+ writer.write_negative_one
270
+ end
271
+
272
+ writer.write_byte(out[:spendable] ? 1 : 0)
273
+ writer.write_string(out[:output_description].to_s)
274
+ writer.write_string(out[:basket].to_s)
275
+ writer.write_string_slice(out[:tags])
276
+ writer.write_optional_string(out[:custom_instructions])
277
+ end
278
+ end
279
+
280
+ def deserialize_action_inputs(reader)
281
+ count = reader.read_varint
282
+ return nil if count == 0xFFFF_FFFF_FFFF_FFFF
283
+
284
+ count.times.map do
285
+ txid_hex = reader.read_bytes(32).unpack1('H*')
286
+ vout = reader.read_varint
287
+ source_outpoint = "#{txid_hex}.#{vout}"
288
+
289
+ source_satoshis = reader.read_varint
290
+
291
+ source_locking_script = reader.read_int_bytes
292
+ source_locking_script = nil if source_locking_script.empty?
293
+
294
+ unlocking_script = reader.read_int_bytes
295
+ unlocking_script = nil if unlocking_script.empty?
296
+
297
+ input_description = reader.read_string
298
+ sequence_number = reader.read_varint
299
+
300
+ inp = {
301
+ source_outpoint: source_outpoint,
302
+ source_satoshis: source_satoshis,
303
+ input_description: input_description,
304
+ sequence_number: sequence_number
305
+ }
306
+ inp[:source_locking_script] = source_locking_script unless source_locking_script.nil?
307
+ inp[:unlocking_script] = unlocking_script unless unlocking_script.nil?
308
+ inp
309
+ end
310
+ end
311
+
312
+ def deserialize_action_outputs(reader)
313
+ count = reader.read_varint
314
+ return nil if count == 0xFFFF_FFFF_FFFF_FFFF
315
+
316
+ count.times.map do
317
+ output_index = reader.read_varint
318
+ satoshis = reader.read_varint
319
+
320
+ locking_script = reader.read_int_bytes
321
+ locking_script = nil if locking_script.empty?
322
+
323
+ spendable = reader.read_byte == 1
324
+ output_description = reader.read_string
325
+ basket = reader.read_string
326
+ tags = reader.read_string_slice
327
+ custom = reader.read_optional_string
328
+
329
+ out = {
330
+ output_index: output_index,
331
+ satoshis: satoshis,
332
+ spendable: spendable,
333
+ output_description: output_description,
334
+ basket: basket
335
+ }
336
+ out[:locking_script] = locking_script unless locking_script.nil?
337
+ out[:tags] = tags unless tags.nil?
338
+ out[:custom_instructions] = custom unless custom.nil?
339
+ out
340
+ end
341
+ end
342
+
343
+ private_class_method :serialize_action_inputs, :serialize_action_outputs,
344
+ :deserialize_action_inputs, :deserialize_action_outputs
345
+ end
346
+ end
347
+ end
348
+ end