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.
- checksums.yaml +4 -4
- data/README.md +228 -10
- data/lib/antd/client.rb +282 -27
- data/lib/antd/errors.rb +353 -0
- data/lib/antd/grpc_client.rb +245 -2
- data/lib/antd/grpc_manifests.rb +128 -0
- data/lib/antd/manifests.rb +300 -0
- data/lib/antd/models.rb +221 -0
- data/lib/antd/v1/chunks_pb.rb +1 -1
- data/lib/antd/v1/common_pb.rb +2 -1
- data/lib/antd/v1/data_pb.rb +1 -1
- data/lib/antd/v1/health_pb.rb +1 -1
- data/lib/antd/v1/manifests_pb.rb +22 -0
- data/lib/antd/v1/manifests_services_pb.rb +39 -0
- data/lib/antd/v1/pointers_pb.rb +38 -0
- data/lib/antd/v1/pointers_services_pb.rb +59 -0
- data/lib/antd/v1/upload_pb.rb +2 -1
- data/lib/antd/v1/upload_services_pb.rb +4 -2
- data/lib/antd/version.rb +1 -1
- data/lib/antd.rb +2 -0
- metadata +8 -2
data/lib/antd/errors.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
3
5
|
module Antd
|
|
4
6
|
# Base error type for all antd errors.
|
|
5
7
|
class AntdError < StandardError
|
|
@@ -56,6 +58,357 @@ module Antd
|
|
|
56
58
|
def initialize(message) = super(message, status_code: 503)
|
|
57
59
|
end
|
|
58
60
|
|
|
61
|
+
# The pointer is at its final state, transferred or frozen: nothing can
|
|
62
|
+
# update it or hand it over again (HTTP 409 with +code: "POINTER_FINAL"+;
|
|
63
|
+
# gRPC FAILED_PRECONDITION with +x-antd-error-code: POINTER_FINAL+).
|
|
64
|
+
# Usually refused before anything is paid: the daemon reads the pointer
|
|
65
|
+
# first. A write that was already paid for when the network refused it (the
|
|
66
|
+
# pointer became final while the write was in flight) keeps that payment, so
|
|
67
|
+
# read the pointer before paying again.
|
|
68
|
+
class PointerFinalError < AntdError
|
|
69
|
+
def initialize(message) = super(message, status_code: 409)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The pointer's owner signed two different final states and no one of them
|
|
73
|
+
# is held by a majority of the close group, so there is no answer to give
|
|
74
|
+
# (HTTP 409 with +code: "POINTER_FORKED"+; gRPC FAILED_PRECONDITION with
|
|
75
|
+
# +x-antd-error-code: POINTER_FORKED+). Subclasses +ForkError+, so
|
|
76
|
+
# +rescue Antd::ForkError+ catches it too.
|
|
77
|
+
class PointerForkedError < ForkError
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The request conflicts with the resource's current state (HTTP 409 with
|
|
81
|
+
# +code: "CONFLICT"+; gRPC ABORTED with +x-antd-error-code: CONFLICT+). For
|
|
82
|
+
# pointers, two cases share this code, and it does not say which one
|
|
83
|
+
# happened or whether anything was paid: a write while a prepared write to
|
|
84
|
+
# the same pointer awaits payment is refused before paying (finalize the
|
|
85
|
+
# prepared write, or wait for it to expire); an update that lost to a newer
|
|
86
|
+
# state while it was in flight may already have been paid for. Read the
|
|
87
|
+
# pointer again and check what was paid before submitting another paid
|
|
88
|
+
# write.
|
|
89
|
+
class ConflictError < AntdError
|
|
90
|
+
def initialize(message) = super(message, status_code: 409)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Too few of an address's close group answered, or took a write, for the
|
|
94
|
+
# result to be the network's (HTTP 503 with +code: "CLOSE_GROUP_SHORTFALL"+;
|
|
95
|
+
# gRPC UNAVAILABLE with +x-antd-error-code: CLOSE_GROUP_SHORTFALL+).
|
|
96
|
+
#
|
|
97
|
+
# Reads are free to retry; one during an update in flight or churn usually
|
|
98
|
+
# clears. A paid write is not, unless +retryable+ is true: a pointer
|
|
99
|
+
# finalize then kept the paid state under the same +upload_id+, and
|
|
100
|
+
# repeating that finalize stores it against the same payment. Over REST
|
|
101
|
+
# +retryable+ is the body's +retryable+ (true only for JSON +true+); over
|
|
102
|
+
# gRPC it is read from the "(paid attempt retained...)" hint that closes
|
|
103
|
+
# the status details.
|
|
104
|
+
#
|
|
105
|
+
# Not a +ServiceUnavailableError+ (which means a daemon without a wallet):
|
|
106
|
+
# rescue it by name. Daemons without the pointer API reported a shortfall as
|
|
107
|
+
# a 500, an +InternalError+.
|
|
108
|
+
class CloseGroupShortfallError < AntdError
|
|
109
|
+
attr_reader :retryable
|
|
110
|
+
|
|
111
|
+
# @param message [String]
|
|
112
|
+
# @param retryable [Boolean] whether a pointer finalize kept the paid state
|
|
113
|
+
def initialize(message, retryable: false)
|
|
114
|
+
@retryable = retryable == true
|
|
115
|
+
super(message, status_code: 503)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
alias retryable? retryable
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# A finalize stored some chunks while others remained unstored after the
|
|
122
|
+
# daemon's retries (HTTP 502 with +code: "PARTIAL_UPLOAD"+; gRPC ABORTED).
|
|
123
|
+
# The on-chain payment persists and the stored chunks stay on the network.
|
|
124
|
+
# How to finish the upload depends on +retryable+ and +retention_known+:
|
|
125
|
+
#
|
|
126
|
+
# - +retryable+ (antd >= 0.14.0; implies +retention_known+): the daemon kept
|
|
127
|
+
# the paid attempt (payment proofs + unstored chunks) under the same
|
|
128
|
+
# +upload_id+. Call the same +finalize_*+ method again with the same
|
|
129
|
+
# +upload_id+ and payment artefacts to store the remainder against the
|
|
130
|
+
# same payment -- no re-prepare, no second signature, no double payment.
|
|
131
|
+
# Bound the loop: a persistent failure raises this error on every call, so
|
|
132
|
+
# cap the attempts and treat a +chunks_failed+ that stops shrinking as
|
|
133
|
+
# stuck. The retained attempt expires with the daemon's pending-upload
|
|
134
|
+
# TTL.
|
|
135
|
+
# - +retention_known+ and not +retryable+: the daemon confirmed nothing was
|
|
136
|
+
# retained (a merkle finalize with deliberately unpaid batches).
|
|
137
|
+
# Re-preparing the same content skips already-stored chunks, so a retry
|
|
138
|
+
# pays only for the missing remainder.
|
|
139
|
+
# - not +retention_known+: retention is unknown. The error did not say, in
|
|
140
|
+
# a form this SDK could read, whether the paid attempt was kept, and the
|
|
141
|
+
# daemon may still hold it: it records the resume handle before it
|
|
142
|
+
# returns the error. Stop automatic recovery, keep the +upload_id+ and the
|
|
143
|
+
# original payment artefacts (tx hashes, or the merkle winner pool hash),
|
|
144
|
+
# and reconcile before re-preparing or paying again. Never pay again on
|
|
145
|
+
# this signal alone. Daemons older than 0.14.0 never send +retryable+, so
|
|
146
|
+
# their REST partial uploads read as unknown.
|
|
147
|
+
#
|
|
148
|
+
# Over REST the counts and flags come from the structured error body; a
|
|
149
|
+
# count of the wrong JSON type reads as 0, and +retention_known+ is true
|
|
150
|
+
# only when the body's +retryable+ is a JSON boolean. Over gRPC they are
|
|
151
|
+
# parsed best-effort from the status details ("Partial upload: S/T chunks
|
|
152
|
+
# stored, F failed after retries: <reason> (<hint>)"); only an ABORTED
|
|
153
|
+
# whose details start with "Partial upload:" is a partial upload.
|
|
154
|
+
# +retention_known+ is true there only when the details start with the
|
|
155
|
+
# counts, all three parse, and the details end with one of the daemon's two
|
|
156
|
+
# hints: "(paid attempt retained...)" sets +retryable+, and "(stored chunks
|
|
157
|
+
# persist; re-prepare the same content...)" means the daemon confirmed
|
|
158
|
+
# nothing was retained (daemons older than 0.14.0 write only this one).
|
|
159
|
+
# Counts that do not parse leave all three zero and both flags false.
|
|
160
|
+
# Readable counts with a missing, truncated or unrecognised hint keep the
|
|
161
|
+
# counts, but both flags stay false: retention unknown, not "nothing
|
|
162
|
+
# retained".
|
|
163
|
+
#
|
|
164
|
+
# Subclasses +NetworkError+ because the daemon reports it as a 502: existing
|
|
165
|
+
# +rescue Antd::NetworkError+ blocks keep catching it, and +status_code+ is
|
|
166
|
+
# 502 on both transports. Rescue +PartialUploadError+ first to handle it
|
|
167
|
+
# specifically. See docs/external-signer-flow.md section 6.
|
|
168
|
+
class PartialUploadError < NetworkError
|
|
169
|
+
attr_reader :chunks_stored, :chunks_failed, :total_chunks, :retryable, :retention_known
|
|
170
|
+
|
|
171
|
+
# @param message [String]
|
|
172
|
+
# @param chunks_stored [Integer] chunks confirmed stored on the network
|
|
173
|
+
# @param chunks_failed [Integer] chunks still unstored after retries
|
|
174
|
+
# @param total_chunks [Integer] chunks in the upload
|
|
175
|
+
# @param retryable [Boolean] whether the daemon retained the paid attempt
|
|
176
|
+
# @param retention_known [Boolean] whether the daemon said, in a form this
|
|
177
|
+
# SDK could read, whether it retained the paid attempt. +retryable+
|
|
178
|
+
# implies it: +retryable: true+ always yields +retention_known == true+.
|
|
179
|
+
def initialize(message, chunks_stored: 0, chunks_failed: 0, total_chunks: 0, retryable: false,
|
|
180
|
+
retention_known: false)
|
|
181
|
+
@chunks_stored = chunks_stored
|
|
182
|
+
@chunks_failed = chunks_failed
|
|
183
|
+
@total_chunks = total_chunks
|
|
184
|
+
@retryable = retryable
|
|
185
|
+
@retention_known = (retention_known || retryable) ? true : false
|
|
186
|
+
super(message)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
alias retryable? retryable
|
|
190
|
+
alias retention_known? retention_known
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Fixed text every PARTIAL_UPLOAD message from the daemon opens with. Over
|
|
194
|
+
# gRPC the status carries no structured code, so an ABORTED status is a
|
|
195
|
+
# partial upload only when its details start with this prefix.
|
|
196
|
+
PARTIAL_UPLOAD_PREFIX = "Partial upload:"
|
|
197
|
+
|
|
198
|
+
# Fixed prefix of the daemon's PARTIAL_UPLOAD message:
|
|
199
|
+
# "Partial upload: <stored>/<total> chunks stored, <failed> failed".
|
|
200
|
+
# Anchored with +\A+ (start of input; +^+ would also match after a
|
|
201
|
+
# newline), so counts quoted later in a garbled message are never read.
|
|
202
|
+
PARTIAL_UPLOAD_COUNTS = %r{\APartial upload: (\d+)/(\d+) chunks stored, (\d+) failed}
|
|
203
|
+
|
|
204
|
+
# The daemon closes every PARTIAL_UPLOAD message with one of two
|
|
205
|
+
# parenthesised hints (partial_upload_hint in antd/src/error.rs). This one
|
|
206
|
+
# opens the hint when it kept the paid attempt for a same-upload_id retry.
|
|
207
|
+
PARTIAL_UPLOAD_RETAINED_HINT = "paid attempt retained"
|
|
208
|
+
|
|
209
|
+
# This one opens the hint when it did not. Daemons older than 0.14.0 write
|
|
210
|
+
# only this one.
|
|
211
|
+
PARTIAL_UPLOAD_NOT_RETAINED_HINT = "stored chunks persist; re-prepare the same content"
|
|
212
|
+
private_constant :PARTIAL_UPLOAD_NOT_RETAINED_HINT
|
|
213
|
+
|
|
214
|
+
# The hint that closes the message: "(<hint>...)" at the very end. +\z+ is
|
|
215
|
+
# the end of the input; Ruby's +$+ is the end of a LINE and +\Z+ allows a
|
|
216
|
+
# trailing newline. A hint quoted inside the failure reason, a truncated
|
|
217
|
+
# or unclosed tail, or any text after the hint does not match.
|
|
218
|
+
PARTIAL_UPLOAD_RETENTION_TAIL =
|
|
219
|
+
/\((#{Regexp.union(PARTIAL_UPLOAD_RETAINED_HINT, PARTIAL_UPLOAD_NOT_RETAINED_HINT).source})[^()]*\)\z/
|
|
220
|
+
private_constant :PARTIAL_UPLOAD_RETENTION_TAIL
|
|
221
|
+
|
|
222
|
+
# Largest count the daemon can send (its counts are u64). Ruby integers are
|
|
223
|
+
# unbounded, so a larger digit run is treated as a failed conversion.
|
|
224
|
+
PARTIAL_UPLOAD_COUNT_MAX = 18_446_744_073_709_551_615
|
|
225
|
+
private_constant :PARTIAL_UPLOAD_COUNT_MAX
|
|
226
|
+
|
|
227
|
+
# Whether a gRPC status's details are the daemon's PARTIAL_UPLOAD message.
|
|
228
|
+
# Anchored: the details must start with +PARTIAL_UPLOAD_PREFIX+. A status
|
|
229
|
+
# that merely quotes "Partial upload:" further into its text (an upstream
|
|
230
|
+
# error wrapping one, say) is not a partial upload and must not select the
|
|
231
|
+
# paid-attempt recovery path with zero counts.
|
|
232
|
+
#
|
|
233
|
+
# Pass +GRPC::BadStatus#details+ -- the status message exactly as the
|
|
234
|
+
# daemon sent it -- not +#message+, which grpc-ruby decorates as
|
|
235
|
+
# "<code>:<details>" ("10:Partial upload: ...") and so never starts with
|
|
236
|
+
# the prefix.
|
|
237
|
+
#
|
|
238
|
+
# @param details [String, nil] gRPC status details
|
|
239
|
+
# @return [Boolean]
|
|
240
|
+
def self.partial_upload_message?(details)
|
|
241
|
+
details.to_s.start_with?(PARTIAL_UPLOAD_PREFIX)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Recovers the chunk counts and the retention flags from a PARTIAL_UPLOAD
|
|
245
|
+
# message (over gRPC, the status details). Used for gRPC, where the status
|
|
246
|
+
# carries no structured detail; REST callers get the body fields instead.
|
|
247
|
+
#
|
|
248
|
+
# Conservative: +retention_known+ is true only when the message starts
|
|
249
|
+
# with the counts pattern ("Partial upload: <stored>/<total> chunks
|
|
250
|
+
# stored, <failed> failed"), all three counts converted (each at most
|
|
251
|
+
# u64::MAX, the daemon's count type), and the message ends with one of the
|
|
252
|
+
# daemon's two hints, "(paid attempt retained...)" or "(stored chunks
|
|
253
|
+
# persist; re-prepare the same content...)"; +retryable+ is then true only
|
|
254
|
+
# for the first. On a prefix or pattern miss, or a count out of range, all
|
|
255
|
+
# three counts are 0 and both flags are false. Readable counts with a
|
|
256
|
+
# missing, truncated or unrecognised tail, or text after it, keep the
|
|
257
|
+
# counts but leave both flags false: the daemon's answer on retention was
|
|
258
|
+
# not read, so retention is unknown, never "nothing retained". A message
|
|
259
|
+
# the parser cannot fully read never selects the same-upload_id retry, and
|
|
260
|
+
# never reads as confirmed non-retention.
|
|
261
|
+
#
|
|
262
|
+
# @param message [String]
|
|
263
|
+
# @return [Hash] +:chunks_stored+, +:chunks_failed+, +:total_chunks+,
|
|
264
|
+
# +:retryable+, +:retention_known+ -- splat into +PartialUploadError.new+
|
|
265
|
+
def self.parse_partial_upload_message(message)
|
|
266
|
+
text = message.to_s
|
|
267
|
+
m = partial_upload_message?(text) && PARTIAL_UPLOAD_COUNTS.match(text)
|
|
268
|
+
# The groups are ASCII digit runs, so #to_i is exact; only the range can fail.
|
|
269
|
+
stored, total, failed = m && [m[1], m[2], m[3]].map(&:to_i)
|
|
270
|
+
unless m && [stored, total, failed].all? { |n| n <= PARTIAL_UPLOAD_COUNT_MAX }
|
|
271
|
+
return { chunks_stored: 0, chunks_failed: 0, total_chunks: 0, retryable: false, retention_known: false }
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Only the hint that closes the message is the daemon's answer.
|
|
275
|
+
tail = PARTIAL_UPLOAD_RETENTION_TAIL.match(text)
|
|
276
|
+
{
|
|
277
|
+
chunks_stored: stored,
|
|
278
|
+
chunks_failed: failed,
|
|
279
|
+
total_chunks: total,
|
|
280
|
+
retryable: !tail.nil? && tail[1] == PARTIAL_UPLOAD_RETAINED_HINT,
|
|
281
|
+
retention_known: !tail.nil?
|
|
282
|
+
}
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Metadata key the daemon (0.16.0 or later) sets on gRPC
|
|
286
|
+
# statuses whose code is shared with older errors, carrying the REST +code+.
|
|
287
|
+
ANTD_ERROR_CODE_METADATA = "x-antd-error-code"
|
|
288
|
+
|
|
289
|
+
# The gRPC status code the daemon pairs each tagged code with
|
|
290
|
+
# (antd/src/error.rs). A tag on any other status code is not trusted.
|
|
291
|
+
GRPC_STATUS_FOR_ERROR_CODE = {
|
|
292
|
+
"POINTER_FINAL" => 9, # FAILED_PRECONDITION
|
|
293
|
+
"POINTER_FORKED" => 9, # FAILED_PRECONDITION
|
|
294
|
+
"CONFLICT" => 10, # ABORTED
|
|
295
|
+
"CLOSE_GROUP_SHORTFALL" => 14 # UNAVAILABLE
|
|
296
|
+
}.freeze
|
|
297
|
+
private_constant :GRPC_STATUS_FOR_ERROR_CODE
|
|
298
|
+
|
|
299
|
+
# The hint that closes a CLOSE_GROUP_SHORTFALL message when a pointer
|
|
300
|
+
# finalize kept the paid state: "(paid attempt retained...)" at the very
|
|
301
|
+
# end of the details, as for a retained partial upload.
|
|
302
|
+
CLOSE_GROUP_RETAINED_TAIL = /\(#{Regexp.escape(PARTIAL_UPLOAD_RETAINED_HINT)}[^()]*\)\z/
|
|
303
|
+
private_constant :CLOSE_GROUP_RETAINED_TAIL
|
|
304
|
+
|
|
305
|
+
# The typed error for a daemon error +code+ that shares its HTTP status (and
|
|
306
|
+
# gRPC code) with older errors: the 409s POINTER_FINAL, POINTER_FORKED and
|
|
307
|
+
# CONFLICT, and CLOSE_GROUP_SHORTFALL's 503. nil for any other code, which
|
|
308
|
+
# keeps the status-based mapping.
|
|
309
|
+
#
|
|
310
|
+
# @param code [Object] the daemon's error code
|
|
311
|
+
# @param message [String]
|
|
312
|
+
# @param retryable [Boolean] CLOSE_GROUP_SHORTFALL only
|
|
313
|
+
# @return [AntdError, nil]
|
|
314
|
+
def self.error_for_code(code, message, retryable: false)
|
|
315
|
+
case code
|
|
316
|
+
when "POINTER_FINAL" then PointerFinalError.new(message)
|
|
317
|
+
when "POINTER_FORKED" then PointerForkedError.new(message)
|
|
318
|
+
when "CONFLICT" then ConflictError.new(message)
|
|
319
|
+
when "CLOSE_GROUP_SHORTFALL" then CloseGroupShortfallError.new(message, retryable: retryable)
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# The typed error for a gRPC status the daemon tagged with
|
|
324
|
+
# +x-antd-error-code+ (in the trailing metadata, +GRPC::BadStatus#metadata+),
|
|
325
|
+
# when the tag agrees with the status code: FAILED_PRECONDITION for
|
|
326
|
+
# POINTER_FINAL / POINTER_FORKED, ABORTED for CONFLICT, UNAVAILABLE for
|
|
327
|
+
# CLOSE_GROUP_SHORTFALL. nil for an untagged status, a tag this SDK does not
|
|
328
|
+
# know, or a tag on another status code; the caller then keeps the
|
|
329
|
+
# code-based mapping (so an untagged FAILED_PRECONDITION is still a
|
|
330
|
+
# +PaymentError+).
|
|
331
|
+
#
|
|
332
|
+
# @param code [Integer] gRPC status code
|
|
333
|
+
# @param details [String, nil] the status details as the daemon sent them,
|
|
334
|
+
# read for CLOSE_GROUP_SHORTFALL's retained hint
|
|
335
|
+
# @param message [String] the message for the raised error
|
|
336
|
+
# @param metadata [Hash, nil] trailing metadata
|
|
337
|
+
# @return [AntdError, nil]
|
|
338
|
+
def self.error_for_grpc_metadata(code, details, message, metadata)
|
|
339
|
+
tag = metadata.is_a?(Hash) ? metadata[ANTD_ERROR_CODE_METADATA] : nil
|
|
340
|
+
tag = tag.first if tag.is_a?(Array)
|
|
341
|
+
return nil unless tag.is_a?(String) && GRPC_STATUS_FOR_ERROR_CODE[tag] == code
|
|
342
|
+
|
|
343
|
+
error_for_code(tag, message, retryable: CLOSE_GROUP_RETAINED_TAIL.match?(details.to_s))
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Maps a non-2xx REST response onto a typed error, preferring the body's
|
|
347
|
+
# machine-readable +code+ over the bare HTTP status where they diverge:
|
|
348
|
+
# +PARTIAL_UPLOAD+ arrives as a 502 that would otherwise read as a generic
|
|
349
|
+
# +NetworkError+, POINTER_FINAL, POINTER_FORKED and CONFLICT are 409s that
|
|
350
|
+
# must not read as +AlreadyExistsError+, and CLOSE_GROUP_SHORTFALL is a 503
|
|
351
|
+
# that must not read as +ServiceUnavailableError+ (see +error_for_code+).
|
|
352
|
+
# Every other code keeps the status-based mapping of +error_for_status+. A
|
|
353
|
+
# JSON object body's +error+ field becomes the message when it is a string;
|
|
354
|
+
# otherwise (non-JSON body, a JSON value that is not an object, a non-string
|
|
355
|
+
# +error+) the raw body is the message.
|
|
356
|
+
#
|
|
357
|
+
# Never raises on a malformed body: every field is type-checked, not
|
|
358
|
+
# coerced. Only a +code+ that is exactly the string "PARTIAL_UPLOAD"
|
|
359
|
+
# selects +PartialUploadError+; a count that is not a non-negative JSON
|
|
360
|
+
# integer reads as 0; +retryable+ is true only for JSON +true+; and
|
|
361
|
+
# +retention_known+ is true only when +retryable+ is a JSON boolean (absent
|
|
362
|
+
# on daemons < 0.14.0, +null+ or any other type reads as unknown).
|
|
363
|
+
#
|
|
364
|
+
# @param code [Integer] HTTP status
|
|
365
|
+
# @param body [String, nil] raw response body
|
|
366
|
+
# @return [AntdError]
|
|
367
|
+
def self.error_for_response(code, body)
|
|
368
|
+
message = body.to_s
|
|
369
|
+
parsed = begin
|
|
370
|
+
JSON.parse(message)
|
|
371
|
+
rescue JSON::ParserError
|
|
372
|
+
nil # use raw body as message
|
|
373
|
+
end
|
|
374
|
+
parsed = nil unless parsed.is_a?(Hash)
|
|
375
|
+
|
|
376
|
+
if parsed
|
|
377
|
+
message = parsed["error"] if parsed["error"].is_a?(String)
|
|
378
|
+
if parsed["code"] == "PARTIAL_UPLOAD"
|
|
379
|
+
return PartialUploadError.new(
|
|
380
|
+
message,
|
|
381
|
+
chunks_stored: body_count(parsed["chunks_stored"]),
|
|
382
|
+
chunks_failed: body_count(parsed["chunks_failed"]),
|
|
383
|
+
total_chunks: body_count(parsed["total_chunks"]),
|
|
384
|
+
retryable: parsed["retryable"] == true,
|
|
385
|
+
# Known only when the daemon sent the flag as a JSON boolean. Absent
|
|
386
|
+
# (daemons < 0.14.0), null or any other type -> unknown: the daemon
|
|
387
|
+
# may still hold the paid attempt, so this is not a re-prepare signal.
|
|
388
|
+
retention_known: [true, false].include?(parsed["retryable"])
|
|
389
|
+
)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# retryable is present, and true, only on a pointer finalize that kept
|
|
393
|
+
# the paid state; anything else reads as not retained.
|
|
394
|
+
coded = error_for_code(parsed["code"], message, retryable: parsed["retryable"] == true)
|
|
395
|
+
return coded if coded
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
error_for_status(code, message)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# A chunk count from a PARTIAL_UPLOAD body: a non-negative JSON integer is
|
|
402
|
+
# taken as is; anything else (absent, null, string, float, boolean, array,
|
|
403
|
+
# object, negative) reads as 0 rather than raising or being coerced.
|
|
404
|
+
#
|
|
405
|
+
# @param value [Object] parsed JSON value
|
|
406
|
+
# @return [Integer]
|
|
407
|
+
def self.body_count(value)
|
|
408
|
+
value.is_a?(Integer) && value >= 0 ? value : 0
|
|
409
|
+
end
|
|
410
|
+
private_class_method :body_count
|
|
411
|
+
|
|
59
412
|
# Returns the appropriate error type for an HTTP status code.
|
|
60
413
|
def self.error_for_status(code, message)
|
|
61
414
|
case code
|
data/lib/antd/grpc_client.rb
CHANGED
|
@@ -17,6 +17,8 @@ require_relative "v1/chunks_services_pb"
|
|
|
17
17
|
require_relative "v1/files_services_pb"
|
|
18
18
|
require_relative "v1/upload_services_pb"
|
|
19
19
|
require_relative "v1/wallet_services_pb"
|
|
20
|
+
require_relative "v1/pointers_services_pb"
|
|
21
|
+
require "base64"
|
|
20
22
|
|
|
21
23
|
module Antd
|
|
22
24
|
DEFAULT_GRPC_TARGET = "localhost:50051"
|
|
@@ -47,6 +49,7 @@ module Antd
|
|
|
47
49
|
@file_stub = Antd::V1::FileService::Stub.new(target, :this_channel_is_insecure)
|
|
48
50
|
@upload_stub = Antd::V1::UploadService::Stub.new(target, :this_channel_is_insecure)
|
|
49
51
|
@wallet_stub = Antd::V1::WalletService::Stub.new(target, :this_channel_is_insecure)
|
|
52
|
+
@pointer_stub = Antd::V1::PointerService::Stub.new(target, :this_channel_is_insecure)
|
|
50
53
|
end
|
|
51
54
|
|
|
52
55
|
# --- Health ---
|
|
@@ -324,6 +327,16 @@ module Antd
|
|
|
324
327
|
# @param upload_id [String]
|
|
325
328
|
# @param tx_hashes [Hash<String, String>]
|
|
326
329
|
# @return [String] hex chunk address
|
|
330
|
+
# @raise [PartialUploadError] when some chunks stored and others did not
|
|
331
|
+
# (gRPC ABORTED, details prefixed +Partial upload:+). The payment
|
|
332
|
+
# persists. If +retryable+, call this method again with the same
|
|
333
|
+
# arguments to store the remainder against the same payment
|
|
334
|
+
# (antd >= 0.14.0). If +retention_known+ but not +retryable+, nothing
|
|
335
|
+
# was retained: re-prepare the same content, which skips stored chunks.
|
|
336
|
+
# If +retention_known+ is false, retention is unknown: stop, keep the
|
|
337
|
+
# upload_id and payment artefacts, and reconcile before re-preparing or
|
|
338
|
+
# paying again. See +PartialUploadError+ and
|
|
339
|
+
# docs/external-signer-flow.md section 6.
|
|
327
340
|
def finalize_chunk_upload(upload_id, tx_hashes)
|
|
328
341
|
req = Antd::V1::FinalizeChunkRequest.new(
|
|
329
342
|
upload_id: upload_id,
|
|
@@ -378,6 +391,16 @@ module Antd
|
|
|
378
391
|
# @param upload_id [String]
|
|
379
392
|
# @param tx_hashes [Hash<String, String>]
|
|
380
393
|
# @return [FinalizeUploadResult]
|
|
394
|
+
# @raise [PartialUploadError] when some chunks stored and others did not
|
|
395
|
+
# (gRPC ABORTED, details prefixed +Partial upload:+). The payment
|
|
396
|
+
# persists. If +retryable+, call this method again with the same
|
|
397
|
+
# arguments to store the remainder against the same payment
|
|
398
|
+
# (antd >= 0.14.0). If +retention_known+ but not +retryable+, nothing
|
|
399
|
+
# was retained: re-prepare the same content, which skips stored chunks.
|
|
400
|
+
# If +retention_known+ is false, retention is unknown: stop, keep the
|
|
401
|
+
# upload_id and payment artefacts, and reconcile before re-preparing or
|
|
402
|
+
# paying again. See +PartialUploadError+ and
|
|
403
|
+
# docs/external-signer-flow.md section 6.
|
|
381
404
|
def finalize_upload(upload_id, tx_hashes)
|
|
382
405
|
req = Antd::V1::FinalizeUploadRequest.new(
|
|
383
406
|
upload_id: upload_id,
|
|
@@ -399,6 +422,16 @@ module Antd
|
|
|
399
422
|
# @param winner_pool_hash [String]
|
|
400
423
|
# @param store_data_map [Boolean]
|
|
401
424
|
# @return [FinalizeUploadResult]
|
|
425
|
+
# @raise [PartialUploadError] when some chunks stored and others did not
|
|
426
|
+
# (gRPC ABORTED, details prefixed +Partial upload:+). The payment
|
|
427
|
+
# persists. If +retryable+, call this method again with the same
|
|
428
|
+
# arguments to store the remainder against the same payment
|
|
429
|
+
# (antd >= 0.14.0). If +retention_known+ but not +retryable+, nothing
|
|
430
|
+
# was retained: re-prepare the same content, which skips stored chunks.
|
|
431
|
+
# If +retention_known+ is false, retention is unknown: stop, keep the
|
|
432
|
+
# upload_id and payment artefacts, and reconcile before re-preparing or
|
|
433
|
+
# paying again. See +PartialUploadError+ and
|
|
434
|
+
# docs/external-signer-flow.md section 6.
|
|
402
435
|
def finalize_merkle_upload(upload_id, winner_pool_hash, store_data_map: false)
|
|
403
436
|
req = Antd::V1::FinalizeUploadRequest.new(
|
|
404
437
|
upload_id: upload_id,
|
|
@@ -438,8 +471,183 @@ module Antd
|
|
|
438
471
|
resp.approved
|
|
439
472
|
end
|
|
440
473
|
|
|
474
|
+
# --- Pointers ---
|
|
475
|
+
#
|
|
476
|
+
# The same methods, models and errors as the REST +Client+ (see there),
|
|
477
|
+
# over PointerService. The pointer errors share gRPC status codes with
|
|
478
|
+
# older ones, so the daemon names them in the +x-antd-error-code+
|
|
479
|
+
# trailer, which +grpc_call+ reads (see +Antd.error_for_grpc_metadata+).
|
|
480
|
+
# Unset optionals arrive as "" or an absent message and read as nil, as
|
|
481
|
+
# REST's nulls do. Every write pays: do not retry +pointer_create+,
|
|
482
|
+
# +pointer_update+, +pointer_transfer+ or +pointer_finalize+
|
|
483
|
+
# automatically.
|
|
484
|
+
|
|
485
|
+
# Mirrors +Client#pointer_keys+.
|
|
486
|
+
# @return [Array<String>]
|
|
487
|
+
def pointer_keys
|
|
488
|
+
resp = grpc_call { @pointer_stub.list_keys(Antd::V1::ListPointerKeysRequest.new) }
|
|
489
|
+
resp.keys.map(&:address)
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# Mirrors +Client#pointer_create+. Pays; do not retry automatically.
|
|
493
|
+
# @return [PointerWriteResult]
|
|
494
|
+
def pointer_create(owner, target, kind: nil)
|
|
495
|
+
req = Antd::V1::PointerWriteRequest.new(owner: owner, target: target, kind: kind.to_s)
|
|
496
|
+
pointer_write_result(grpc_call { @pointer_stub.create(req) })
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Mirrors +Client#pointer_update+. Pays; do not retry automatically.
|
|
500
|
+
# @return [PointerWriteResult]
|
|
501
|
+
def pointer_update(owner, target, kind: nil)
|
|
502
|
+
req = Antd::V1::PointerWriteRequest.new(owner: owner, target: target, kind: kind.to_s)
|
|
503
|
+
pointer_write_result(grpc_call { @pointer_stub.update(req) })
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# Mirrors +Client#pointer_transfer+. Pays; do not retry automatically.
|
|
507
|
+
# @return [PointerTransferResult]
|
|
508
|
+
def pointer_transfer(owner, recipient)
|
|
509
|
+
req = Antd::V1::PointerTransferRequest.new(owner: owner, recipient: recipient)
|
|
510
|
+
resp = grpc_call { @pointer_stub.transfer(req) }
|
|
511
|
+
PointerTransferResult.new(
|
|
512
|
+
address: resp.address,
|
|
513
|
+
state_id: resp.state_id,
|
|
514
|
+
finality: resp.finality.nil? ? nil : pointer_finality_from(resp.finality),
|
|
515
|
+
finality_error: presence(resp.finality_error)
|
|
516
|
+
)
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# Mirrors +Client#pointer_get+.
|
|
520
|
+
# @return [PointerRecord]
|
|
521
|
+
def pointer_get(address)
|
|
522
|
+
resp = grpc_call { @pointer_stub.get(Antd::V1::GetPointerRequest.new(address: address)) }
|
|
523
|
+
PointerRecord.new(
|
|
524
|
+
address: resp.address,
|
|
525
|
+
owner: resp.owner,
|
|
526
|
+
counter: Antd.parse_pointer_counter("counter", resp.counter),
|
|
527
|
+
kind: resp.kind,
|
|
528
|
+
kind_tag: resp.kind_tag,
|
|
529
|
+
target: resp.target,
|
|
530
|
+
state_id: resp.state_id,
|
|
531
|
+
is_final: resp.is_final,
|
|
532
|
+
transferred_to: presence(resp.transferred_to)
|
|
533
|
+
)
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Mirrors +Client#pointer_resolve+.
|
|
537
|
+
# @return [PointerTarget]
|
|
538
|
+
def pointer_resolve(address)
|
|
539
|
+
resp = grpc_call { @pointer_stub.resolve(Antd::V1::ResolvePointerRequest.new(address: address)) }
|
|
540
|
+
PointerTarget.new(kind: resp.kind, kind_tag: resp.kind_tag, target: resp.target)
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# Mirrors +Client#pointer_finality+.
|
|
544
|
+
# @return [PointerFinality]
|
|
545
|
+
def pointer_finality(address)
|
|
546
|
+
resp = grpc_call { @pointer_stub.finality(Antd::V1::PointerFinalityRequest.new(address: address)) }
|
|
547
|
+
pointer_finality_from(resp)
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# Mirrors +Client#pointer_controller+.
|
|
551
|
+
# @return [PointerController]
|
|
552
|
+
def pointer_controller(address)
|
|
553
|
+
resp = grpc_call { @pointer_stub.controller(Antd::V1::PointerControllerRequest.new(address: address)) }
|
|
554
|
+
PointerController.new(pointer: resp.pointer, transfers: resp.transfers.to_a, frozen: resp.frozen)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
# Mirrors +Client#pointer_prepare+.
|
|
558
|
+
# @return [PointerPrepareResult]
|
|
559
|
+
def pointer_prepare(owner, op, target: nil, kind: nil, recipient: nil, include_signed_quotes: false)
|
|
560
|
+
req = Antd::V1::PreparePointerRequest.new(
|
|
561
|
+
owner: owner,
|
|
562
|
+
op: op.to_s,
|
|
563
|
+
target: target.to_s,
|
|
564
|
+
kind: kind.to_s,
|
|
565
|
+
recipient: recipient.to_s,
|
|
566
|
+
include_signed_quotes: include_signed_quotes ? true : false
|
|
567
|
+
)
|
|
568
|
+
resp = grpc_call { @pointer_stub.prepare(req) }
|
|
569
|
+
PointerPrepareResult.new(
|
|
570
|
+
upload_id: resp.upload_id,
|
|
571
|
+
op: resp.op,
|
|
572
|
+
address: resp.address,
|
|
573
|
+
state_id: resp.state_id,
|
|
574
|
+
counter: Antd.parse_pointer_counter("counter", resp.counter),
|
|
575
|
+
payment_type: resp.payment_type,
|
|
576
|
+
payments: resp.payments.map do |p|
|
|
577
|
+
PaymentInfo.new(quote_hash: p.quote_hash, rewards_address: p.rewards_address, amount: p.amount)
|
|
578
|
+
end,
|
|
579
|
+
total_amount: resp.total_amount,
|
|
580
|
+
payment_vault_address: resp.payment_vault_address,
|
|
581
|
+
payment_token_address: resp.payment_token_address,
|
|
582
|
+
rpc_url: resp.rpc_url,
|
|
583
|
+
# Bytes on the wire; base64 here, the same form REST returns.
|
|
584
|
+
signed_quotes: resp.signed_quotes.map do |q|
|
|
585
|
+
SignedQuoteEntry.new(
|
|
586
|
+
quote_hash: q.quote_hash,
|
|
587
|
+
quote: Base64.strict_encode64(q.quote),
|
|
588
|
+
commitment_sidecar: q.commitment_sidecar.empty? ? nil : Base64.strict_encode64(q.commitment_sidecar)
|
|
589
|
+
)
|
|
590
|
+
end
|
|
591
|
+
)
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
# Mirrors +Client#pointer_finalize+. A +CloseGroupShortfallError+ with
|
|
595
|
+
# +retryable+ means the daemon kept the paid state: repeat the call with
|
|
596
|
+
# the same +upload_id+.
|
|
597
|
+
# @return [PointerFinalizeResult]
|
|
598
|
+
def pointer_finalize(upload_id, tx_hashes)
|
|
599
|
+
req = Antd::V1::FinalizePointerRequest.new(upload_id: upload_id, tx_hashes: tx_hashes)
|
|
600
|
+
resp = grpc_call { @pointer_stub.finalize(req) }
|
|
601
|
+
PointerFinalizeResult.new(
|
|
602
|
+
address: resp.address,
|
|
603
|
+
state_id: resp.state_id,
|
|
604
|
+
counter: Antd.parse_pointer_counter("counter", resp.counter),
|
|
605
|
+
finality: resp.finality.nil? ? nil : pointer_finality_from(resp.finality)
|
|
606
|
+
)
|
|
607
|
+
end
|
|
608
|
+
|
|
441
609
|
private
|
|
442
610
|
|
|
611
|
+
def pointer_write_result(resp)
|
|
612
|
+
PointerWriteResult.new(
|
|
613
|
+
address: resp.address,
|
|
614
|
+
state_id: resp.state_id,
|
|
615
|
+
counter: Antd.parse_pointer_counter("counter", resp.counter)
|
|
616
|
+
)
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
# Converts the proto finality, where an unset counter or transferred_to is
|
|
620
|
+
# "" and an unset state or majority an absent message: each reads as nil.
|
|
621
|
+
def pointer_finality_from(f)
|
|
622
|
+
PointerFinality.new(
|
|
623
|
+
address: f.address,
|
|
624
|
+
group: f.group,
|
|
625
|
+
answered: f.answered,
|
|
626
|
+
status: f.status,
|
|
627
|
+
counter: f.counter.empty? ? nil : Antd.parse_pointer_counter("finality counter", f.counter),
|
|
628
|
+
state: f.state.nil? ? nil : pointer_final_state_from(f.state),
|
|
629
|
+
states: f.states.map { |s| pointer_final_state_from(s) },
|
|
630
|
+
majority: f.majority.nil? ? nil : pointer_final_state_from(f.majority),
|
|
631
|
+
transferred_to: presence(f.transferred_to)
|
|
632
|
+
)
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def pointer_final_state_from(s)
|
|
636
|
+
PointerFinalState.new(
|
|
637
|
+
state_id: s.state_id,
|
|
638
|
+
kind: s.kind,
|
|
639
|
+
kind_tag: s.kind_tag,
|
|
640
|
+
target: s.target,
|
|
641
|
+
holders: s.holders,
|
|
642
|
+
transferred_to: presence(s.transferred_to)
|
|
643
|
+
)
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
# A proto string, or nil when unset (empty).
|
|
647
|
+
def presence(value)
|
|
648
|
+
value.nil? || value.empty? ? nil : value
|
|
649
|
+
end
|
|
650
|
+
|
|
443
651
|
# Drives a +return_op: true+ server-streaming call, yielding +DownloadFrame+s.
|
|
444
652
|
# The byte denominator is surfaced first as a leading +DownloadFrame+ (its
|
|
445
653
|
# +meta+ set), read from the response's +x-content-length+ initial metadata;
|
|
@@ -520,14 +728,31 @@ module Antd
|
|
|
520
728
|
payment_type: resp.payment_type,
|
|
521
729
|
depth: is_merkle ? resp.depth : nil,
|
|
522
730
|
pool_commitments: pool_commitments,
|
|
523
|
-
merkle_payment_timestamp: is_merkle ? resp.merkle_payment_timestamp.to_i : nil
|
|
731
|
+
merkle_payment_timestamp: is_merkle ? resp.merkle_payment_timestamp.to_i : nil,
|
|
732
|
+
# Already-stored preflight (parity with REST): the external signer
|
|
733
|
+
# pays for total_chunks - already_stored_count chunks.
|
|
734
|
+
total_chunks: resp.total_chunks,
|
|
735
|
+
already_stored_count: resp.already_stored_count
|
|
524
736
|
)
|
|
525
737
|
end
|
|
526
738
|
|
|
527
739
|
|
|
528
740
|
# Executes a gRPC call and translates errors to Antd error types.
|
|
741
|
+
#
|
|
742
|
+
# The daemon's +x-antd-error-code+ trailer comes first: where it names a
|
|
743
|
+
# code that shares its gRPC status with older errors (POINTER_FINAL,
|
|
744
|
+
# POINTER_FORKED, CONFLICT, CLOSE_GROUP_SHORTFALL) and agrees with the
|
|
745
|
+
# status code, it selects the typed error. Anything else is re-raised to
|
|
746
|
+
# the status-code mapping below.
|
|
529
747
|
def grpc_call
|
|
530
|
-
|
|
748
|
+
begin
|
|
749
|
+
yield
|
|
750
|
+
rescue GRPC::BadStatus => e
|
|
751
|
+
tagged = Antd.error_for_grpc_metadata(e.code, e.details, e.message, e.metadata)
|
|
752
|
+
raise tagged unless tagged.nil?
|
|
753
|
+
|
|
754
|
+
raise
|
|
755
|
+
end
|
|
531
756
|
rescue GRPC::InvalidArgument => e
|
|
532
757
|
raise BadRequestError, e.message
|
|
533
758
|
rescue GRPC::NotFound => e
|
|
@@ -542,6 +767,24 @@ module Antd
|
|
|
542
767
|
raise NetworkError, e.message
|
|
543
768
|
rescue GRPC::FailedPrecondition => e
|
|
544
769
|
raise PaymentError, e.message
|
|
770
|
+
rescue GRPC::Aborted => e
|
|
771
|
+
# PARTIAL_UPLOAD: some chunks stored, some still unstored after
|
|
772
|
+
# retries. The daemon's status message always opens with "Partial
|
|
773
|
+
# upload:", so only an ABORTED whose details *start* with it becomes
|
|
774
|
+
# the typed error. Any other ABORTED -- including one that merely
|
|
775
|
+
# quotes the marker further in -- keeps the generic mapping instead of
|
|
776
|
+
# masquerading as a partial upload with zero counts. Gate and parse on
|
|
777
|
+
# +e.details+ (the message as the daemon sent it), not +e.message+,
|
|
778
|
+
# which grpc-ruby decorates as "10:<details>". The counts and the
|
|
779
|
+
# daemon's closing retention hint ride that text over gRPC (no
|
|
780
|
+
# structured detail yet), so parse them best-effort to match the REST
|
|
781
|
+
# client's typed error: retention is known only when the counts parse
|
|
782
|
+
# and the text ends with one of the daemon's two hints (see
|
|
783
|
+
# Antd.parse_partial_upload_message). The raised message stays
|
|
784
|
+
# +e.message+, like every other branch.
|
|
785
|
+
raise AntdError.new(e.message, status_code: e.code) unless Antd.partial_upload_message?(e.details)
|
|
786
|
+
|
|
787
|
+
raise PartialUploadError.new(e.message, **Antd.parse_partial_upload_message(e.details))
|
|
545
788
|
rescue GRPC::BadStatus => e
|
|
546
789
|
raise AntdError.new(e.message, status_code: e.code)
|
|
547
790
|
end
|