antd 0.1.0 → 0.3.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.
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "manifests"
4
+ require_relative "grpc_client"
5
+ require_relative "v1/manifests_services_pb"
6
+
7
+ # The manifest methods on +Antd::GrpcClient+, over ManifestService. The
8
+ # models and the entry check are in manifests.rb, shared with REST.
9
+ module Antd
10
+ # Channel arguments for the manifest stub's own channel. grpc-ruby has no
11
+ # per-call message limit, so ManifestService gets a channel of its own
12
+ # with both limits raised to +MANIFEST_GRPC_MAX_MESSAGE+; the other
13
+ # services keep gRPC's defaults.
14
+ MANIFEST_GRPC_CHANNEL_ARGS = {
15
+ "grpc.max_receive_message_length" => MANIFEST_GRPC_MAX_MESSAGE,
16
+ "grpc.max_send_message_length" => MANIFEST_GRPC_MAX_MESSAGE
17
+ }.freeze
18
+
19
+ class GrpcClient
20
+ # --- Manifests ---
21
+ #
22
+ # The same methods and models as the REST +Client+ (see there). The
23
+ # `.ant` bytes travel raw over gRPC; REST's base64 never shows. Unset
24
+ # optionals (proto3 +optional+ without a value, or an empty string) read
25
+ # as nil, as REST's omitted fields do.
26
+
27
+ # Mirrors +Client#build_manifest+. Nothing is paid for.
28
+ #
29
+ # Needs antd 0.16.0 or later.
30
+ #
31
+ # @return [BuiltManifest]
32
+ # @raise [BadRequestError] an entry with both or neither of +data_map+ and
33
+ # +address+, or a bad +size+, before anything is sent
34
+ # @raise [BadRequestError] an entry the daemon rejects (INVALID_ARGUMENT);
35
+ # the message names +entries[i]+
36
+ def build_manifest(entries, name: nil, torrent_info_hashes: nil, embed_roots: true)
37
+ inputs = Antd.manifest_entry_inputs(entries)
38
+ req = Antd::V1::BuildManifestRequest.new(
39
+ torrent_info_hashes: Array(torrent_info_hashes),
40
+ # Always sent, so "unset means true" never depends on the daemon.
41
+ embed_roots: embed_roots ? true : false,
42
+ entries: inputs.map { |e| manifest_entry_input_pb(e) }
43
+ )
44
+ req.name = name unless name.nil? || name.empty?
45
+
46
+ resp = grpc_call { manifest_stub.build(req) }
47
+ BuiltManifest.new(
48
+ manifest: resp.manifest,
49
+ link: manifest_pb_string(resp.link),
50
+ entries: resp.entries.map { |e| manifest_entry_info_from_pb(e) }
51
+ )
52
+ end
53
+
54
+ # Mirrors +Client#decode_manifest+. The bytes are sent raw.
55
+ #
56
+ # Needs antd 0.16.0 or later.
57
+ #
58
+ # @return [DecodedManifest]
59
+ def decode_manifest(manifest, resolve: false)
60
+ raise BadRequestError, MANIFEST_DECODE_NEEDS_INPUT if manifest.nil? || manifest.empty?
61
+
62
+ decode_manifest_pb(Antd::V1::DecodeManifestRequest.new(manifest: manifest.b, resolve: resolve ? true : false))
63
+ end
64
+
65
+ # Mirrors +Client#decode_manifest_link+.
66
+ #
67
+ # Needs antd 0.16.0 or later.
68
+ #
69
+ # @return [DecodedManifest]
70
+ def decode_manifest_link(link, resolve: false)
71
+ raise BadRequestError, MANIFEST_DECODE_NEEDS_INPUT if link.nil? || link.empty?
72
+
73
+ decode_manifest_pb(Antd::V1::DecodeManifestRequest.new(link: link, resolve: resolve ? true : false))
74
+ end
75
+
76
+ private
77
+
78
+ # ManifestService on its own channel, with +MANIFEST_GRPC_CHANNEL_ARGS+.
79
+ # Made on first use, so +initialize+ stays as it is.
80
+ def manifest_stub
81
+ @manifest_stub ||= Antd::V1::ManifestService::Stub.new(
82
+ @target, :this_channel_is_insecure, channel_args: MANIFEST_GRPC_CHANNEL_ARGS
83
+ )
84
+ end
85
+
86
+ def decode_manifest_pb(req)
87
+ resp = grpc_call { manifest_stub.decode(req) }
88
+ DecodedManifest.new(
89
+ source: resp.source,
90
+ name: resp.has_name? ? manifest_pb_string(resp.name) : nil,
91
+ torrent_info_hash_v1: manifest_pb_string(resp.torrent_info_hash_v1),
92
+ torrent_info_hash_v2: manifest_pb_string(resp.torrent_info_hash_v2),
93
+ entries: resp.entries.map { |e| manifest_entry_info_from_pb(e) }
94
+ )
95
+ end
96
+
97
+ def manifest_entry_input_pb(e)
98
+ attrs = {}
99
+ attrs[:path] = e.path unless e.path.nil? || e.path.empty?
100
+ attrs[:size] = e.size unless e.size.nil?
101
+ if e.data_map.nil? || e.data_map.empty?
102
+ attrs[:address] = e.address
103
+ else
104
+ attrs[:data_map] = e.data_map
105
+ end
106
+ Antd::V1::ManifestEntryInput.new(**attrs)
107
+ end
108
+
109
+ # proto3 +optional+ sizes are checked for presence: unset is nil, not 0.
110
+ def manifest_entry_info_from_pb(e)
111
+ ManifestEntryInfo.new(
112
+ effective_name: e.effective_name,
113
+ path: e.has_path? ? manifest_pb_string(e.path) : nil,
114
+ size: e.has_size? ? e.size : nil,
115
+ kind: e.kind,
116
+ content_address: e.content_address,
117
+ data_map: manifest_pb_string(e.data_map),
118
+ exact_size: e.has_exact_size? ? e.exact_size : nil,
119
+ error: manifest_pb_string(e.error)
120
+ )
121
+ end
122
+
123
+ # A proto string, or nil when empty.
124
+ def manifest_pb_string(value)
125
+ value.nil? || value.empty? ? nil : value
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,300 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require_relative "models"
5
+ require_relative "errors"
6
+ require_relative "client"
7
+
8
+ # Manifests (ADR-0006): `.ant` files and `ant://` links that describe a set
9
+ # of files, each with an optional path and size and either an embedded
10
+ # DataMap or a public DataMap address. The daemon builds and decodes them
11
+ # through ant-core, so callers never re-implement the format. Nothing is
12
+ # uploaded or paid for.
13
+ #
14
+ # This file holds the models and the REST methods on +Antd::Client+;
15
+ # grpc_manifests.rb adds the same methods to +Antd::GrpcClient+.
16
+ module Antd
17
+ # +ManifestEntryInfo#kind+ values.
18
+ module ManifestEntryKind
19
+ # The manifest carries the entry's DataMap: it is the key to the file.
20
+ EMBEDDED = "embedded"
21
+ # The manifest carries a public file's DataMap address.
22
+ PUBLIC = "public"
23
+ end
24
+
25
+ # +DecodedManifest#source+ values.
26
+ module ManifestSource
27
+ # `.ant` bytes or an `ant://manifest/` link.
28
+ MANIFEST = "manifest"
29
+ # A file link or bare address, reported as one public entry with no path.
30
+ FILE_LINK = "file_link"
31
+ end
32
+
33
+ # One file for +build_manifest+. Set exactly one of +data_map+ or
34
+ # +address+; a Hash with the same keys works too.
35
+ #
36
+ # - +path+: the relative path the entry extracts under, subject to the
37
+ # portable path rules. nil: it extracts under its content address.
38
+ # - +size+: the plaintext size to record, as given (the creator's claim).
39
+ # nil: none is recorded; readers take the exact size from an embedded
40
+ # root DataMap.
41
+ # - +data_map+: a hex DataMap, as the upload methods return it. The entry
42
+ # embeds it.
43
+ # - +address+: a public file's DataMap address (hex, 32 bytes).
44
+ ManifestEntryInput = Struct.new(:path, :size, :data_map, :address, keyword_init: true) do
45
+ def initialize(path: nil, size: nil, data_map: nil, address: nil)
46
+ super
47
+ end
48
+ end
49
+
50
+ # What a manifest says about one entry. Unset fields are nil on both
51
+ # transports (REST omits them; gRPC sends no value or an empty string),
52
+ # so a +size+ or +exact_size+ the manifest lacks is nil, never 0.
53
+ #
54
+ # - +effective_name+: +path+, or the content address in hex when there is
55
+ # none.
56
+ # - +path+: the entry's path, or nil.
57
+ # - +size+: the size recorded in the manifest (the creator's claim), or nil.
58
+ # - +kind+: a +ManifestEntryKind+ string.
59
+ # - +content_address+: hex. The file's public DataMap address, whether or
60
+ # not it was ever made public; the same for private and public uploads
61
+ # of one file.
62
+ # - +data_map+: the embedded DataMap in the hex form the download methods
63
+ # accept. Decode only; nil for public entries.
64
+ # - +exact_size+: the exact plaintext size, from an embedded root DataMap
65
+ # or after a resolving decode; nil when unknown.
66
+ # - +error+: why a resolving decode failed for this entry, or nil. A
67
+ # failed entry does not fail the call.
68
+ ManifestEntryInfo = Struct.new(
69
+ :effective_name, :path, :size, :kind, :content_address, :data_map, :exact_size, :error,
70
+ keyword_init: true
71
+ ) do
72
+ def initialize(effective_name: "", path: nil, size: nil, kind: "", content_address: "",
73
+ data_map: nil, exact_size: nil, error: nil)
74
+ super
75
+ end
76
+ end
77
+
78
+ # Result of +build_manifest+.
79
+ #
80
+ # - +manifest+: the `.ant` bytes (a binary String).
81
+ # - +link+: the `ant://manifest/` link, or nil when it would not fit the
82
+ # recommended 1,500 bytes.
83
+ # - +entries+: +ManifestEntryInfo+s in encoded order (sorted by effective
84
+ # name).
85
+ BuiltManifest = Struct.new(:manifest, :link, :entries, keyword_init: true)
86
+
87
+ # Result of +decode_manifest+ and +decode_manifest_link+.
88
+ #
89
+ # - +source+: a +ManifestSource+ string.
90
+ # - +name+: the suggested root directory name, or nil.
91
+ # - +torrent_info_hash_v1+, +torrent_info_hash_v2+: hex, or nil when absent.
92
+ # - +entries+: +ManifestEntryInfo+s.
93
+ DecodedManifest = Struct.new(
94
+ :source, :name, :torrent_info_hash_v1, :torrent_info_hash_v2, :entries,
95
+ keyword_init: true
96
+ )
97
+
98
+ # The gRPC message ceiling for manifest calls, both ways: a manifest can be
99
+ # 64 MiB, and a decode response carries its DataMaps in hex. gRPC's default
100
+ # receive limit is 4 MiB.
101
+ MANIFEST_GRPC_MAX_MESSAGE = 256 * 1024 * 1024
102
+
103
+ # The daemon's cap on the entry errors one 400 names.
104
+ MANIFEST_MAX_REPORTED_ENTRY_ERRORS = 20
105
+ private_constant :MANIFEST_MAX_REPORTED_ENTRY_ERRORS
106
+
107
+ # The daemon's message for empty decode input (empty bytes or link).
108
+ MANIFEST_DECODE_NEEDS_INPUT = "needs a manifest (base64 .ant bytes) or a link"
109
+
110
+ # Normalises +build_manifest+ entries to +ManifestEntryInput+s before
111
+ # anything is sent. A refusal is the +BadRequestError+ (400) the daemon
112
+ # would send, with its wording: every bad entry (both or neither of
113
+ # +data_map+ and +address+, nil and "" counting as unset; or a +size+ that
114
+ # is not a non-negative Integer) is collected and named as
115
+ # "N invalid entries: entries[i]: ...; entries[j]: ..." (at most 20, then
116
+ # "; and K more").
117
+ #
118
+ # @api private
119
+ # @param entries [Array<ManifestEntryInput, Hash>]
120
+ # @return [Array<ManifestEntryInput>]
121
+ # @raise [BadRequestError]
122
+ def self.manifest_entry_inputs(entries)
123
+ raise BadRequestError, "entries must be an Array" unless entries.is_a?(Array)
124
+
125
+ errors = []
126
+ inputs = entries.each_with_index.map do |e, i|
127
+ e = ManifestEntryInput.new(**e.transform_keys(&:to_sym)) if e.is_a?(Hash)
128
+ unless e.is_a?(ManifestEntryInput)
129
+ errors << "entries[#{i}]: must be a ManifestEntryInput or Hash"
130
+ next
131
+ end
132
+
133
+ has_data_map = !(e.data_map.nil? || e.data_map.empty?)
134
+ has_address = !(e.address.nil? || e.address.empty?)
135
+ if has_data_map && has_address
136
+ errors << "entries[#{i}]: give data_map or address, not both"
137
+ elsif !has_data_map && !has_address
138
+ errors << "entries[#{i}]: needs a data_map or an address"
139
+ end
140
+ unless e.size.nil? || (e.size.is_a?(Integer) && e.size >= 0)
141
+ errors << "entries[#{i}]: size must be a non-negative Integer or nil"
142
+ end
143
+ e
144
+ rescue ArgumentError
145
+ # A Hash with a key ManifestEntryInput does not have.
146
+ errors << "entries[#{i}]: unknown field (expected path, size, data_map, address)"
147
+ nil
148
+ end
149
+ raise BadRequestError, Antd.manifest_entry_errors_message(errors) unless errors.empty?
150
+
151
+ inputs
152
+ end
153
+
154
+ # The daemon's +entry_errors_message+.
155
+ #
156
+ # @api private
157
+ def self.manifest_entry_errors_message(errors)
158
+ shown = errors.first(MANIFEST_MAX_REPORTED_ENTRY_ERRORS).join("; ")
159
+ message = "#{errors.length} invalid entries: #{shown}"
160
+ message += "; and #{errors.length - MANIFEST_MAX_REPORTED_ENTRY_ERRORS} more" if errors.length > MANIFEST_MAX_REPORTED_ENTRY_ERRORS
161
+ message
162
+ end
163
+
164
+ class Client
165
+ # --- Manifests ---
166
+
167
+ # Build a `.ant` manifest from DataMaps and public addresses already in
168
+ # hand (POST /v1/manifests/build). Nothing is uploaded or paid for.
169
+ #
170
+ # By default the daemon resolves each shrunk DataMap's root (fetching its
171
+ # wrapper records) and embeds it when it is at most 64 KiB, so recipients
172
+ # skip that fetch (ADR-0006's policy). That reads from the network, so the
173
+ # call can time out; it is safe to retry.
174
+ #
175
+ # Needs antd 0.16.0 or later.
176
+ #
177
+ # @param entries [Array<ManifestEntryInput, Hash>] each with exactly one
178
+ # of +data_map+ or +address+; checked before anything is sent
179
+ # @param name [String, nil] suggested root directory name (one portable
180
+ # path component)
181
+ # @param torrent_info_hashes [Array<String>, nil] BitTorrent info hashes
182
+ # of the same files: hex, 40 characters for v1 and/or 64 for v2
183
+ # @param embed_roots [Boolean] false embeds shrunk DataMaps exactly as
184
+ # given (the wire's +embed_roots+; default true)
185
+ # @return [BuiltManifest]
186
+ # @raise [BadRequestError] an entry with both or neither of +data_map+ and
187
+ # +address+, or a bad +size+, worded as the daemon words it
188
+ # @raise [BadRequestError] an entry the daemon rejects; the message names
189
+ # +entries[i]+
190
+ def build_manifest(entries, name: nil, torrent_info_hashes: nil, embed_roots: true)
191
+ inputs = Antd.manifest_entry_inputs(entries)
192
+ wire = inputs.map do |e|
193
+ m = {}
194
+ m[:path] = e.path unless e.path.nil? || e.path.empty?
195
+ m[:size] = e.size unless e.size.nil?
196
+ if e.data_map.nil? || e.data_map.empty?
197
+ m[:address] = e.address
198
+ else
199
+ m[:data_map] = e.data_map
200
+ end
201
+ m
202
+ end
203
+ body = { entries: wire, embed_roots: embed_roots ? true : false }
204
+ body[:name] = name unless name.nil? || name.empty?
205
+ hashes = Array(torrent_info_hashes)
206
+ body[:torrent_info_hashes] = hashes unless hashes.empty?
207
+
208
+ j = do_json(:post, "/v1/manifests/build", body)
209
+ BuiltManifest.new(
210
+ manifest: manifest_bytes_from_b64(j["manifest"]),
211
+ link: manifest_string(j["link"]),
212
+ entries: manifest_entry_infos(j["entries"])
213
+ )
214
+ end
215
+
216
+ # Decode `.ant` bytes (POST /v1/manifests/decode). With +resolve+, the
217
+ # daemon fetches each entry's DataMap (and wrapper records) to report
218
+ # +exact_size+ and confirm the DataMap is on the network; content chunks
219
+ # are not fetched, and one entry's failure is reported in its +error+,
220
+ # not as a failed call. Nothing is paid for; safe to retry.
221
+ #
222
+ # Needs antd 0.16.0 or later.
223
+ #
224
+ # @param manifest [String] the `.ant` bytes
225
+ # @param resolve [Boolean]
226
+ # @return [DecodedManifest]
227
+ # @raise [BadRequestError] an empty +manifest+
228
+ # @raise [BadRequestError] bytes that are not a manifest
229
+ def decode_manifest(manifest, resolve: false)
230
+ raise BadRequestError, MANIFEST_DECODE_NEEDS_INPUT if manifest.nil? || manifest.empty?
231
+
232
+ post_manifest_decode({ manifest: b64_encode(manifest), resolve: resolve ? true : false })
233
+ end
234
+
235
+ # Decode an `ant://` manifest link, file link or bare 64-hex address
236
+ # (POST /v1/manifests/decode). See +decode_manifest+ for +resolve+.
237
+ #
238
+ # Needs antd 0.16.0 or later.
239
+ #
240
+ # @param link [String]
241
+ # @param resolve [Boolean]
242
+ # @return [DecodedManifest]
243
+ # @raise [BadRequestError] an empty +link+
244
+ # @raise [BadRequestError] a link the daemon cannot read
245
+ def decode_manifest_link(link, resolve: false)
246
+ raise BadRequestError, MANIFEST_DECODE_NEEDS_INPUT if link.nil? || link.empty?
247
+
248
+ post_manifest_decode({ link: link, resolve: resolve ? true : false })
249
+ end
250
+
251
+ private
252
+
253
+ def post_manifest_decode(body)
254
+ j = do_json(:post, "/v1/manifests/decode", body)
255
+ DecodedManifest.new(
256
+ source: j["source"] || "",
257
+ name: manifest_string(j["name"]),
258
+ torrent_info_hash_v1: manifest_string(j["torrent_info_hash_v1"]),
259
+ torrent_info_hash_v2: manifest_string(j["torrent_info_hash_v2"]),
260
+ entries: manifest_entry_infos(j["entries"])
261
+ )
262
+ end
263
+
264
+ # The build response's base64 `.ant` bytes, raw.
265
+ def manifest_bytes_from_b64(value)
266
+ b64_decode(value.to_s)
267
+ rescue ArgumentError => e
268
+ raise InternalError, "build_manifest: manifest is not valid base64: #{e.message}"
269
+ end
270
+
271
+ def manifest_entry_infos(raw)
272
+ return [] unless raw.is_a?(Array)
273
+
274
+ raw.filter_map do |e|
275
+ next unless e.is_a?(Hash)
276
+
277
+ ManifestEntryInfo.new(
278
+ effective_name: e["effective_name"] || "",
279
+ path: manifest_string(e["path"]),
280
+ size: manifest_size(e["size"]),
281
+ kind: e["kind"] || "",
282
+ content_address: e["content_address"] || "",
283
+ data_map: manifest_string(e["data_map"]),
284
+ exact_size: manifest_size(e["exact_size"]),
285
+ error: manifest_string(e["error"])
286
+ )
287
+ end
288
+ end
289
+
290
+ # An optional size: a JSON integer, or nil when absent (never 0).
291
+ def manifest_size(value)
292
+ value.is_a?(Integer) && value >= 0 ? value : nil
293
+ end
294
+
295
+ # An optional string: nil when absent or empty.
296
+ def manifest_string(value)
297
+ value.is_a?(String) && !value.empty? ? value : nil
298
+ end
299
+ end
300
+ end
data/lib/antd/models.rb CHANGED
@@ -195,4 +195,225 @@ module Antd
195
195
  end
