bsv-sdk 0.4.0 → 0.5.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 +30 -0
- data/lib/bsv/identity/client.rb +353 -0
- data/lib/bsv/identity/constants.rb +41 -0
- data/lib/bsv/identity/identity_parser.rb +247 -0
- data/lib/bsv/identity/types.rb +118 -0
- data/lib/bsv/identity.rb +18 -0
- data/lib/bsv/overlay/admin_token_template.rb +249 -0
- data/lib/bsv/overlay/broadcast_facilitator.rb +134 -0
- data/lib/bsv/overlay/constants.rb +52 -0
- data/lib/bsv/overlay/errors.rb +17 -0
- data/lib/bsv/overlay/host_reputation_tracker.rb +266 -0
- data/lib/bsv/overlay/lookup_facilitator.rb +125 -0
- data/lib/bsv/overlay/lookup_resolver.rb +406 -0
- data/lib/bsv/overlay/topic_broadcaster.rb +402 -0
- data/lib/bsv/overlay/types.rb +111 -0
- data/lib/bsv/overlay.rb +29 -0
- data/lib/bsv/script/push_drop_template.rb +207 -0
- data/lib/bsv/script.rb +6 -4
- data/lib/bsv/version.rb +1 -1
- data/lib/bsv/wallet_interface/proto_wallet.rb +9 -9
- data/lib/bsv-sdk.rb +2 -0
- metadata +18 -2
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
module BSV
|
|
8
|
+
module Overlay
|
|
9
|
+
# Broadcasts transactions to overlay topics via SHIP (Service Host Interconnect Protocol).
|
|
10
|
+
#
|
|
11
|
+
# Discovers interested overlay hosts by querying the +ls_ship+ SLAP service,
|
|
12
|
+
# then dispatches a TaggedBEEF to each host in parallel and verifies
|
|
13
|
+
# acknowledgements according to the configured requirements.
|
|
14
|
+
#
|
|
15
|
+
# == Topic validation
|
|
16
|
+
#
|
|
17
|
+
# All topics must be non-empty strings beginning with +tm_+. An empty
|
|
18
|
+
# topics array or a topic without the +tm_+ prefix raises ArgumentError.
|
|
19
|
+
#
|
|
20
|
+
# == Acknowledgement modes
|
|
21
|
+
#
|
|
22
|
+
# Three independent ack modes may be combined:
|
|
23
|
+
#
|
|
24
|
+
# - +require_ack_from_any_host+ (default: +'all'+) — at least one successful
|
|
25
|
+
# host must satisfy the requirement.
|
|
26
|
+
# - +require_ack_from_all_hosts+ (default: +[]+) — every successful host must
|
|
27
|
+
# satisfy the requirement. An empty array disables this check.
|
|
28
|
+
# - +require_ack_from_specific_hosts+ (default: +{}+) — named hosts must
|
|
29
|
+
# each satisfy their individual requirement.
|
|
30
|
+
#
|
|
31
|
+
# Requirement values:
|
|
32
|
+
# - +'all'+ — the host must have acknowledged every one of the broadcaster's topics.
|
|
33
|
+
# - +'any'+ — the host must have acknowledged at least one topic.
|
|
34
|
+
# - +Array<String>+ — the host must have acknowledged all topics in the array.
|
|
35
|
+
#
|
|
36
|
+
# == Host caching
|
|
37
|
+
#
|
|
38
|
+
# SHIP host discovery results are cached for +SHIP_CACHE_TTL+ seconds (5 minutes)
|
|
39
|
+
# to avoid redundant SLAP queries on repeated broadcasts.
|
|
40
|
+
class TopicBroadcaster
|
|
41
|
+
# Seconds before the SHIP host cache expires.
|
|
42
|
+
SHIP_CACHE_TTL = 300
|
|
43
|
+
|
|
44
|
+
# @param topics [Array<String>] overlay topic names (must start with +tm_+)
|
|
45
|
+
# @param network_preset [Symbol] +:mainnet+, +:testnet+, or +:local+
|
|
46
|
+
# @param facilitator [BroadcastFacilitator, nil] injectable facilitator
|
|
47
|
+
# @param resolver [LookupResolver, nil] injectable resolver
|
|
48
|
+
# @param require_ack_from_all_hosts [Array, String] requirement all hosts must satisfy
|
|
49
|
+
# @param require_ack_from_any_host [String] requirement at least one host must satisfy
|
|
50
|
+
# @param require_ack_from_specific_hosts [Hash] per-host requirements
|
|
51
|
+
def initialize(
|
|
52
|
+
topics,
|
|
53
|
+
network_preset: :mainnet,
|
|
54
|
+
facilitator: nil,
|
|
55
|
+
resolver: nil,
|
|
56
|
+
require_ack_from_all_hosts: [],
|
|
57
|
+
require_ack_from_any_host: 'all',
|
|
58
|
+
require_ack_from_specific_hosts: {}
|
|
59
|
+
)
|
|
60
|
+
validate_topics!(topics)
|
|
61
|
+
|
|
62
|
+
@topics = topics.dup.freeze
|
|
63
|
+
@network_preset = network_preset
|
|
64
|
+
@facilitator = facilitator || default_facilitator
|
|
65
|
+
@resolver = resolver || default_resolver
|
|
66
|
+
|
|
67
|
+
@require_ack_from_all_hosts = require_ack_from_all_hosts
|
|
68
|
+
@require_ack_from_any_host = require_ack_from_any_host
|
|
69
|
+
@require_ack_from_specific_hosts = require_ack_from_specific_hosts
|
|
70
|
+
|
|
71
|
+
@ship_cache = nil
|
|
72
|
+
@ship_cache_at = nil
|
|
73
|
+
@ship_cache_mutex = Mutex.new
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Broadcast a transaction to all interested overlay hosts.
|
|
77
|
+
#
|
|
78
|
+
# @param tx [BSV::Transaction::Transaction] the transaction to broadcast
|
|
79
|
+
# @return [OverlayBroadcastResult]
|
|
80
|
+
def broadcast(tx)
|
|
81
|
+
beef = serialise_beef(tx)
|
|
82
|
+
return beef if beef.is_a?(OverlayBroadcastResult)
|
|
83
|
+
|
|
84
|
+
if local?
|
|
85
|
+
host_topics = { 'http://localhost:8080' => Set.new(@topics) }
|
|
86
|
+
else
|
|
87
|
+
host_topics = find_interested_hosts
|
|
88
|
+
if host_topics.empty?
|
|
89
|
+
return error_result(
|
|
90
|
+
'ERR_NO_HOSTS_INTERESTED',
|
|
91
|
+
"No #{@network_preset} hosts are interested in receiving this transaction."
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
results = dispatch(beef, host_topics)
|
|
97
|
+
|
|
98
|
+
successful = results.compact
|
|
99
|
+
if successful.empty?
|
|
100
|
+
return error_result(
|
|
101
|
+
'ERR_ALL_HOSTS_REJECTED',
|
|
102
|
+
"All #{@network_preset} topical hosts have rejected the transaction."
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
host_acks = build_host_acks(successful)
|
|
107
|
+
|
|
108
|
+
ack_check = check_ack_requirements(host_acks)
|
|
109
|
+
return ack_check if ack_check.is_a?(OverlayBroadcastResult)
|
|
110
|
+
|
|
111
|
+
OverlayBroadcastResult.new(
|
|
112
|
+
status: 'success',
|
|
113
|
+
txid: tx.txid_hex,
|
|
114
|
+
message: "Sent to #{successful.size} Overlay Service host(s)."
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Discover overlay hosts interested in the broadcaster's topics via SHIP.
|
|
119
|
+
#
|
|
120
|
+
# Results are cached for +SHIP_CACHE_TTL+ seconds.
|
|
121
|
+
#
|
|
122
|
+
# @return [Hash{String => Set<String>}] map of host URL to set of interested topics
|
|
123
|
+
def find_interested_hosts
|
|
124
|
+
@ship_cache_mutex.synchronize do
|
|
125
|
+
if @ship_cache && (Time.now.to_f - @ship_cache_at) < SHIP_CACHE_TTL
|
|
126
|
+
return @ship_cache.transform_values(&:dup)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
hosts = fetch_ship_hosts
|
|
130
|
+
@ship_cache = hosts
|
|
131
|
+
@ship_cache_at = Time.now.to_f
|
|
132
|
+
hosts.transform_values(&:dup)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
# ---- Topic validation ----
|
|
139
|
+
|
|
140
|
+
def validate_topics!(topics)
|
|
141
|
+
raise ArgumentError, 'topics must be a non-empty array' if topics.nil? || topics.empty?
|
|
142
|
+
|
|
143
|
+
topics.each do |topic|
|
|
144
|
+
raise ArgumentError, "topic #{topic.inspect} must start with 'tm_'" unless topic.is_a?(String) && topic.start_with?('tm_')
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# ---- BEEF serialisation ----
|
|
149
|
+
|
|
150
|
+
def serialise_beef(tx)
|
|
151
|
+
tx.to_beef
|
|
152
|
+
rescue StandardError => e
|
|
153
|
+
error_result('ERR_BEEF_SERIALIZATION_FAILED', "Failed to serialise transaction to BEEF: #{e.message}")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# ---- SHIP host discovery ----
|
|
157
|
+
|
|
158
|
+
def fetch_ship_hosts
|
|
159
|
+
question = LookupQuestion.new(
|
|
160
|
+
service: 'ls_ship',
|
|
161
|
+
query: { 'topics' => @topics }
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
answer = @resolver.query(question)
|
|
165
|
+
return {} unless answer.type == 'output-list'
|
|
166
|
+
|
|
167
|
+
parse_ship_answer(answer)
|
|
168
|
+
rescue StandardError
|
|
169
|
+
{}
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def parse_ship_answer(answer)
|
|
173
|
+
results = {}
|
|
174
|
+
|
|
175
|
+
answer.outputs.each do |output|
|
|
176
|
+
beef_data = output['beef'] || output[:beef]
|
|
177
|
+
output_index = (output['outputIndex'] || output[:output_index] || 0).to_i
|
|
178
|
+
next if output_index.negative?
|
|
179
|
+
|
|
180
|
+
next unless beef_data
|
|
181
|
+
|
|
182
|
+
advert = decode_ship_advert(beef_data, output_index)
|
|
183
|
+
next unless advert
|
|
184
|
+
next unless advert.protocol == Constants::PROTOCOL_SHIP
|
|
185
|
+
next unless @topics.include?(advert.topic_or_service)
|
|
186
|
+
|
|
187
|
+
domain = normalise_domain(advert.domain)
|
|
188
|
+
next if domain.nil? || domain.empty?
|
|
189
|
+
|
|
190
|
+
results[domain] ||= Set.new
|
|
191
|
+
results[domain] << advert.topic_or_service
|
|
192
|
+
rescue StandardError
|
|
193
|
+
next
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
results
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def decode_ship_advert(beef_data, output_index)
|
|
200
|
+
beef = parse_beef(beef_data)
|
|
201
|
+
return nil unless beef
|
|
202
|
+
|
|
203
|
+
beef_tx = beef.transactions.last
|
|
204
|
+
return nil unless beef_tx&.transaction
|
|
205
|
+
|
|
206
|
+
tx = beef_tx.transaction
|
|
207
|
+
txout = tx.outputs[output_index]
|
|
208
|
+
return nil unless txout
|
|
209
|
+
|
|
210
|
+
AdminTokenTemplate.decode(txout.locking_script)
|
|
211
|
+
rescue StandardError
|
|
212
|
+
nil
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def parse_beef(beef_data)
|
|
216
|
+
case beef_data
|
|
217
|
+
when String
|
|
218
|
+
BSV::Transaction::Beef.from_binary(beef_data)
|
|
219
|
+
when Array
|
|
220
|
+
BSV::Transaction::Beef.from_binary(beef_data.pack('C*'))
|
|
221
|
+
end
|
|
222
|
+
rescue StandardError
|
|
223
|
+
nil
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def normalise_domain(domain)
|
|
227
|
+
url = domain.start_with?('https://', 'http://') ? domain : "https://#{domain}"
|
|
228
|
+
return nil if private_url?(url)
|
|
229
|
+
|
|
230
|
+
url
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Reject URLs whose hostname is a private/loopback IP literal (SSRF protection).
|
|
234
|
+
def private_url?(url)
|
|
235
|
+
require 'ipaddr'
|
|
236
|
+
host = URI(url).hostname
|
|
237
|
+
return true if host.nil? || host.empty?
|
|
238
|
+
|
|
239
|
+
addr = IPAddr.new(host)
|
|
240
|
+
addr.loopback? || addr.private? || addr.link_local?
|
|
241
|
+
rescue IPAddr::InvalidAddressError
|
|
242
|
+
false # Not an IP literal — domain names pass through
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# ---- Parallel dispatch ----
|
|
246
|
+
|
|
247
|
+
def dispatch(beef, host_topics)
|
|
248
|
+
results = {}
|
|
249
|
+
mutex = Mutex.new
|
|
250
|
+
|
|
251
|
+
threads = host_topics.map do |host, host_topic_set|
|
|
252
|
+
Thread.new do
|
|
253
|
+
topics_for_host = @topics.select { |t| host_topic_set.include?(t) }
|
|
254
|
+
tagged = TaggedBEEF.new(beef: beef, topics: topics_for_host)
|
|
255
|
+
steak = @facilitator.send_beef(host, tagged)
|
|
256
|
+
mutex.synchronize { results[host] = steak }
|
|
257
|
+
rescue StandardError
|
|
258
|
+
mutex.synchronize { results[host] = nil }
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
threads.each(&:join)
|
|
263
|
+
results
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# ---- Acknowledgement tracking ----
|
|
267
|
+
|
|
268
|
+
# Build a map of host → Set of acknowledged topic names.
|
|
269
|
+
#
|
|
270
|
+
# A topic is considered acknowledged if the host's AdmittanceInstructions
|
|
271
|
+
# for that topic has at least one entry in +outputs_to_admit+,
|
|
272
|
+
# +coins_to_retain+, or +coins_removed+.
|
|
273
|
+
def build_host_acks(successful_results)
|
|
274
|
+
successful_results.each_with_object({}) do |(host, steak), acks|
|
|
275
|
+
next unless steak.is_a?(Hash)
|
|
276
|
+
|
|
277
|
+
acked = Set.new
|
|
278
|
+
steak.each do |topic, instructions|
|
|
279
|
+
next unless instructions.is_a?(AdmittanceInstructions)
|
|
280
|
+
|
|
281
|
+
admit = instructions.outputs_to_admit
|
|
282
|
+
retain = instructions.coins_to_retain
|
|
283
|
+
removed = instructions.coins_removed
|
|
284
|
+
|
|
285
|
+
next unless (admit && !admit.empty?) ||
|
|
286
|
+
(retain && !retain.empty?) ||
|
|
287
|
+
(removed && !removed.empty?)
|
|
288
|
+
|
|
289
|
+
acked << topic
|
|
290
|
+
end
|
|
291
|
+
acks[host] = acked
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Check all three ack requirement modes and return an error result if
|
|
296
|
+
# any check fails, or nil on success.
|
|
297
|
+
def check_ack_requirements(host_acks)
|
|
298
|
+
if any_host_requirement_active? && !any_host_satisfies?(host_acks, @require_ack_from_any_host)
|
|
299
|
+
return error_result(
|
|
300
|
+
'ERR_REQUIRE_ACK_FROM_ANY_HOST_FAILED',
|
|
301
|
+
'No host acknowledged the required topics.'
|
|
302
|
+
)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
if all_hosts_requirement_active? && !all_hosts_satisfy?(host_acks, @require_ack_from_all_hosts)
|
|
306
|
+
return error_result(
|
|
307
|
+
'ERR_REQUIRE_ACK_FROM_ALL_HOSTS_FAILED',
|
|
308
|
+
'Not all hosts acknowledged the required topics.'
|
|
309
|
+
)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
if @require_ack_from_specific_hosts.any? && !specific_hosts_satisfy?(host_acks)
|
|
313
|
+
return error_result(
|
|
314
|
+
'ERR_REQUIRE_ACK_FROM_SPECIFIC_HOSTS_FAILED',
|
|
315
|
+
'Specific hosts did not acknowledge the required topics.'
|
|
316
|
+
)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
nil
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def any_host_requirement_active?
|
|
323
|
+
req = @require_ack_from_any_host
|
|
324
|
+
req && req != []
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def all_hosts_requirement_active?
|
|
328
|
+
req = @require_ack_from_all_hosts
|
|
329
|
+
req && req != [] && !req.empty?
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Returns true if at least one host in +host_acks+ satisfies +requirement+.
|
|
333
|
+
def any_host_satisfies?(host_acks, requirement)
|
|
334
|
+
host_acks.any? { |_host, acked| host_meets_requirement?(acked, requirement) }
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Returns true if every host in +host_acks+ satisfies +requirement+.
|
|
338
|
+
def all_hosts_satisfy?(host_acks, requirement)
|
|
339
|
+
host_acks.all? { |_host, acked| host_meets_requirement?(acked, requirement) }
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Returns true if every named host in +require_ack_from_specific_hosts+
|
|
343
|
+
# responded and met its individual requirement.
|
|
344
|
+
def specific_hosts_satisfy?(host_acks)
|
|
345
|
+
@require_ack_from_specific_hosts.all? do |host, requirement|
|
|
346
|
+
acked = host_acks[host]
|
|
347
|
+
next false unless acked
|
|
348
|
+
|
|
349
|
+
host_meets_requirement?(acked, requirement)
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Evaluate whether a host's acknowledged topic set satisfies a requirement.
|
|
354
|
+
#
|
|
355
|
+
# @param acked [Set<String>] topics the host acknowledged
|
|
356
|
+
# @param requirement [String, Array<String>] +'all'+, +'any'+, or topic list
|
|
357
|
+
def host_meets_requirement?(acked, requirement)
|
|
358
|
+
case requirement
|
|
359
|
+
when 'all'
|
|
360
|
+
@topics.all? { |t| acked.include?(t) }
|
|
361
|
+
when 'any'
|
|
362
|
+
@topics.any? { |t| acked.include?(t) }
|
|
363
|
+
when Array
|
|
364
|
+
requirement.all? { |t| acked.include?(t) }
|
|
365
|
+
else
|
|
366
|
+
true
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# ---- Result helpers ----
|
|
371
|
+
|
|
372
|
+
def error_result(code, description)
|
|
373
|
+
OverlayBroadcastResult.new(status: 'error', code: code, description: description)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# ---- Defaults ----
|
|
377
|
+
|
|
378
|
+
def local?
|
|
379
|
+
@network_preset == :local
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def default_facilitator
|
|
383
|
+
if local?
|
|
384
|
+
HTTPSBroadcastFacilitator.new(allow_http: true)
|
|
385
|
+
else
|
|
386
|
+
HTTPSBroadcastFacilitator.new
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def default_resolver
|
|
391
|
+
LookupResolver.new(network_preset: @network_preset)
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Alias for TopicBroadcaster.
|
|
396
|
+
#
|
|
397
|
+
# SHIPBroadcaster and TopicBroadcaster are the same class; the alias exists
|
|
398
|
+
# for compatibility with the Go and TypeScript SDKs where the class is known
|
|
399
|
+
# by both names.
|
|
400
|
+
SHIPBroadcaster = TopicBroadcaster
|
|
401
|
+
end
|
|
402
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Overlay
|
|
5
|
+
# Tagged BEEF (Background Evaluation Extended Format) structure.
|
|
6
|
+
#
|
|
7
|
+
# Comprises a transaction, its SPV information, and the overlay topics
|
|
8
|
+
# where its inclusion is requested.
|
|
9
|
+
class TaggedBEEF
|
|
10
|
+
# @return [String] raw binary BEEF-encoded transaction data
|
|
11
|
+
attr_reader :beef
|
|
12
|
+
|
|
13
|
+
# @return [Array<String>] overlay topic names where the transaction is to be submitted
|
|
14
|
+
attr_reader :topics
|
|
15
|
+
|
|
16
|
+
# @param beef [String] raw binary BEEF-encoded transaction data
|
|
17
|
+
# @param topics [Array<String>] overlay topic names
|
|
18
|
+
def initialize(beef:, topics:)
|
|
19
|
+
@beef = beef
|
|
20
|
+
@topics = topics
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Instructs the Overlay Services Engine about which outputs to admit and
|
|
25
|
+
# which previous outputs to retain. Returned by a Topic Manager.
|
|
26
|
+
class AdmittanceInstructions
|
|
27
|
+
# @return [Array<Integer>] indices of admissible outputs in the managed topic
|
|
28
|
+
attr_reader :outputs_to_admit
|
|
29
|
+
|
|
30
|
+
# @return [Array<Integer>] indices of inputs spending previously-admitted outputs to retain
|
|
31
|
+
attr_reader :coins_to_retain
|
|
32
|
+
|
|
33
|
+
# @return [Array<Integer>, nil] indices of inputs spending previously-admitted outputs
|
|
34
|
+
# that are now considered spent and removed from the topic (optional)
|
|
35
|
+
attr_reader :coins_removed
|
|
36
|
+
|
|
37
|
+
# @param outputs_to_admit [Array<Integer>]
|
|
38
|
+
# @param coins_to_retain [Array<Integer>]
|
|
39
|
+
# @param coins_removed [Array<Integer>, nil]
|
|
40
|
+
def initialize(outputs_to_admit:, coins_to_retain:, coins_removed: nil)
|
|
41
|
+
@outputs_to_admit = outputs_to_admit
|
|
42
|
+
@coins_to_retain = coins_to_retain
|
|
43
|
+
@coins_removed = coins_removed
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The question asked to the Overlay Services Engine when a consumer of state
|
|
48
|
+
# wishes to look up information.
|
|
49
|
+
class LookupQuestion
|
|
50
|
+
# @return [String] identifier of the Lookup Service to query
|
|
51
|
+
attr_reader :service
|
|
52
|
+
|
|
53
|
+
# @return [Hash] query forwarded to the Lookup Service; structure depends on the service
|
|
54
|
+
attr_reader :query
|
|
55
|
+
|
|
56
|
+
# @param service [String]
|
|
57
|
+
# @param query [Hash]
|
|
58
|
+
def initialize(service:, query:)
|
|
59
|
+
@service = service
|
|
60
|
+
@query = query
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# How the Overlay Services Engine responds to a LookupQuestion.
|
|
65
|
+
class LookupAnswer
|
|
66
|
+
# @return [String] response type (e.g. 'output-list')
|
|
67
|
+
attr_reader :type
|
|
68
|
+
|
|
69
|
+
# @return [Array] outputs or freeform response data from the Lookup Service
|
|
70
|
+
attr_reader :outputs
|
|
71
|
+
|
|
72
|
+
# @param type [String]
|
|
73
|
+
# @param outputs [Array]
|
|
74
|
+
def initialize(type:, outputs:)
|
|
75
|
+
@type = type
|
|
76
|
+
@outputs = outputs
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Result of broadcasting a transaction to an Overlay Services host.
|
|
81
|
+
class OverlayBroadcastResult
|
|
82
|
+
# @return [String] result status ('success' or 'error')
|
|
83
|
+
attr_reader :status
|
|
84
|
+
|
|
85
|
+
# @return [String, nil] transaction identifier (present on success)
|
|
86
|
+
attr_reader :txid
|
|
87
|
+
|
|
88
|
+
# @return [String, nil] human-readable result message
|
|
89
|
+
attr_reader :message
|
|
90
|
+
|
|
91
|
+
# @return [String, nil] machine-readable error code
|
|
92
|
+
attr_reader :code
|
|
93
|
+
|
|
94
|
+
# @return [String, nil] human-readable error description
|
|
95
|
+
attr_reader :description
|
|
96
|
+
|
|
97
|
+
# @param status [String]
|
|
98
|
+
# @param txid [String, nil]
|
|
99
|
+
# @param message [String, nil]
|
|
100
|
+
# @param code [String, nil]
|
|
101
|
+
# @param description [String, nil]
|
|
102
|
+
def initialize(status:, txid: nil, message: nil, code: nil, description: nil)
|
|
103
|
+
@status = status
|
|
104
|
+
@txid = txid
|
|
105
|
+
@message = message
|
|
106
|
+
@code = code
|
|
107
|
+
@description = description
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
data/lib/bsv/overlay.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
# Overlay Services module for BSV blockchain.
|
|
5
|
+
#
|
|
6
|
+
# Provides foundation types, protocol constants, and error classes for
|
|
7
|
+
# interacting with BSV Overlay Services via the SHIP and SLAP protocols.
|
|
8
|
+
module Overlay
|
|
9
|
+
autoload :Constants, 'bsv/overlay/constants'
|
|
10
|
+
autoload :TaggedBEEF, 'bsv/overlay/types'
|
|
11
|
+
autoload :AdmittanceInstructions, 'bsv/overlay/types'
|
|
12
|
+
autoload :LookupQuestion, 'bsv/overlay/types'
|
|
13
|
+
autoload :LookupAnswer, 'bsv/overlay/types'
|
|
14
|
+
autoload :OverlayBroadcastResult, 'bsv/overlay/types'
|
|
15
|
+
autoload :OverlayError, 'bsv/overlay/errors'
|
|
16
|
+
autoload :NoCompetentHostsError, 'bsv/overlay/errors'
|
|
17
|
+
autoload :AllHostsRejectedError, 'bsv/overlay/errors'
|
|
18
|
+
autoload :AcknowledgementError, 'bsv/overlay/errors'
|
|
19
|
+
autoload :HostReputationTracker, 'bsv/overlay/host_reputation_tracker'
|
|
20
|
+
autoload :AdminTokenTemplate, 'bsv/overlay/admin_token_template'
|
|
21
|
+
autoload :LookupFacilitator, 'bsv/overlay/lookup_facilitator'
|
|
22
|
+
autoload :HTTPSLookupFacilitator, 'bsv/overlay/lookup_facilitator'
|
|
23
|
+
autoload :LookupResolver, 'bsv/overlay/lookup_resolver'
|
|
24
|
+
autoload :BroadcastFacilitator, 'bsv/overlay/broadcast_facilitator'
|
|
25
|
+
autoload :HTTPSBroadcastFacilitator, 'bsv/overlay/broadcast_facilitator'
|
|
26
|
+
autoload :TopicBroadcaster, 'bsv/overlay/topic_broadcaster'
|
|
27
|
+
autoload :SHIPBroadcaster, 'bsv/overlay/topic_broadcaster'
|
|
28
|
+
end
|
|
29
|
+
end
|