antd 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 44a5ae69094c720b34c8d440aa9072ab3d83e192fc790351c9d08ab8d4ef6f8f
4
- data.tar.gz: '0329577dd7ac059a32b8f6bd47369e0452d3f13679f8c37e5da245359772b339'
3
+ metadata.gz: ab6786553e76394691c28a2968384dd279738e876ecf961e74a0b7b46904cc0e
4
+ data.tar.gz: a04836fdba38a5457a3cfda27c3adde84bc8175a87dcd1c58e719fb315cc1f0a
5
5
  SHA512:
6
- metadata.gz: d36cc92042a95ea03a429f5a706fe208d5b4c9c9ff030bcaf7fe72014e7e50feb15a0dd8b26426183de8581d9a90357bdc326dac959ca7ca590a3efaf5726914
7
- data.tar.gz: ec4d4aadcc549fde1a6214030934aedefe9ca948dc4c72177bfa82d237c85819eb4edf0890e8940444d63113208760fa222850ce423a9d9a77e1e5fd15a06d0b
6
+ metadata.gz: '0806f4881413dd24b52a385319ee23ca30de479dea639b54ef0088bd31f8f0cdba73c0d458843082f697e6a92a3bb4c1f5c34dd77532d38d8032dcb4845f3c78'
7
+ data.tar.gz: 12fffe89716557c9dc2716a99f5f8b39a4958251387050f8828f45ff69628e2350e5aa6dc370f55a89d3abfc06fbb214770bf9cd1c31e516e794fc09daaf9fd4
data/README.md CHANGED
@@ -87,7 +87,14 @@ puts "Retrieved: #{data}"
87
87
  ```
88
88
 
89
89
  The `GrpcClient` raises the same `Antd::AntdError` hierarchy as the REST
90
- client, translating gRPC status codes to the appropriate error subclass.
90
+ client, translating gRPC status codes to the appropriate error subclass
91
+ (an `ABORTED` whose status details — `GRPC::BadStatus#details`, the text
92
+ the daemon sent — start with `Partial upload:` becomes
93
+ `Antd::PartialUploadError`, with the chunk counts, `retryable` and
94
+ `retention_known` parsed from that text — see
95
+ [Partial uploads](#partial-uploads); any other
96
+ `ABORTED`, including one that only mentions `Partial upload:` later in its
97
+ text, stays a generic `Antd::AntdError`).
91
98
 
92
99
  > **Note:** Wallet operations (address, balance, approve) and payment_mode are available via REST only.
93
100
 
@@ -172,6 +179,82 @@ end
172
179
  | `TooLargeError` | 413 | Payload too large |
173
180
  | `InternalError` | 500 | Server error |
174
181
  | `NetworkError` | 502 | Network unreachable |