196
196
  alias_method :is_meta, :meta?
197
197
  end
198
+
199
+ # One +payments+ quote in full signed form, returned by +pointer_prepare+
200
+ # when called with +include_signed_quotes: true+. +quote+ and
201
+ # +commitment_sidecar+ are opaque base64 strings on both transports (the
202
+ # gRPC client encodes the proto's bytes the same way); only antd parses
203
+ # them. +commitment_sidecar+ is nil for baseline quotes.
204
+ SignedQuoteEntry = Struct.new(:quote_hash, :quote, :commitment_sidecar, keyword_init: true)
205
+
206
+ # --- Pointers (ADR-0016 / ADR-0018 in ant-node) ---
207
+ #
208
+ # A pointer is a small, owner-signed, mutable reference. Its address is
209
+ # derived from the owner's key and never changes; what it points at (a
210
+ # chunk, such as a public file's data map, or another pointer) can be
211
+ # changed by the owner, and the owner can hand it over to another pointer
212
+ # for good. antd signs with owner keys it loaded at startup
213
+ # (+--pointer-keys+), so a write names the pointer by its address (+owner+),
214
+ # never by a key.
215
+ #
216
+ # Every write pays, and every attempt pays again: never retry a create,
217
+ # update, transfer or finalize automatically.
218
+
219
+ # Counter of a pointer's final state, after a transfer or a freeze:
220
+ # 2^64 - 1 (u64::MAX). Nothing can replace a state at this counter.
221
+ POINTER_FINAL_COUNTER = 18_446_744_073_709_551_615
222
+
223
+ # What a pointer's target is. Pass as +kind:+ to the pointer writes; the
224
+ # daemon defaults to +CHUNK+ when it is omitted.
225
+ module PointerKind
226
+ # An immutable chunk, such as a public file's data map. The default.
227
+ CHUNK = "chunk"
228
+ # Another pointer: a forwarding the owner can still change. Use
229
+ # +pointer_transfer+ to hand a pointer over for good.
230
+ POINTER = "pointer"
231
+
232
+ # The kind a raw +kind_tag+ names (0 chunk, 1 pointer), or nil for a tag
233
+ # this SDK does not know.
234
+ #
235
+ # @param tag [Integer]
236
+ # @return [String, nil]
237
+ def self.from_tag(tag)
238
+ case tag
239
+ when 0 then CHUNK
240
+ when 1 then POINTER
241
+ end
242
+ end
243
+ end
244
+
245
+ # The write +pointer_prepare+ signs for an external signer.
246
+ module PointerOp
247
+ # Create the pointer at +target+. Refused if it already exists.
248
+ CREATE = "create"
249
+ # Point the pointer at +target+, creating it if it does not exist.
250
+ UPDATE = "update"
251
+ # Hand the pointer over for good to the pointer at +recipient+, which must
252
+ # already exist. Irreversible once stored.
253
+ TRANSFER = "transfer"
254
+ end
255
+
256
+ # Where a pointer stands with respect to finality (+PointerFinality#status+).
257
+ #
258
+ # The status is the daemon's wire string. These are the values this SDK
259
+ # knows; a newer daemon may send another, which is kept verbatim (the
260
+ # "unknown" arm: +PointerFinality#status_known?+ is false). Give a +case+
261
+ # over the status an +else+ branch.
262
+ module PointerFinalityStatus
263
+ # No peer that answered holds a final state: the owner can still move the
264
+ # pointer. Rules a final state out only when every peer answered.
265
+ OPEN = "open"
266
+ # One final state, not yet held by a majority: a transfer still
267
+ # spreading, or a write that fell short.
268
+ SETTLING = "settling"
269
+ # One final state held by a majority, but not every peer answered, so a
270
+ # rival could still be unseen. Ask again.
271
+ UNCONFIRMED = "unconfirmed"
272
+ # One final state held by a majority, every peer answered, no rival. The
273
+ # only status a recipient should rely on.
274
+ FINAL = "final"
275
+ # The owner signed two or more different final states.
276
+ FORKED = "forked"
277
+
278
+ # Every status this SDK knows.
279
+ KNOWN = [OPEN, SETTLING, UNCONFIRMED, FINAL, FORKED].freeze
280
+
281
+ # @param status [String]
282
+ # @return [Boolean] whether this SDK knows +status+
283
+ def self.known?(status)
284
+ KNOWN.include?(status)
285
+ end
286
+ end
287
+
288
+ # The state a pointer write stored (+pointer_create+ / +pointer_update+).
289
+ # +counter+ is an Integer; a final state sits at +POINTER_FINAL_COUNTER+.
290
+ PointerWriteResult = Struct.new(:address, :state_id, :counter, keyword_init: true)
291
+
292
+ # A pointer's current state, as the network serves it (+pointer_get+).
293
+ #
294
+ # - +owner+: the owner's ML-DSA-65 public key (hex, 1,952 bytes). The
295
+ # address is derived from it, so it is the proof of who owns the pointer.
296
+ # - +counter+: Integer, +POINTER_FINAL_COUNTER+ once final.
297
+ # - +kind+: "chunk", "pointer", or "unknown" for a kind the daemon does not
298
+ # know; +kind_tag+ carries the raw value either way.
299
+ # - +is_final+: the state is at the final counter, so nothing can replace it.
300
+ # - +transferred_to+: the pointer this one was handed over to, or nil.
301
+ PointerRecord = Struct.new(
302
+ :address, :owner, :counter, :kind, :kind_tag, :target, :state_id,
303
+ :is_final, :transferred_to,
304
+ keyword_init: true
305
+ ) do
306
+ # @return [Boolean] true at the final counter: transferred or frozen
307
+ def final?
308
+ is_final == true
309
+ end
310
+
311
+ # @return [String, nil] the target's +PointerKind+, if this SDK knows its tag
312
+ def target_kind
313
+ PointerKind.from_tag(kind_tag)
314
+ end
315
+ end
316
+
317
+ # Where a chain of pointers ends (+pointer_resolve+): the target's +kind+
318
+ # ("chunk", or "unknown" for a kind the daemon does not know), its raw
319
+ # +kind_tag+ and the +target+ address.
320
+ PointerTarget = Struct.new(:kind, :kind_tag, :target, keyword_init: true) do
321
+ # @return [String, nil] the target's +PointerKind+, if this SDK knows its tag
322
+ def target_kind
323
+ PointerKind.from_tag(kind_tag)
324
+ end
325
+ end
326
+
327
+ # One final state, as a finality check reports it. +holders+ is how many
328
+ # peers of the close group hold it; +transferred_to+ is nil unless the
329
+ # state is a transfer.
330
+ PointerFinalState = Struct.new(
331
+ :state_id, :kind, :kind_tag, :target, :holders, :transferred_to,
332
+ keyword_init: true
333
+ )
334
+
335
+ # Where a pointer stands, as its whole close group reports it
336
+ # (+pointer_finality+). Check it before relying on a transfer: only
337
+ # +final?+ means the transfer is beyond dispute.
338
+ #
339
+ # - +group+ / +answered+: peers in the close group, and peers that answered.
340
+ # - +status+: a +PointerFinalityStatus+ string (see there for unknown ones).
341
+ # - +counter+: +open+ only, the Integer counter a read would return, or nil
342
+ # when no state is corroborated.
343
+ # - +state+: +settling+, +unconfirmed+ and +final+, the one final state seen
344
+ # (+PointerFinalState+), else nil.
345
+ # - +states+ / +majority+: +forked+ only, every final state seen (most-held
346
+ # first) and the one a majority holds, which reads return. +states+ is
347
+ # empty and +majority+ nil otherwise.
348
+ # - +transferred_to+: the pointer this one was handed over to, once that is
349
+ # beyond dispute (status +final+), else nil.
350
+ PointerFinality = Struct.new(
351
+ :address, :group, :answered, :status, :counter, :state, :states,
352
+ :majority, :transferred_to,
353
+ keyword_init: true
354
+ ) do
355
+ # @return [Boolean] one final state, held by a majority, every peer
356
+ # answering and no rival seen
357
+ def final?
358
+ status == PointerFinalityStatus::FINAL
359
+ end
360
+
361
+ # @return [Boolean] false for a status a newer daemon added
362
+ def status_known?
363
+ PointerFinalityStatus.known?(status)
364
+ end
365
+ end
366
+
367
+ # Whose pointer decides what an address resolves to, after following every
368
+ # transfer (+pointer_controller+). +transfers+ lists every pointer the
369
+ # address was handed over to, in order; +frozen+ is true when +pointer+ is
370
+ # final without being handed over, so nobody can move it.
371
+ PointerController = Struct.new(:pointer, :transfers, :frozen, keyword_init: true)
372
+
373
+ # What a transfer stored, and where the close group says it stands.
374
+ # +finality+ (a +PointerFinality+) is nil when that check failed, and
375
+ # +finality_error+ says why; the transfer itself is stored and final either
376
+ # way. Ask again with +pointer_finality+.
377
+ PointerTransferResult = Struct.new(:address, :state_id, :finality, :finality_error, keyword_init: true)
378
+
379
+ # A pointer write signed by the daemon and quoted for an external signer
380
+ # (+pointer_prepare+). Pay +payments+ with +payForQuotes()+, then call
381
+ # +pointer_finalize+. +op+ is a +PointerOp+ string, +counter+ an Integer,
382
+ # +payment_type+ always "wave_batch" (a pointer state is paid with one
383
+ # quote). +signed_quotes+ (+SignedQuoteEntry+) is filled only when the
384
+ # prepare asked for it, else empty.
385
+ #
386
+ # While it awaits payment, other writes to the pointer raise
387
+ # +ConflictError+ until it is finalized or expires (one hour).
388
+ PointerPrepareResult = Struct.new(
389
+ :upload_id, :op, :address, :state_id, :counter, :payment_type,
390
+ :payments, :total_amount, :payment_vault_address, :payment_token_address,
391
+ :rpc_url, :signed_quotes,
392
+ keyword_init: true
393
+ )
394
+
395
+ # The state a pointer finalize stored. +finality+ is set for a transfer
396
+ # (where the close group says it stands), else nil.
397
+ PointerFinalizeResult = Struct.new(:address, :state_id, :counter, :finality, keyword_init: true)
398
+
399
+ # Parses a pointer counter. The daemon sends counters as decimal strings on
400
+ # both transports, because a final state sits at 2^64 - 1, past what a JSON
401
+ # number keeps exactly in many languages. Ruby integers are unbounded, so
402
+ # the value is checked against the daemon's u64 range here.
403
+ #
404
+ # Strict: only a String of ASCII digits within u64 parses. +Integer()+ alone
405
+ # would also take a sign, underscores or a radix prefix, and a malformed
406
+ # counter must never read as 0.
407
+ #
408
+ # @param field [String] the field name, for the error message
409
+ # @param value [Object] the wire value
410
+ # @return [Integer]
411
+ # @raise [InternalError] when +value+ is not a decimal u64
412
+ def self.parse_pointer_counter(field, value)
413
+ if value.is_a?(String) && value.match?(/\A[0-9]+\z/)
414
+ n = Integer(value, 10)
415
+ return n if n <= POINTER_FINAL_COUNTER
416
+ end
417
+ raise InternalError, "invalid pointer #{field} #{value.inspect}: expected a decimal u64"
418
+ end
198
419
  end
@@ -7,7 +7,7 @@ require 'google/protobuf'
7
7
  require 'antd/v1/common_pb'
8
8
 
9
9
 
10
- descriptor_data = "\n\x14\x61ntd/v1/chunks.proto\x12\x07\x61ntd.v1\x1a\x14\x61ntd/v1/common.proto\"\"\n\x0fGetChunkRequest\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\" \n\x10GetChunkResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\x1f\n\x0fPutChunkRequest\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"@\n\x10PutChunkResponse\x12\x1b\n\x04\x63ost\x18\x01 \x01(\x0b\x32\r.antd.v1.Cost\x12\x0f\n\x07\x61\x64\x64ress\x18\x02 \x01(\t\"#\n\x13PrepareChunkRequest\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\xf6\x01\n\x14PrepareChunkResponse\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x16\n\x0e\x61lready_stored\x18\x02 \x01(\x08\x12\x11\n\tupload_id\x18\x03 \x01(\t\x12\x14\n\x0cpayment_type\x18\x04 \x01(\t\x12\'\n\x08payments\x18\x05 \x03(\x0b\x32\x15.antd.v1.PaymentEntry\x12\x14\n\x0ctotal_amount\x18\x06 \x01(\t\x12\x1d\n\x15payment_vault_address\x18\x07 \x01(\t\x12\x1d\n\x15payment_token_address\x18\x08 \x01(\t\x12\x0f\n\x07rpc_url\x18\t \x01(\t\"\x9a\x01\n\x14\x46inalizeChunkRequest\x12\x11\n\tupload_id\x18\x01 \x01(\t\x12>\n\ttx_hashes\x18\x02 \x03(\x0b\x32+.antd.v1.FinalizeChunkRequest.TxHashesEntry\x1a/\n\rTxHashesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"(\n\x15\x46inalizeChunkResponse\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t2\xa3\x02\n\x0c\x43hunkService\x12:\n\x03Get\x12\x18.antd.v1.GetChunkRequest\x1a\x19.antd.v1.GetChunkResponse\x12:\n\x03Put\x12\x18.antd.v1.PutChunkRequest\x1a\x19.antd.v1.PutChunkResponse\x12K\n\x0cPrepareChunk\x12\x1c.antd.v1.PrepareChunkRequest\x1a\x1d.antd.v1.PrepareChunkResponse\x12N\n\rFinalizeChunk\x12\x1d.antd.v1.FinalizeChunkRequest\x1a\x1e.antd.v1.FinalizeChunkResponseBDZ8github.com/WithAutonomi/ant-sdk/antd-go/proto/antd/v1;v1\xaa\x02\x07\x41ntd.V1b\x06proto3"
10
+ descriptor_data = "\n\x14\x61ntd/v1/chunks.proto\x12\x07\x61ntd.v1\x1a\x14\x61ntd/v1/common.proto\"\"\n\x0fGetChunkRequest\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\" \n\x10GetChunkResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\x1f\n\x0fPutChunkRequest\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"@\n\x10PutChunkResponse\x12\x1b\n\x04\x63ost\x18\x01 \x01(\x0b\x32\r.antd.v1.Cost\x12\x0f\n\x07\x61\x64\x64ress\x18\x02 \x01(\t\"B\n\x13PrepareChunkRequest\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x1d\n\x15include_signed_quotes\x18\x02 \x01(\x08\"\xa8\x02\n\x14PrepareChunkResponse\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x16\n\x0e\x61lready_stored\x18\x02 \x01(\x08\x12\x11\n\tupload_id\x18\x03 \x01(\t\x12\x14\n\x0cpayment_type\x18\x04 \x01(\t\x12\'\n\x08payments\x18\x05 \x03(\x0b\x32\x15.antd.v1.PaymentEntry\x12\x14\n\x0ctotal_amount\x18\x06 \x01(\t\x12\x1d\n\x15payment_vault_address\x18\x07 \x01(\t\x12\x1d\n\x15payment_token_address\x18\x08 \x01(\t\x12\x0f\n\x07rpc_url\x18\t \x01(\t\x12\x30\n\rsigned_quotes\x18\n \x03(\x0b\x32\x19.antd.v1.SignedQuoteEntry\"\x9a\x01\n\x14\x46inalizeChunkRequest\x12\x11\n\tupload_id\x18\x01 \x01(\t\x12>\n\ttx_hashes\x18\x02 \x03(\x0b\x32+.antd.v1.FinalizeChunkRequest.TxHashesEntry\x1a/\n\rTxHashesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"(\n\x15\x46inalizeChunkResponse\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t2\xa3\x02\n\x0c\x43hunkService\x12:\n\x03Get\x12\x18.antd.v1.GetChunkRequest\x1a\x19.antd.v1.GetChunkResponse\x12:\n\x03Put\x12\x18.antd.v1.PutChunkRequest\x1a\x19.antd.v1.PutChunkResponse\x12K\n\x0cPrepareChunk\x12\x1c.antd.v1.PrepareChunkRequest\x1a\x1d.antd.v1.PrepareChunkResponse\x12N\n\rFinalizeChunk\x12\x1d.antd.v1.FinalizeChunkRequest\x1a\x1e.antd.v1.FinalizeChunkResponseBDZ8github.com/WithAutonomi/ant-sdk/antd-go/proto/antd/v1;v1\xaa\x02\x07\x41ntd.V1b\x06proto3"
11
11
 
12
12
  pool = ::Google::Protobuf::DescriptorPool.generated_pool
13
13
  pool.add_serialized_file(descriptor_data)
@@ -5,7 +5,7 @@
5
5
  require 'google/protobuf'
6
6
 
7
7
 
8
- descriptor_data = "\n\x14\x61ntd/v1/common.proto\x12\x07\x61ntd.v1\"y\n\x04\x43ost\x12\x13\n\x0b\x61tto_tokens\x18\x01 \x01(\t\x12\x11\n\tfile_size\x18\x02 \x01(\x04\x12\x13\n\x0b\x63hunk_count\x18\x03 \x01(\r\x12\x1e\n\x16\x65stimated_gas_cost_wei\x18\x04 \x01(\t\x12\x14\n\x0cpayment_mode\x18\x05 \x01(\t\"\x16\n\x07\x41\x64\x64ress\x12\x0b\n\x03hex\x18\x01 \x01(\t\"\x1d\n\x0ePublicKeyProto\x12\x0b\n\x03hex\x18\x01 \x01(\t\"\x1d\n\x0eSecretKeyProto\x12\x0b\n\x03hex\x18\x01 \x01(\t\"K\n\x0cPaymentEntry\x12\x12\n\nquote_hash\x18\x01 \x01(\t\x12\x17\n\x0frewards_address\x18\x02 \x01(\t\x12\x0e\n\x06\x61mount\x18\x03 \x01(\tBDZ8github.com/WithAutonomi/ant-sdk/antd-go/proto/antd/v1;v1\xaa\x02\x07\x41ntd.V1b\x06proto3"
8
+ descriptor_data = "\n\x14\x61ntd/v1/common.proto\x12\x07\x61ntd.v1\"y\n\x04\x43ost\x12\x13\n\x0b\x61tto_tokens\x18\x01 \x01(\t\x12\x11\n\tfile_size\x18\x02 \x01(\x04\x12\x13\n\x0b\x63hunk_count\x18\x03 \x01(\r\x12\x1e\n\x16\x65stimated_gas_cost_wei\x18\x04 \x01(\t\x12\x14\n\x0cpayment_mode\x18\x05 \x01(\t\"\x16\n\x07\x41\x64\x64ress\x12\x0b\n\x03hex\x18\x01 \x01(\t\"\x1d\n\x0ePublicKeyProto\x12\x0b\n\x03hex\x18\x01 \x01(\t\"\x1d\n\x0eSecretKeyProto\x12\x0b\n\x03hex\x18\x01 \x01(\t\"K\n\x0cPaymentEntry\x12\x12\n\nquote_hash\x18\x01 \x01(\t\x12\x17\n\x0frewards_address\x18\x02 \x01(\t\x12\x0e\n\x06\x61mount\x18\x03 \x01(\t\"Q\n\x10SignedQuoteEntry\x12\x12\n\nquote_hash\x18\x01 \x01(\t\x12\r\n\x05quote\x18\x02 \x01(\x0c\x12\x1a\n\x12\x63ommitment_sidecar\x18\x03 \x01(\x0c\x42\x44Z8github.com/WithAutonomi/ant-sdk/antd-go/proto/antd/v1;v1\xaa\x02\x07\x41ntd.V1b\x06proto3"
9
9
 
10
10
  pool = ::Google::Protobuf::DescriptorPool.generated_pool
11
11
  pool.add_serialized_file(descriptor_data)
@@ -17,5 +17,6 @@ module Antd
17
17
  PublicKeyProto = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.PublicKeyProto").msgclass
18
18
  SecretKeyProto = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.SecretKeyProto").msgclass
19
19
  PaymentEntry = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.PaymentEntry").msgclass
20
+ SignedQuoteEntry = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.SignedQuoteEntry").msgclass
20
21
  end
21
22
  end