182
+ | `PartialUploadError` | 502 (`code: "PARTIAL_UPLOAD"`) | Finalize stored some chunks but not all — subclass of `NetworkError` |
183
+
184
+ ### Partial uploads
185
+
186
+ A `finalize_upload` / `finalize_merkle_upload` / `finalize_chunk_upload` call
187
+ can fail *after* the external signer has paid: some chunks store, others miss
188
+ quorum after the daemon's own retries. The daemon reports this as HTTP `502`
189
+ with `code: "PARTIAL_UPLOAD"` (gRPC `ABORTED`), and the SDK raises
190
+ `Antd::PartialUploadError` carrying `chunks_stored`, `chunks_failed`,
191
+ `total_chunks`, `retryable` and `retention_known`. The on-chain payment
192
+ persists and the stored chunks stay on the network; what to do next depends
193
+ on what the daemon said about the paid attempt:
194
+
195
+ - **`retryable`** (antd >= 0.14.0; implies `retention_known`) — the daemon
196
+ kept the paid attempt under the same `upload_id`. Call the **same finalize
197
+ method again with the same `upload_id` and payment artefacts** to store the
198
+ remainder against the same payment: no re-prepare, no second signature, no
199
+ double payment. Bound the loop — a persistent failure raises this error on
200
+ every call — so cap the attempts and treat a `chunks_failed` that stops
201
+ shrinking as stuck. The retained attempt expires with the daemon's
202
+ pending-upload TTL.
203
+ - **`retention_known && !retryable`** — the daemon confirmed nothing was
204
+ retained (a merkle finalize with deliberately unpaid batches). Re-prepare
205
+ the same content: already-stored chunks are skipped, so the retry pays only
206
+ for the remainder.
207
+ - **`!retention_known`** — retention is unknown: the error did not say, in a
208
+ form the SDK could read, whether the paid attempt was kept. The daemon may
209
+ still hold it (it records the resume handle before it returns the error).
210
+ Stop automatic recovery, keep the `upload_id` and the original payment
211
+ artefacts (tx hashes, or the merkle winner pool hash), and reconcile before
212
+ re-preparing or paying again. **Never pay again on this signal alone.**
213
+ Daemons older than 0.14.0 never send `retryable`, so their REST partial
214
+ uploads read as unknown.
215
+
216
+ Over REST, `retention_known` is true only when the body's `retryable` is a
217
+ JSON boolean (`true` or `false`); absent, `null` or any other type reads as
218
+ unknown. Over gRPC there is no structured body, so the counts and flags are
219
+ parsed from the status text,
220
+ `Partial upload: <stored>/<total> chunks stored, <failed> failed after retries: <reason> (<hint>)`.
221
+ `retention_known` is true only when the text starts with that counts
222
+ pattern, all three counts parse (each within the daemon's u64 range), and the
223
+ text ends with one of the daemon's two hints: `(paid attempt retained...)`
224
+ sets `retryable`, and `(stored chunks persist; re-prepare the same content...)`
225
+ means the daemon confirmed nothing was retained (daemons older than 0.14.0
226
+ write only this one). If the counts cannot be read, all three are `0` and
227
+ both flags are `false`. Readable counts with a missing, truncated or
228
+ unrecognised hint keep the counts, but both flags stay `false`: retention is
229
+ unknown, so stop and reconcile rather than re-prepare.
230
+
231
+ `PartialUploadError` subclasses `NetworkError` (the 502 mapping), so existing
232
+ `rescue Antd::NetworkError` blocks keep catching it; rescue the subclass first
233
+ to handle it specifically.
234
+
235
+ ```ruby
236
+ MAX_ATTEMPTS = 5
237
+ last_failed = nil
238
+ attempt = 0
239
+ begin
240
+ attempt += 1
241
+ result = client.finalize_upload(prep.upload_id, tx_hashes)
242
+ rescue Antd::PartialUploadError => e
243
+ # Not resumable. With retention_known, nothing was retained: re-prepare.
244
+ # Without it, retention is unknown: stop, keep upload_id + tx_hashes and
245
+ # reconcile before re-preparing or paying again.
246
+ raise unless e.retryable
247
+ stuck = !last_failed.nil? && e.chunks_failed >= last_failed
248
+ raise if attempt >= MAX_ATTEMPTS || stuck # bounded: same payment, same upload_id
249
+ last_failed = e.chunks_failed
250
+ sleep(2 * attempt)
251
+ retry
252
+ end
253
+ ```
254
+
255
+ See [`docs/external-signer-flow.md` section 6](../docs/external-signer-flow.md#6-retry-a-partial-store--same-upload_id-same-payment)
256
+ for the daemon contract and `examples/07_external_signer.rb` for a
257
+ `finalize_with_retry` helper.
175
258
 
176
259
  ## Examples
177
260
 
@@ -182,3 +265,4 @@ See the [examples/](examples/) directory:
182
265
  - `03_chunks.rb` — Chunk put/get
183
266
  - `04_files.rb` — File upload and download
184
267
  - `06_private_data.rb` — Private data put/get
268
+ - `07_external_signer.rb` — External-signer prepare/pay/finalize with a bounded partial-upload retry
data/lib/antd/client.rb CHANGED
@@ -217,6 +217,16 @@ module Antd
217
217
  # @param tx_hashes [Hash<String, String>] map of quote_hash to tx_hash
218
218
  # @return [String] network address of the stored chunk
219
219
  # (matches +PrepareChunkResult#address+)
220
+ # @raise [PartialUploadError] when some chunks stored and others did not
221
+ # (HTTP 502, +code: "PARTIAL_UPLOAD"+). The payment persists. If
222
+ # +retryable+, call this method again with the same arguments to store
223
+ # the remainder against the same payment (antd >= 0.14.0). If
224
+ # +retention_known+ but not +retryable+, nothing was retained:
225
+ # re-prepare the same content, which skips stored chunks. If
226
+ # +retention_known+ is false, retention is unknown: stop, keep the
227
+ # upload_id and payment artefacts, and reconcile before re-preparing
228
+ # or paying again. See +PartialUploadError+ and
229
+ # docs/external-signer-flow.md section 6.
220
230
  def finalize_chunk_upload(upload_id, tx_hashes)
221
231
  j = do_json(:post, "/v1/chunks/finalize", {
222
232
  upload_id: upload_id,
@@ -365,6 +375,16 @@ module Antd
365
375
  # @param upload_id [String] the upload ID from prepare_upload
366
376
  # @param tx_hashes [Hash<String, String>] map of quote_hash to tx_hash
367
377
  # @return [FinalizeUploadResult]
378
+ # @raise [PartialUploadError] when some chunks stored and others did not
379
+ # (HTTP 502, +code: "PARTIAL_UPLOAD"+). The payment persists. If
380
+ # +retryable+, call this method again with the same arguments to store
381
+ # the remainder against the same payment (antd >= 0.14.0). If
382
+ # +retention_known+ but not +retryable+, nothing was retained:
383
+ # re-prepare the same content, which skips stored chunks. If
384
+ # +retention_known+ is false, retention is unknown: stop, keep the
385
+ # upload_id and payment artefacts, and reconcile before re-preparing
386
+ # or paying again. See +PartialUploadError+ and
387
+ # docs/external-signer-flow.md section 6.
368
388
  def finalize_upload(upload_id, tx_hashes)
369
389
  j = do_json(:post, "/v1/upload/finalize", {
370
390
  upload_id: upload_id,
@@ -378,6 +398,16 @@ module Antd
378
398
  # @param winner_pool_hash [String] hash of the winning pool commitment
379
399
  # @param store_data_map [Boolean] whether to store the data map on-network
380
400
  # @return [FinalizeUploadResult]
401
+ # @raise [PartialUploadError] when some chunks stored and others did not
402
+ # (HTTP 502, +code: "PARTIAL_UPLOAD"+). The payment persists. If
403
+ # +retryable+, call this method again with the same arguments to store
404
+ # the remainder against the same payment (antd >= 0.14.0). If
405
+ # +retention_known+ but not +retryable+, nothing was retained:
406
+ # re-prepare the same content, which skips stored chunks. If
407
+ # +retention_known+ is false, retention is unknown: stop, keep the
408
+ # upload_id and payment artefacts, and reconcile before re-preparing
409
+ # or paying again. See +PartialUploadError+ and
410
+ # docs/external-signer-flow.md section 6.
381
411
  def finalize_merkle_upload(upload_id, winner_pool_hash, store_data_map: false)
382
412
  j = do_json(:post, "/v1/upload/finalize", {
383
413
  upload_id: upload_id,
@@ -483,6 +513,10 @@ module Antd
483
513
  end
484
514
 
485
515
  # Perform a JSON HTTP request and return the parsed response body.
516
+ #
517
+ # A non-2xx response is raised via +Antd.error_for_response+, which maps
518
+ # the body's +code+ / +error+ (and the PARTIAL_UPLOAD counts) onto the
519
+ # typed +AntdError+ hierarchy.
486
520
  def do_json(method, path, body = nil)
487
521
  uri = build_uri(path)
488
522
  http = Net::HTTP.new(uri.host, uri.port)
@@ -505,14 +539,7 @@ module Antd
505
539
  code = response.code.to_i
506
540
 
507
541
  unless (200...300).cover?(code)
508
- msg = response.body.to_s
509
- begin
510
- parsed = JSON.parse(msg)
511
- msg = parsed["error"] if parsed["error"]
512
- rescue JSON::ParserError
513
- # use raw body as message
514
- end
515
- raise Antd.error_for_status(code, msg)
542
+ raise Antd.error_for_response(code, response.body)
516
543
  end
517
544
 
518
545
  return {} if response.body.nil? || response.body.empty?
@@ -524,9 +551,9 @@ module Antd
524
551
  # they arrive. Reuses the same base-URL / timeout / error-mapping plumbing
525
552
  # as +do_json+, but never buffers the success body in memory.
526
553
  #
527
- # On a non-2xx response the (short) body is read fully, parsed for an
528
- # +{"error":...}+ field, and raised via +Antd.error_for_status+ — mirroring
529
- # +do_json+. On 2xx, chunks are streamed straight to the block.
554
+ # On a non-2xx response the (short) body is read fully and raised via
555
+ # +Antd.error_for_response+ — mirroring +do_json+. On 2xx, chunks are
556
+ # streamed straight to the block.
530
557
  def do_stream(method, path, body = nil)
531
558
  uri = build_uri(path)
532
559
  http = Net::HTTP.new(uri.host, uri.port)
@@ -550,14 +577,7 @@ module Antd
550
577
  code = response.code.to_i
551
578
 
552
579
  unless (200...300).cover?(code)
553
- msg = response.body.to_s
554
- begin
555
- parsed = JSON.parse(msg)
556
- msg = parsed["error"] if parsed["error"]
557
- rescue JSON::ParserError
558
- # use raw body as message
559
- end
560
- raise Antd.error_for_status(code, msg)
580
+ raise Antd.error_for_response(code, response.body)
561
581
  end
562
582
 
563
583
  response.read_body do |chunk|
@@ -599,14 +619,7 @@ module Antd
599
619
  code = response.code.to_i
600
620
 
601
621
  unless (200...300).cover?(code)
602
- msg = response.body.to_s
603
- begin
604
- parsed = JSON.parse(msg)
605
- msg = parsed["error"] if parsed["error"]
606
- rescue JSON::ParserError
607
- # use raw body as message
608
- end
609
- raise Antd.error_for_status(code, msg)
622
+ raise Antd.error_for_response(code, response.body)
610
623
  end
611
624
 
612
625
  buffer = +""
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,228 @@ module Antd
56
58
  def initialize(message) = super(message, status_code: 503)
57
59
  end
58
60
 
61
+ # A finalize stored some chunks while others remained unstored after the
62
+ # daemon's retries (HTTP 502 with +code: "PARTIAL_UPLOAD"+; gRPC ABORTED).
63
+ # The on-chain payment persists and the stored chunks stay on the network.
64
+ # How to finish the upload depends on +retryable+ and +retention_known+:
65
+ #
66
+ # - +retryable+ (antd >= 0.14.0; implies +retention_known+): the daemon kept
67
+ # the paid attempt (payment proofs + unstored chunks) under the same
68
+ # +upload_id+. Call the same +finalize_*+ method again with the same
69
+ # +upload_id+ and payment artefacts to store the remainder against the
70
+ # same payment -- no re-prepare, no second signature, no double payment.
71
+ # Bound the loop: a persistent failure raises this error on every call, so
72
+ # cap the attempts and treat a +chunks_failed+ that stops shrinking as
73
+ # stuck. The retained attempt expires with the daemon's pending-upload
74
+ # TTL.
75
+ # - +retention_known+ and not +retryable+: the daemon confirmed nothing was
76
+ # retained (a merkle finalize with deliberately unpaid batches).
77
+ # Re-preparing the same content skips already-stored chunks, so a retry
78
+ # pays only for the missing remainder.
79
+ # - not +retention_known+: retention is unknown. The error did not say, in
80
+ # a form this SDK could read, whether the paid attempt was kept, and the
81
+ # daemon may still hold it: it records the resume handle before it
82
+ # returns the error. Stop automatic recovery, keep the +upload_id+ and the
83
+ # original payment artefacts (tx hashes, or the merkle winner pool hash),
84
+ # and reconcile before re-preparing or paying again. Never pay again on
85
+ # this signal alone. Daemons older than 0.14.0 never send +retryable+, so
86
+ # their REST partial uploads read as unknown.
87
+ #
88
+ # Over REST the counts and flags come from the structured error body; a
89
+ # count of the wrong JSON type reads as 0, and +retention_known+ is true
90
+ # only when the body's +retryable+ is a JSON boolean. Over gRPC they are
91
+ # parsed best-effort from the status details ("Partial upload: S/T chunks
92
+ # stored, F failed after retries: <reason> (<hint>)"); only an ABORTED
93
+ # whose details start with "Partial upload:" is a partial upload.
94
+ # +retention_known+ is true there only when the details start with the
95
+ # counts, all three parse, and the details end with one of the daemon's two
96
+ # hints: "(paid attempt retained...)" sets +retryable+, and "(stored chunks
97
+ # persist; re-prepare the same content...)" means the daemon confirmed
98
+ # nothing was retained (daemons older than 0.14.0 write only this one).
99
+ # Counts that do not parse leave all three zero and both flags false.
100
+ # Readable counts with a missing, truncated or unrecognised hint keep the
101
+ # counts, but both flags stay false: retention unknown, not "nothing
102
+ # retained".
103
+ #
104
+ # Subclasses +NetworkError+ because the daemon reports it as a 502: existing
105
+ # +rescue Antd::NetworkError+ blocks keep catching it, and +status_code+ is
106
+ # 502 on both transports. Rescue +PartialUploadError+ first to handle it
107
+ # specifically. See docs/external-signer-flow.md section 6.
108
+ class PartialUploadError < NetworkError
109
+ attr_reader :chunks_stored, :chunks_failed, :total_chunks, :retryable, :retention_known
110
+
111
+ # @param message [String]
112
+ # @param chunks_stored [Integer] chunks confirmed stored on the network
113
+ # @param chunks_failed [Integer] chunks still unstored after retries
114
+ # @param total_chunks [Integer] chunks in the upload
115
+ # @param retryable [Boolean] whether the daemon retained the paid attempt
116
+ # @param retention_known [Boolean] whether the daemon said, in a form this
117
+ # SDK could read, whether it retained the paid attempt. +retryable+
118
+ # implies it: +retryable: true+ always yields +retention_known == true+.
119
+ def initialize(message, chunks_stored: 0, chunks_failed: 0, total_chunks: 0, retryable: false,
120
+ retention_known: false)
121
+ @chunks_stored = chunks_stored
122
+ @chunks_failed = chunks_failed
123
+ @total_chunks = total_chunks
124
+ @retryable = retryable
125
+ @retention_known = (retention_known || retryable) ? true : false
126
+ super(message)
127
+ end
128
+
129
+ alias retryable? retryable
130
+ alias retention_known? retention_known
131
+ end
132
+
133
+ # Fixed text every PARTIAL_UPLOAD message from the daemon opens with. Over
134
+ # gRPC the status carries no structured code, so an ABORTED status is a
135
+ # partial upload only when its details start with this prefix.
136
+ PARTIAL_UPLOAD_PREFIX = "Partial upload:"
137
+
138
+ # Fixed prefix of the daemon's PARTIAL_UPLOAD message:
139
+ # "Partial upload: <stored>/<total> chunks stored, <failed> failed".
140
+ # Anchored with +\A+ (start of input; +^+ would also match after a
141
+ # newline), so counts quoted later in a garbled message are never read.
142
+ PARTIAL_UPLOAD_COUNTS = %r{\APartial upload: (\d+)/(\d+) chunks stored, (\d+) failed}
143
+
144
+ # The daemon closes every PARTIAL_UPLOAD message with one of two
145
+ # parenthesised hints (partial_upload_hint in antd/src/error.rs). This one
146
+ # opens the hint when it kept the paid attempt for a same-upload_id retry.
147
+ PARTIAL_UPLOAD_RETAINED_HINT = "paid attempt retained"
148
+
149
+ # This one opens the hint when it did not. Daemons older than 0.14.0 write
150
+ # only this one.
151
+ PARTIAL_UPLOAD_NOT_RETAINED_HINT = "stored chunks persist; re-prepare the same content"
152
+ private_constant :PARTIAL_UPLOAD_NOT_RETAINED_HINT
153
+
154
+ # The hint that closes the message: "(<hint>...)" at the very end. +\z+ is
155
+ # the end of the input; Ruby's +$+ is the end of a LINE and +\Z+ allows a
156
+ # trailing newline. A hint quoted inside the failure reason, a truncated
157
+ # or unclosed tail, or any text after the hint does not match.
158
+ PARTIAL_UPLOAD_RETENTION_TAIL =
159
+ /\((#{Regexp.union(PARTIAL_UPLOAD_RETAINED_HINT, PARTIAL_UPLOAD_NOT_RETAINED_HINT).source})[^()]*\)\z/
160
+ private_constant :PARTIAL_UPLOAD_RETENTION_TAIL
161
+
162
+ # Largest count the daemon can send (its counts are u64). Ruby integers are
163
+ # unbounded, so a larger digit run is treated as a failed conversion.
164
+ PARTIAL_UPLOAD_COUNT_MAX = 18_446_744_073_709_551_615
165
+ private_constant :PARTIAL_UPLOAD_COUNT_MAX
166
+
167
+ # Whether a gRPC status's details are the daemon's PARTIAL_UPLOAD message.
168
+ # Anchored: the details must start with +PARTIAL_UPLOAD_PREFIX+. A status
169
+ # that merely quotes "Partial upload:" further into its text (an upstream
170
+ # error wrapping one, say) is not a partial upload and must not select the
171
+ # paid-attempt recovery path with zero counts.
172
+ #
173
+ # Pass +GRPC::BadStatus#details+ -- the status message exactly as the
174
+ # daemon sent it -- not +#message+, which grpc-ruby decorates as
175
+ # "<code>:<details>" ("10:Partial upload: ...") and so never starts with
176
+ # the prefix.
177
+ #
178
+ # @param details [String, nil] gRPC status details
179
+ # @return [Boolean]
180
+ def self.partial_upload_message?(details)
181
+ details.to_s.start_with?(PARTIAL_UPLOAD_PREFIX)
182
+ end
183
+
184
+ # Recovers the chunk counts and the retention flags from a PARTIAL_UPLOAD
185
+ # message (over gRPC, the status details). Used for gRPC, where the status
186
+ # carries no structured detail; REST callers get the body fields instead.
187
+ #
188
+ # Conservative: +retention_known+ is true only when the message starts
189
+ # with the counts pattern ("Partial upload: <stored>/<total> chunks
190
+ # stored, <failed> failed"), all three counts converted (each at most
191
+ # u64::MAX, the daemon's count type), and the message ends with one of the
192
+ # daemon's two hints, "(paid attempt retained...)" or "(stored chunks
193
+ # persist; re-prepare the same content...)"; +retryable+ is then true only
194
+ # for the first. On a prefix or pattern miss, or a count out of range, all
195
+ # three counts are 0 and both flags are false. Readable counts with a
196
+ # missing, truncated or unrecognised tail, or text after it, keep the
197
+ # counts but leave both flags false: the daemon's answer on retention was
198
+ # not read, so retention is unknown, never "nothing retained". A message
199
+ # the parser cannot fully read never selects the same-upload_id retry, and
200
+ # never reads as confirmed non-retention.
201
+ #
202
+ # @param message [String]
203
+ # @return [Hash] +:chunks_stored+, +:chunks_failed+, +:total_chunks+,
204
+ # +:retryable+, +:retention_known+ -- splat into +PartialUploadError.new+
205
+ def self.parse_partial_upload_message(message)
206
+ text = message.to_s
207
+ m = partial_upload_message?(text) && PARTIAL_UPLOAD_COUNTS.match(text)
208
+ # The groups are ASCII digit runs, so #to_i is exact; only the range can fail.
209
+ stored, total, failed = m && [m[1], m[2], m[3]].map(&:to_i)
210
+ unless m && [stored, total, failed].all? { |n| n <= PARTIAL_UPLOAD_COUNT_MAX }
211
+ return { chunks_stored: 0, chunks_failed: 0, total_chunks: 0, retryable: false, retention_known: false }
212
+ end
213
+
214
+ # Only the hint that closes the message is the daemon's answer.
215
+ tail = PARTIAL_UPLOAD_RETENTION_TAIL.match(text)
216
+ {
217
+ chunks_stored: stored,
218
+ chunks_failed: failed,
219
+ total_chunks: total,
220
+ retryable: !tail.nil? && tail[1] == PARTIAL_UPLOAD_RETAINED_HINT,
221
+ retention_known: !tail.nil?
222
+ }
223
+ end
224
+
225
+ # Maps a non-2xx REST response onto a typed error, preferring the body's
226
+ # machine-readable +code+ over the bare HTTP status where they diverge:
227
+ # +PARTIAL_UPLOAD+ arrives as a 502 that would otherwise read as a generic
228
+ # +NetworkError+. Every other code keeps the status-based mapping of
229
+ # +error_for_status+. A JSON object body's +error+ field becomes the
230
+ # message when it is a string; otherwise (non-JSON body, a JSON value that
231
+ # is not an object, a non-string +error+) the raw body is the message.
232
+ #
233
+ # Never raises on a malformed body: every field is type-checked, not
234
+ # coerced. Only a +code+ that is exactly the string "PARTIAL_UPLOAD"
235
+ # selects +PartialUploadError+; a count that is not a non-negative JSON
236
+ # integer reads as 0; +retryable+ is true only for JSON +true+; and
237
+ # +retention_known+ is true only when +retryable+ is a JSON boolean (absent
238
+ # on daemons < 0.14.0, +null+ or any other type reads as unknown).
239
+ #
240
+ # @param code [Integer] HTTP status
241
+ # @param body [String, nil] raw response body
242
+ # @return [AntdError]
243
+ def self.error_for_response(code, body)
244
+ message = body.to_s
245
+ parsed = begin
246
+ JSON.parse(message)
247
+ rescue JSON::ParserError
248
+ nil # use raw body as message
249
+ end
250
+ parsed = nil unless parsed.is_a?(Hash)
251
+
252
+ if parsed
253
+ message = parsed["error"] if parsed["error"].is_a?(String)
254
+ if parsed["code"] == "PARTIAL_UPLOAD"
255
+ return PartialUploadError.new(
256
+ message,
257
+ chunks_stored: body_count(parsed["chunks_stored"]),
258
+ chunks_failed: body_count(parsed["chunks_failed"]),
259
+ total_chunks: body_count(parsed["total_chunks"]),
260
+ retryable: parsed["retryable"] == true,
261
+ # Known only when the daemon sent the flag as a JSON boolean. Absent
262
+ # (daemons < 0.14.0), null or any other type -> unknown: the daemon
263
+ # may still hold the paid attempt, so this is not a re-prepare signal.
264
+ retention_known: [true, false].include?(parsed["retryable"])
265
+ )
266
+ end
267
+ end
268
+
269
+ error_for_status(code, message)
270
+ end
271
+
272
+ # A chunk count from a PARTIAL_UPLOAD body: a non-negative JSON integer is
273
+ # taken as is; anything else (absent, null, string, float, boolean, array,
274
+ # object, negative) reads as 0 rather than raising or being coerced.
275
+ #
276
+ # @param value [Object] parsed JSON value
277
+ # @return [Integer]
278
+ def self.body_count(value)
279
+ value.is_a?(Integer) && value >= 0 ? value : 0
280
+ end
281
+ private_class_method :body_count
282
+
59
283
  # Returns the appropriate error type for an HTTP status code.
60
284
  def self.error_for_status(code, message)
61
285
  case code
@@ -324,6 +324,16 @@ module Antd
324
324
  # @param upload_id [String]
325
325
  # @param tx_hashes [Hash<String, String>]
326
326
  # @return [String] hex chunk address
327
+ # @raise [PartialUploadError] when some chunks stored and others did not
328
+ # (gRPC ABORTED, details prefixed +Partial upload:+). The payment
329
+ # persists. If +retryable+, call this method again with the same
330
+ # arguments to store the remainder against the same payment
331
+ # (antd >= 0.14.0). If +retention_known+ but not +retryable+, nothing
332
+ # was retained: re-prepare the same content, which skips stored chunks.
333
+ # If +retention_known+ is false, retention is unknown: stop, keep the
334
+ # upload_id and payment artefacts, and reconcile before re-preparing or
335
+ # paying again. See +PartialUploadError+ and
336
+ # docs/external-signer-flow.md section 6.
327
337
  def finalize_chunk_upload(upload_id, tx_hashes)
328
338
  req = Antd::V1::FinalizeChunkRequest.new(
329
339
  upload_id: upload_id,
@@ -378,6 +388,16 @@ module Antd
378
388
  # @param upload_id [String]
379
389
  # @param tx_hashes [Hash<String, String>]
380
390
  # @return [FinalizeUploadResult]
391
+ # @raise [PartialUploadError] when some chunks stored and others did not
392
+ # (gRPC ABORTED, details prefixed +Partial upload:+). The payment
393
+ # persists. If +retryable+, call this method again with the same
394
+ # arguments to store the remainder against the same payment
395
+ # (antd >= 0.14.0). If +retention_known+ but not +retryable+, nothing
396
+ # was retained: re-prepare the same content, which skips stored chunks.
397
+ # If +retention_known+ is false, retention is unknown: stop, keep the
398
+ # upload_id and payment artefacts, and reconcile before re-preparing or
399
+ # paying again. See +PartialUploadError+ and
400
+ # docs/external-signer-flow.md section 6.
381
401
  def finalize_upload(upload_id, tx_hashes)
382
402
  req = Antd::V1::FinalizeUploadRequest.new(
383
403
  upload_id: upload_id,
@@ -399,6 +419,16 @@ module Antd
399
419
  # @param winner_pool_hash [String]
400
420
  # @param store_data_map [Boolean]
401
421
  # @return [FinalizeUploadResult]
422
+ # @raise [PartialUploadError] when some chunks stored and others did not
423
+ # (gRPC ABORTED, details prefixed +Partial upload:+). The payment
424
+ # persists. If +retryable+, call this method again with the same
425
+ # arguments to store the remainder against the same payment
426
+ # (antd >= 0.14.0). If +retention_known+ but not +retryable+, nothing
427
+ # was retained: re-prepare the same content, which skips stored chunks.
428
+ # If +retention_known+ is false, retention is unknown: stop, keep the
429
+ # upload_id and payment artefacts, and reconcile before re-preparing or
430
+ # paying again. See +PartialUploadError+ and
431
+ # docs/external-signer-flow.md section 6.
402
432
  def finalize_merkle_upload(upload_id, winner_pool_hash, store_data_map: false)
403
433
  req = Antd::V1::FinalizeUploadRequest.new(
404
434
  upload_id: upload_id,
@@ -520,7 +550,11 @@ module Antd
520
550
  payment_type: resp.payment_type,
521
551
  depth: is_merkle ? resp.depth : nil,
522
552
  pool_commitments: pool_commitments,
523
- merkle_payment_timestamp: is_merkle ? resp.merkle_payment_timestamp.to_i : nil
553
+ merkle_payment_timestamp: is_merkle ? resp.merkle_payment_timestamp.to_i : nil,
554
+ # Already-stored preflight (parity with REST): the external signer
555
+ # pays for total_chunks - already_stored_count chunks.
556
+ total_chunks: resp.total_chunks,
557
+ already_stored_count: resp.already_stored_count
524
558
  )
525
559
  end
526
560
 
@@ -542,6 +576,24 @@ module Antd
542
576
  raise NetworkError, e.message
543
577
  rescue GRPC::FailedPrecondition => e
544
578
  raise PaymentError, e.message
579
+ rescue GRPC::Aborted => e
580
+ # PARTIAL_UPLOAD: some chunks stored, some still unstored after
581
+ # retries. The daemon's status message always opens with "Partial
582
+ # upload:", so only an ABORTED whose details *start* with it becomes
583
+ # the typed error. Any other ABORTED -- including one that merely
584
+ # quotes the marker further in -- keeps the generic mapping instead of
585
+ # masquerading as a partial upload with zero counts. Gate and parse on
586
+ # +e.details+ (the message as the daemon sent it), not +e.message+,
587
+ # which grpc-ruby decorates as "10:<details>". The counts and the
588
+ # daemon's closing retention hint ride that text over gRPC (no
589
+ # structured detail yet), so parse them best-effort to match the REST
590
+ # client's typed error: retention is known only when the counts parse
591
+ # and the text ends with one of the daemon's two hints (see
592
+ # Antd.parse_partial_upload_message). The raised message stays
593
+ # +e.message+, like every other branch.
594
+ raise AntdError.new(e.message, status_code: e.code) unless Antd.partial_upload_message?(e.details)
595
+
596
+ raise PartialUploadError.new(e.message, **Antd.parse_partial_upload_message(e.details))
545
597
  rescue GRPC::BadStatus => e
546
598
  raise AntdError.new(e.message, status_code: e.code)
547
599
  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
@@ -5,7 +5,7 @@
5
5
  require 'google/protobuf'
6
6
 
7
7
 
8
- descriptor_data = "\n\x14\x61ntd/v1/health.proto\x12\x07\x61ntd.v1\"\x14\n\x12HealthCheckRequest\"\xc8\x01\n\x13HealthCheckResponse\x12\x0e\n\x06status\x18\x01 \x01(\t\x12\x0f\n\x07network\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\t\x12\x13\n\x0b\x65vm_network\x18\x04 \x01(\t\x12\x16\n\x0euptime_seconds\x18\x05 \x01(\x04\x12\x14\n\x0c\x62uild_commit\x18\x06 \x01(\t\x12\x1d\n\x15payment_token_address\x18\x07 \x01(\t\x12\x1d\n\x15payment_vault_address\x18\x08 \x01(\t2S\n\rHealthService\x12\x42\n\x05\x43heck\x12\x1b.antd.v1.HealthCheckRequest\x1a\x1c.antd.v1.HealthCheckResponseBDZ8github.com/WithAutonomi/ant-sdk/antd-go/proto/antd/v1;v1\xaa\x02\x07\x41ntd.V1b\x06proto3"
8
+ descriptor_data = "\n\x14\x61ntd/v1/health.proto\x12\x07\x61ntd.v1\"\x14\n\x12HealthCheckRequest\"\xf1\x02\n\x13HealthCheckResponse\x12\x0e\n\x06status\x18\x01 \x01(\t\x12\x0f\n\x07network\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\t\x12\x13\n\x0b\x65vm_network\x18\x04 \x01(\t\x12\x16\n\x0euptime_seconds\x18\x05 \x01(\x04\x12\x14\n\x0c\x62uild_commit\x18\x06 \x01(\t\x12\x1d\n\x15payment_token_address\x18\x07 \x01(\t\x12\x1d\n\x15payment_vault_address\x18\x08 \x01(\t\x12\x13\n\x0bwrite_ready\x18\t \x01(\x08\x12\x17\n\x0f\x63onnected_peers\x18\n \x01(\r\x12\x1a\n\x12routing_table_size\x18\x0b \x01(\r\x12\x1d\n\x15rebootstrap_threshold\x18\x0c \x01(\r\x12#\n\x16last_store_ok_secs_ago\x18\r \x01(\x04H\x00\x88\x01\x01\x42\x19\n\x17_last_store_ok_secs_ago2S\n\rHealthService\x12\x42\n\x05\x43heck\x12\x1b.antd.v1.HealthCheckRequest\x1a\x1c.antd.v1.HealthCheckResponseBDZ8github.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)
@@ -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/upload.proto\x12\x07\x61ntd.v1\x1a\x14\x61ntd/v1/common.proto\"<\n\x18PrepareFileUploadRequest\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x12\n\nvisibility\x18\x02 \x01(\t\"<\n\x18PrepareDataUploadRequest\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x12\n\nvisibility\x18\x02 \x01(\t\"\xb7\x02\n\x15PrepareUploadResponse\x12\x11\n\tupload_id\x18\x01 \x01(\t\x12\x14\n\x0cpayment_type\x18\x02 \x01(\t\x12\'\n\x08payments\x18\x03 \x03(\x0b\x32\x15.antd.v1.PaymentEntry\x12\r\n\x05\x64\x65pth\x18\x04 \x01(\r\x12\x36\n\x10pool_commitments\x18\x05 \x03(\x0b\x32\x1c.antd.v1.PoolCommitmentEntry\x12 \n\x18merkle_payment_timestamp\x18\x06 \x01(\x04\x12\x14\n\x0ctotal_amount\x18\x07 \x01(\t\x12\x1d\n\x15payment_vault_address\x18\x08 \x01(\t\x12\x1d\n\x15payment_token_address\x18\t \x01(\t\x12\x0f\n\x07rpc_url\x18\n \x01(\t\"Y\n\x13PoolCommitmentEntry\x12\x11\n\tpool_hash\x18\x01 \x01(\t\x12/\n\ncandidates\x18\x02 \x03(\x0b\x32\x1b.antd.v1.CandidateNodeEntry\"=\n\x12\x43\x61ndidateNodeEntry\x12\x17\n\x0frewards_address\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\t\"\xce\x01\n\x15\x46inalizeUploadRequest\x12\x11\n\tupload_id\x18\x01 \x01(\t\x12?\n\ttx_hashes\x18\x02 \x03(\x0b\x32,.antd.v1.FinalizeUploadRequest.TxHashesEntry\x12\x18\n\x10winner_pool_hash\x18\x03 \x01(\t\x12\x16\n\x0estore_data_map\x18\x04 \x01(\x08\x1a/\n\rTxHashesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"l\n\x16\x46inalizeUploadResponse\x12\x10\n\x08\x64\x61ta_map\x18\x01 \x01(\t\x12\x0f\n\x07\x61\x64\x64ress\x18\x02 \x01(\t\x12\x18\n\x10\x64\x61ta_map_address\x18\x03 \x01(\t\x12\x15\n\rchunks_stored\x18\x04 \x01(\x04\x32\x92\x02\n\rUploadService\x12V\n\x11PrepareFileUpload\x12!.antd.v1.PrepareFileUploadRequest\x1a\x1e.antd.v1.PrepareUploadResponse\x12V\n\x11PrepareDataUpload\x12!.antd.v1.PrepareDataUploadRequest\x1a\x1e.antd.v1.PrepareUploadResponse\x12Q\n\x0e\x46inalizeUpload\x12\x1e.antd.v1.FinalizeUploadRequest\x1a\x1f.antd.v1.FinalizeUploadResponseBDZ8github.com/WithAutonomi/ant-sdk/antd-go/proto/antd/v1;v1\xaa\x02\x07\x41ntd.V1b\x06proto3"
10
+ descriptor_data = "\n\x14\x61ntd/v1/upload.proto\x12\x07\x61ntd.v1\x1a\x14\x61ntd/v1/common.proto\"[\n\x18PrepareFileUploadRequest\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x12\n\nvisibility\x18\x02 \x01(\t\x12\x1d\n\x15include_signed_quotes\x18\x03 \x01(\x08\"[\n\x18PrepareDataUploadRequest\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x12\n\nvisibility\x18\x02 \x01(\t\x12\x1d\n\x15include_signed_quotes\x18\x03 \x01(\x08\"\xd0\x03\n\x15PrepareUploadResponse\x12\x11\n\tupload_id\x18\x01 \x01(\t\x12\x14\n\x0cpayment_type\x18\x02 \x01(\t\x12\'\n\x08payments\x18\x03 \x03(\x0b\x32\x15.antd.v1.PaymentEntry\x12\r\n\x05\x64\x65pth\x18\x04 \x01(\r\x12\x36\n\x10pool_commitments\x18\x05 \x03(\x0b\x32\x1c.antd.v1.PoolCommitmentEntry\x12 \n\x18merkle_payment_timestamp\x18\x06 \x01(\x04\x12\x31\n\x0emerkle_batches\x18\x0b \x03(\x0b\x32\x19.antd.v1.MerkleBatchEntry\x12\x14\n\x0ctotal_amount\x18\x07 \x01(\t\x12\x1d\n\x15payment_vault_address\x18\x08 \x01(\t\x12\x1d\n\x15payment_token_address\x18\t \x01(\t\x12\x0f\n\x07rpc_url\x18\n \x01(\t\x12\x30\n\rsigned_quotes\x18\x0c \x03(\x0b\x32\x19.antd.v1.SignedQuoteEntry\x12\x14\n\x0ctotal_chunks\x18\r \x01(\x04\x12\x1c\n\x14\x61lready_stored_count\x18\x0e \x01(\x04\"{\n\x10MerkleBatchEntry\x12\r\n\x05\x64\x65pth\x18\x01 \x01(\r\x12\x36\n\x10pool_commitments\x18\x02 \x03(\x0b\x32\x1c.antd.v1.PoolCommitmentEntry\x12 \n\x18merkle_payment_timestamp\x18\x03 \x01(\x04\"Y\n\x13PoolCommitmentEntry\x12\x11\n\tpool_hash\x18\x01 \x01(\t\x12/\n\ncandidates\x18\x02 \x03(\x0b\x32\x1b.antd.v1.CandidateNodeEntry\"=\n\x12\x43\x61ndidateNodeEntry\x12\x17\n\x0frewards_address\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\t\"\xea\x01\n\x15\x46inalizeUploadRequest\x12\x11\n\tupload_id\x18\x01 \x01(\t\x12?\n\ttx_hashes\x18\x02 \x03(\x0b\x32,.antd.v1.FinalizeUploadRequest.TxHashesEntry\x12\x18\n\x10winner_pool_hash\x18\x03 \x01(\t\x12\x1a\n\x12winner_pool_hashes\x18\x05 \x03(\t\x12\x16\n\x0estore_data_map\x18\x04 \x01(\x08\x1a/\n\rTxHashesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"l\n\x16\x46inalizeUploadResponse\x12\x10\n\x08\x64\x61ta_map\x18\x01 \x01(\t\x12\x0f\n\x07\x61\x64\x64ress\x18\x02 \x01(\t\x12\x18\n\x10\x64\x61ta_map_address\x18\x03 \x01(\t\x12\x15\n\rchunks_stored\x18\x04 \x01(\x04\x32\x92\x02\n\rUploadService\x12V\n\x11PrepareFileUpload\x12!.antd.v1.PrepareFileUploadRequest\x1a\x1e.antd.v1.PrepareUploadResponse\x12V\n\x11PrepareDataUpload\x12!.antd.v1.PrepareDataUploadRequest\x1a\x1e.antd.v1.PrepareUploadResponse\x12Q\n\x0e\x46inalizeUpload\x12\x1e.antd.v1.FinalizeUploadRequest\x1a\x1f.antd.v1.FinalizeUploadResponseBDZ8github.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)
@@ -17,6 +17,7 @@ module Antd
17
17
  PrepareFileUploadRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.PrepareFileUploadRequest").msgclass
18
18
  PrepareDataUploadRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.PrepareDataUploadRequest").msgclass
19
19
  PrepareUploadResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.PrepareUploadResponse").msgclass
20
+ MerkleBatchEntry = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.MerkleBatchEntry").msgclass
20
21
  PoolCommitmentEntry = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.PoolCommitmentEntry").msgclass
21
22
  CandidateNodeEntry = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.CandidateNodeEntry").msgclass
22
23
  FinalizeUploadRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.FinalizeUploadRequest").msgclass
@@ -40,8 +40,10 @@ module Antd
40
40
  rpc :PrepareDataUpload, ::Antd::V1::PrepareDataUploadRequest, ::Antd::V1::PrepareUploadResponse
41
41
  # Phase 2: finalize an upload after the external EVM payment has landed.
42
42
  # For wave-batch uploads pass `tx_hashes`; for merkle uploads pass
43
- # `winner_pool_hash` from the `MerklePaymentMade` event. The server-side
44
- # stored upload state is consumed (one-shot).
43
+ # `winner_pool_hashes` — one `MerklePaymentMade` winner hash per entry in
44
+ # the prepare response's `merkle_batches` (`winner_pool_hash` remains
45
+ # accepted when there is exactly one batch). The server-side stored upload
46
+ # state is consumed (one-shot) once inputs validate.
45
47
  rpc :FinalizeUpload, ::Antd::V1::FinalizeUploadRequest, ::Antd::V1::FinalizeUploadResponse
46
48
  end
47
49
 
data/lib/antd/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Antd
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: antd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - WithAutonomi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-15 00:00:00.000000000 Z
11
+ date: 2026-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: grpc