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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d8d53161e99f51956a6ba0b004669faf4a0d28e7e6998e8abe9fdbec35f83bd2
|
|
4
|
+
data.tar.gz: d9a2b3e9cb282eaf712ca3783c92cff130dd1958106a62c1c88637e686c223a1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b621b9cf29751e81e87dbb03f3377898732ae3eb56bd10e165d39d6c597f1fcf67ccb31ffdffc5aee5fe7210fa58658e2080d37f3d54fd4759c1a93f9e9f50ee
|
|
7
|
+
data.tar.gz: b1f17116698f10e199db336aaf4bd415c6e750f102299408457692aeb602f06d238c339ce4a58fcfa679261d7d74fef8122edcaa03496b0a1b0dcee4bb1b01a7
|
data/README.md
CHANGED
|
@@ -18,7 +18,7 @@ gem install antd
|
|
|
18
18
|
|
|
19
19
|
## Compatibility
|
|
20
20
|
|
|
21
|
-
This gem talks to a running [antd](https://github.com/WithAutonomi/ant-sdk/tree/main/antd) daemon; it does not join the network itself. Ruby 3.1+. Tested against antd 0.12.x. The REST client has no runtime dependencies; the gRPC transport (`Antd::GrpcClient`) needs the `grpc` gem, which is deliberately not a runtime dependency of this gem — add `gem "grpc"` yourself to use it.
|
|
21
|
+
This gem talks to a running [antd](https://github.com/WithAutonomi/ant-sdk/tree/main/antd) daemon; it does not join the network itself. Ruby 3.1+. Tested against antd 0.12.x; the pointer methods need antd 0.16.0 or later, the first release with pointers. The REST client has no runtime dependencies; the gRPC transport (`Antd::GrpcClient`) needs the `grpc` gem, which is deliberately not a runtime dependency of this gem — add `gem "grpc"` yourself to use it.
|
|
22
22
|
|
|
23
23
|
## Quick Start
|
|
24
24
|
|
|
@@ -53,18 +53,15 @@ Install the gRPC gem (listed as an optional development dependency):
|
|
|
53
53
|
gem install grpc grpc-tools
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
The Ruby protobuf/gRPC stubs in `lib/antd/v1/` are generated from the proto
|
|
57
|
+
definitions and committed. After changing the protos, regenerate them and
|
|
58
|
+
commit the result (CI fails when they drift):
|
|
57
59
|
|
|
58
60
|
```bash
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
--ruby_out=lib --grpc_out=lib \
|
|
62
|
-
antd/v1/common.proto antd/v1/health.proto antd/v1/data.proto \
|
|
63
|
-
antd/v1/chunks.proto antd/v1/files.proto
|
|
61
|
+
gem install grpc-tools -v 1.84.0 # the version the script pins
|
|
62
|
+
antd-ruby/scripts/generate-protos.sh
|
|
64
63
|
```
|
|
65
64
|
|
|
66
|
-
The generated files are expected under `lib/antd/v1/`.
|
|
67
|
-
|
|
68
65
|
### Usage
|
|
69
66
|
|
|
70
67
|
```ruby
|
|
@@ -87,7 +84,24 @@ puts "Retrieved: #{data}"
|
|
|
87
84
|
```
|
|
88
85
|
|
|
89
86
|
The `GrpcClient` raises the same `Antd::AntdError` hierarchy as the REST
|
|
90
|
-
client, translating gRPC status codes to the appropriate error subclass
|
|
87
|
+
client, translating gRPC status codes to the appropriate error subclass
|
|
88
|
+
(an `ABORTED` whose status details — `GRPC::BadStatus#details`, the text
|
|
89
|
+
the daemon sent — start with `Partial upload:` becomes
|
|
90
|
+
`Antd::PartialUploadError`, with the chunk counts, `retryable` and
|
|
91
|
+
`retention_known` parsed from that text — see
|
|
92
|
+
[Partial uploads](#partial-uploads); any other
|
|
93
|
+
`ABORTED`, including one that only mentions `Partial upload:` later in its
|
|
94
|
+
text, stays a generic `Antd::AntdError`).
|
|
95
|
+
|
|
96
|
+
The pointer and close-group errors share gRPC status codes with older ones, so
|
|
97
|
+
the daemon names them in an `x-antd-error-code` trailer
|
|
98
|
+
(`GRPC::BadStatus#metadata`). The client reads it on every call and, when it
|
|
99
|
+
agrees with the status code, raises the same typed error as REST:
|
|
100
|
+
`FAILED_PRECONDITION` + `POINTER_FINAL` → `PointerFinalError`, + `POINTER_FORKED`
|
|
101
|
+
→ `PointerForkedError`, `ABORTED` + `CONFLICT` → `ConflictError`, and
|
|
102
|
+
`UNAVAILABLE` + `CLOSE_GROUP_SHORTFALL` → `CloseGroupShortfallError` (`retryable`
|
|
103
|
+
when the details close with `(paid attempt retained...)`). Without the trailer,
|
|
104
|
+
`FAILED_PRECONDITION` stays a `PaymentError` and `UNAVAILABLE` a `NetworkError`.
|
|
91
105
|
|
|
92
106
|
> **Note:** Wallet operations (address, balance, approve) and payment_mode are available via REST only.
|
|
93
107
|
|
|
@@ -146,6 +160,62 @@ client = Antd::Client.new(base_url: "http://custom-host:9090", timeout: 30)
|
|
|
146
160
|
| `file_get_public(address, dest_path)` | Download a public file by address |
|
|
147
161
|
| `file_cost(path, is_public, payment_mode: :auto)` | Estimate upload cost — returns `UploadCostEstimate` with size, chunks, gas, payment mode |
|
|
148
162
|
|
|
163
|
+
### Pointers
|
|
164
|
+
|
|
165
|
+
A pointer is a small, owner-signed, mutable reference. Its address is derived
|
|
166
|
+
from the owner's key and never changes; what it points at (a chunk, such as a
|
|
167
|
+
public file's data map, or another pointer) can be changed by the owner, and
|
|
168
|
+
the owner can hand it over to another pointer for good. The daemon holds the
|
|
169
|
+
owner keys, loaded at startup with `antd --pointer-keys <file-or-dir>` (files
|
|
170
|
+
in the format `ant pointer keygen` writes), so every write names the pointer by
|
|
171
|
+
its address (`owner`), never by a key. An address the daemon holds no key for
|
|
172
|
+
is a `BadRequestError`.
|
|
173
|
+
|
|
174
|
+
**Every write pays** (about the price of one chunk), and every attempt pays
|
|
175
|
+
again. The SDK never retries a request, and you should not retry
|
|
176
|
+
`pointer_create`, `pointer_update`, `pointer_transfer` or `pointer_finalize`
|
|
177
|
+
automatically either. Reads are free to retry; one during an update or churn
|
|
178
|
+
can fail with `CloseGroupShortfallError`.
|
|
179
|
+
|
|
180
|
+
Both `Antd::Client` and `Antd::GrpcClient` have these methods:
|
|
181
|
+
|
|
182
|
+
| Method | Description |
|
|
183
|
+
|--------|-------------|
|
|
184
|
+
| `pointer_keys` | Addresses of the pointers this daemon holds keys for, so can write — `Array<String>` |
|
|
185
|
+
| `pointer_create(owner, target, kind: nil)` | Create a pointer, paid by the daemon wallet — returns `PointerWriteResult` (`address`, `state_id`, `counter`). `AlreadyExistsError`, before paying, if it exists. `kind` is `PointerKind::CHUNK` (the default) or `PointerKind::POINTER` |
|
|
186
|
+
| `pointer_update(owner, target, kind: nil)` | Point a pointer at a new target, creating it if needed — `PointerWriteResult`. `PointerFinalError`, usually before paying, once it was handed over or frozen |
|
|
187
|
+
| `pointer_transfer(owner, recipient)` | Hand a pointer over for good to the recipient's pointer, which must exist. Irreversible. Returns `PointerTransferResult` with the close group's `finality` (nil, with `finality_error`, if that check failed; the transfer stands) |
|
|
188
|
+
| `pointer_get(address)` | Read a pointer — `PointerRecord` (`counter`, `target`, `kind`/`kind_tag`, `is_final` / `final?`, `transferred_to`, the owner's public key). `NotFoundError` if there is none |
|
|
189
|
+
| `pointer_resolve(address)` | Follow a chain of pointers to the target at its end — `PointerTarget` (`kind`, `kind_tag`, `target`) |
|
|
190
|
+
| `pointer_finality(address)` | Ask the whole close group where a pointer stands — `PointerFinality`. Check `final?` before relying on a transfer |
|
|
191
|
+
| `pointer_controller(address)` | Follow transfers to the pointer whose owner now decides — `PointerController` (`pointer`, `transfers`, `frozen`) |
|
|
192
|
+
| `pointer_prepare(owner, op, target: nil, kind: nil, recipient: nil, include_signed_quotes: false)` | External signer: the daemon signs a `PointerOp::CREATE`, `UPDATE` or `TRANSFER` and quotes it — `PointerPrepareResult` (wave-batch `payments` for `payForQuotes()`; `signed_quotes` when asked). Needs no daemon wallet. Other writes to the pointer raise `ConflictError` until it is finalized or expires (one hour) |
|
|
193
|
+
| `pointer_finalize(upload_id, tx_hashes)` | External signer: store the prepared write once paid — `PointerFinalizeResult`. A write that falls short after payment raises `CloseGroupShortfallError` with `retryable`: repeat the same call to store it against the same payment |
|
|
194
|
+
|
|
195
|
+
Counters are `Integer`s. The daemon sends them as decimal strings, because a
|
|
196
|
+
final state (a transfer or a freeze) sits at `Antd::POINTER_FINAL_COUNTER`
|
|
197
|
+
(2^64 − 1), past what a JSON number keeps exactly in many languages. A counter
|
|
198
|
+
that is not a decimal u64 raises `InternalError`; it never reads as 0.
|
|
199
|
+
|
|
200
|
+
`PointerFinality#status` is one of the `Antd::PointerFinalityStatus` strings:
|
|
201
|
+
`OPEN` (with the corroborated `counter`, or nil), `SETTLING`, `UNCONFIRMED` and
|
|
202
|
+
`FINAL` (with the one final `state`), or `FORKED` (with every final state in
|
|
203
|
+
`states` and the `majority`). Only `FINAL` means a transfer is beyond dispute.
|
|
204
|
+
A status a newer daemon adds is kept verbatim (`status_known?` is false), so
|
|
205
|
+
give a `case` over it an `else` branch. Unset fields read as `nil` on both
|
|
206
|
+
transports: REST sends `null`, gRPC an empty string or no message.
|
|
207
|
+
|
|
208
|
+
The daemon accepts addresses with a `0x` prefix but always returns bare hex;
|
|
209
|
+
normalise before comparing.
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
owner = client.pointer_keys.first
|
|
213
|
+
client.pointer_create(owner, client.data_put_public("v1").address) # pays
|
|
214
|
+
client.pointer_update(owner, client.data_put_public("v2").address) # pays again
|
|
215
|
+
puts client.data_get_public(client.pointer_resolve(owner).target) # "v2"
|
|
216
|
+
puts client.pointer_finality(owner).status # "open"
|
|
217
|
+
```
|
|
218
|
+
|
|
149
219
|
## Error Handling
|
|
150
220
|
|
|
151
221
|
All errors inherit from `Antd::AntdError` and can be caught by type:
|
|
@@ -172,6 +242,151 @@ end
|
|
|
172
242
|
| `TooLargeError` | 413 | Payload too large |
|
|
173
243
|
| `InternalError` | 500 | Server error |
|
|
174
244
|
| `NetworkError` | 502 | Network unreachable |
|
|
245
|
+
| `PartialUploadError` | 502 (`code: "PARTIAL_UPLOAD"`) | Finalize stored some chunks but not all — subclass of `NetworkError` |
|
|
246
|
+
| `ServiceUnavailableError` | 503 | Wallet not configured |
|
|
247
|
+
| `PointerFinalError` | 409 (`code: "POINTER_FINAL"`) / gRPC `FAILED_PRECONDITION` | The pointer is transferred or frozen; nothing can change it |
|
|
248
|
+
| `PointerForkedError` | 409 (`code: "POINTER_FORKED"`) / gRPC `FAILED_PRECONDITION` | The owner signed two final states and no majority holds either — subclass of `ForkError` |
|
|
249
|
+
| `ConflictError` | 409 (`code: "CONFLICT"`) / gRPC `ABORTED` | A pointer write while a prepared write to it awaits payment, or an update that lost to a newer state |
|
|
250
|
+
| `CloseGroupShortfallError` | 503 (`code: "CLOSE_GROUP_SHORTFALL"`) / gRPC `UNAVAILABLE` | Too few of the close group answered or stored; `retryable` is true only for a pointer finalize that kept the paid state |
|
|
251
|
+
|
|
252
|
+
The pointer errors are read from the error body's `code` (gRPC: the
|
|
253
|
+
`x-antd-error-code` trailer), so a 409 with one of those codes is not an
|
|
254
|
+
`AlreadyExistsError`. `CloseGroupShortfallError` is not only for pointers: a
|
|
255
|
+
daemon with the pointer API reports a close group that fell short on any call
|
|
256
|
+
(a chunk store, say) this way, where older daemons sent a 500
|
|
257
|
+
(`InternalError`). It is not a `ServiceUnavailableError`, so rescue it by name.
|
|
258
|
+
|
|
259
|
+
### Partial uploads
|
|
260
|
+
|
|
261
|
+
A `finalize_upload` / `finalize_merkle_upload` / `finalize_chunk_upload` call
|
|
262
|
+
can fail *after* the external signer has paid: some chunks store, others miss
|
|
263
|
+
quorum after the daemon's own retries. The daemon reports this as HTTP `502`
|
|
264
|
+
with `code: "PARTIAL_UPLOAD"` (gRPC `ABORTED`), and the SDK raises
|
|
265
|
+
`Antd::PartialUploadError` carrying `chunks_stored`, `chunks_failed`,
|
|
266
|
+
`total_chunks`, `retryable` and `retention_known`. The on-chain payment
|
|
267
|
+
persists and the stored chunks stay on the network; what to do next depends
|
|
268
|
+
on what the daemon said about the paid attempt:
|
|
269
|
+
|
|
270
|
+
- **`retryable`** (antd >= 0.14.0; implies `retention_known`) — the daemon
|
|
271
|
+
kept the paid attempt under the same `upload_id`. Call the **same finalize
|
|
272
|
+
method again with the same `upload_id` and payment artefacts** to store the
|
|
273
|
+
remainder against the same payment: no re-prepare, no second signature, no
|
|
274
|
+
double payment. Bound the loop — a persistent failure raises this error on
|
|
275
|
+
every call — so cap the attempts and treat a `chunks_failed` that stops
|
|
276
|
+
shrinking as stuck. The retained attempt expires with the daemon's
|
|
277
|
+
pending-upload TTL.
|
|
278
|
+
- **`retention_known && !retryable`** — the daemon confirmed nothing was
|
|
279
|
+
retained (a merkle finalize with deliberately unpaid batches). Re-prepare
|
|
280
|
+
the same content: already-stored chunks are skipped, so the retry pays only
|
|
281
|
+
for the remainder.
|
|
282
|
+
- **`!retention_known`** — retention is unknown: the error did not say, in a
|
|
283
|
+
form the SDK could read, whether the paid attempt was kept. The daemon may
|
|
284
|
+
still hold it (it records the resume handle before it returns the error).
|
|
285
|
+
Stop automatic recovery, keep the `upload_id` and the original payment
|
|
286
|
+
artefacts (tx hashes, or the merkle winner pool hash), and reconcile before
|
|
287
|
+
re-preparing or paying again. **Never pay again on this signal alone.**
|
|
288
|
+
Daemons older than 0.14.0 never send `retryable`, so their REST partial
|
|
289
|
+
uploads read as unknown.
|
|
290
|
+
|
|
291
|
+
Over REST, `retention_known` is true only when the body's `retryable` is a
|
|
292
|
+
JSON boolean (`true` or `false`); absent, `null` or any other type reads as
|
|
293
|
+
unknown. Over gRPC there is no structured body, so the counts and flags are
|
|
294
|
+
parsed from the status text,
|
|
295
|
+
`Partial upload: <stored>/<total> chunks stored, <failed> failed after retries: <reason> (<hint>)`.
|
|
296
|
+
`retention_known` is true only when the text starts with that counts
|
|
297
|
+
pattern, all three counts parse (each within the daemon's u64 range), and the
|
|
298
|
+
text ends with one of the daemon's two hints: `(paid attempt retained...)`
|
|
299
|
+
sets `retryable`, and `(stored chunks persist; re-prepare the same content...)`
|
|
300
|
+
means the daemon confirmed nothing was retained (daemons older than 0.14.0
|
|
301
|
+
write only this one). If the counts cannot be read, all three are `0` and
|
|
302
|
+
both flags are `false`. Readable counts with a missing, truncated or
|
|
303
|
+
unrecognised hint keep the counts, but both flags stay `false`: retention is
|
|
304
|
+
unknown, so stop and reconcile rather than re-prepare.
|
|
305
|
+
|
|
306
|
+
`PartialUploadError` subclasses `NetworkError` (the 502 mapping), so existing
|
|
307
|
+
`rescue Antd::NetworkError` blocks keep catching it; rescue the subclass first
|
|
308
|
+
to handle it specifically.
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
MAX_ATTEMPTS = 5
|
|
312
|
+
last_failed = nil
|
|
313
|
+
attempt = 0
|
|
314
|
+
begin
|
|
315
|
+
attempt += 1
|
|
316
|
+
result = client.finalize_upload(prep.upload_id, tx_hashes)
|
|
317
|
+
rescue Antd::PartialUploadError => e
|
|
318
|
+
# Not resumable. With retention_known, nothing was retained: re-prepare.
|
|
319
|
+
# Without it, retention is unknown: stop, keep upload_id + tx_hashes and
|
|
320
|
+
# reconcile before re-preparing or paying again.
|
|
321
|
+
raise unless e.retryable
|
|
322
|
+
stuck = !last_failed.nil? && e.chunks_failed >= last_failed
|
|
323
|
+
raise if attempt >= MAX_ATTEMPTS || stuck # bounded: same payment, same upload_id
|
|
324
|
+
last_failed = e.chunks_failed
|
|
325
|
+
sleep(2 * attempt)
|
|
326
|
+
retry
|
|
327
|
+
end
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
See [`docs/external-signer-flow.md` section 6](../docs/external-signer-flow.md#6-retry-a-partial-store--same-upload_id-same-payment)
|
|
331
|
+
for the daemon contract and `examples/07_external_signer.rb` for a
|
|
332
|
+
`finalize_with_retry` helper.
|
|
333
|
+
|
|
334
|
+
## Manifests
|
|
335
|
+
|
|
336
|
+
A manifest (ADR-0006) describes a set of files in one `.ant` file or
|
|
337
|
+
`ant://` link: each entry has an optional path and size, and either an
|
|
338
|
+
embedded DataMap (from a private upload; the manifest is then the key to the
|
|
339
|
+
file) or a public file's DataMap address. The daemon builds and decodes
|
|
340
|
+
manifests, so the SDK never handles the format itself. These methods need
|
|
341
|
+
antd 0.16.0 or later.
|
|
342
|
+
|
|
343
|
+
**Nothing is uploaded or paid for.** Building one from DataMaps and addresses
|
|
344
|
+
you already hold is free. Embedding roots (on build) and `resolve` (on
|
|
345
|
+
decode) read from the network, so either can time out; retrying is safe.
|
|
346
|
+
|
|
347
|
+
Both `Antd::Client` and `Antd::GrpcClient` have these methods:
|
|
348
|
+
|
|
349
|
+
| Method | Description |
|
|
350
|
+
|--------|-------------|
|
|
351
|
+
| `build_manifest(entries, name: nil, torrent_info_hashes: nil, embed_roots: true)` | Build a manifest — returns `BuiltManifest` (`manifest`, the raw `.ant` bytes; `link`, or nil when it would exceed 1,500 bytes; `entries`, sorted by effective name). Each entry is a `ManifestEntryInput` or a Hash with `path`, `size`, and exactly one of `data_map` (hex) or `address` (hex); both or neither raises `BadRequestError` (400, the daemon's wording) before anything is sent. By default the daemon resolves each shrunk DataMap and embeds its root when it is at most 64 KiB; `embed_roots: false` embeds them as given |
|
|
352
|
+
| `decode_manifest(manifest, resolve: false)` | Decode `.ant` bytes — returns `DecodedManifest` (`source`, `name`, `torrent_info_hash_v1`/`_v2`, `entries`) |
|
|
353
|
+
| `decode_manifest_link(link, resolve: false)` | Decode an `ant://manifest/` link, a file link or a bare 64-hex address (a `ManifestSource::FILE_LINK`: one public entry, no path) |
|
|
354
|
+
|
|
355
|
+
Each entry comes back as a `ManifestEntryInfo`: `effective_name` (the path,
|
|
356
|
+
or the content address when there is none), `path`, `size` (what the
|
|
357
|
+
manifest records: the creator's claim), `kind` (`ManifestEntryKind::EMBEDDED`
|
|
358
|
+
or `PUBLIC`), `content_address`, `data_map` (decode only, embedded entries
|
|
359
|
+
only, in the hex form `data_get` and `file_get` take), `exact_size`, and
|
|
360
|
+
`error`. With `resolve: true` the daemon fetches each DataMap (not the
|
|
361
|
+
content) to fill in `exact_size`; an entry that fails gets an `error` instead
|
|
362
|
+
of failing the call.
|
|
363
|
+
|
|
364
|
+
`manifest` is always raw bytes: the SDK base64-encodes it for REST and sends
|
|
365
|
+
it as-is over gRPC. Sizes are `Integer`s, and anything a manifest leaves unset
|
|
366
|
+
is `nil` on both transports, so a missing `size` or `exact_size` is `nil`,
|
|
367
|
+
never `0`. An entry the daemon rejects (a bad path such as `../x`, a malformed
|
|
368
|
+
DataMap or address) raises `BadRequestError` (gRPC `INVALID_ARGUMENT`) with
|
|
369
|
+
the daemon's message, which names `entries[i]`. The SDK's own checks (both or
|
|
370
|
+
neither of `data_map` and `address`, an empty manifest or link) raise the same
|
|
371
|
+
error with the daemon's exact text, so the two cannot be told apart.
|
|
372
|
+
|
|
373
|
+
Over gRPC, a manifest can be up to 64 MiB, past gRPC's 4 MiB default, so the
|
|
374
|
+
client talks to ManifestService on a channel of its own with the send and
|
|
375
|
+
receive limits raised to `Antd::MANIFEST_GRPC_MAX_MESSAGE` (256 MiB). The
|
|
376
|
+
other services keep the defaults.
|
|
377
|
+
|
|
378
|
+
```ruby
|
|
379
|
+
a = client.data_put_public("one").address # pays
|
|
380
|
+
b = client.data_put_public("two").address # pays
|
|
381
|
+
built = client.build_manifest(
|
|
382
|
+
[{ path: "notes/one.txt", address: a }, { path: "notes/two.txt", address: b }],
|
|
383
|
+
name: "notes"
|
|
384
|
+
) # free
|
|
385
|
+
File.binwrite("notes.ant", built.manifest)
|
|
386
|
+
client.decode_manifest_link(built.link, resolve: true).entries.each do |e|
|
|
387
|
+
puts "#{e.effective_name}: #{e.exact_size} bytes"
|
|
388
|
+
end
|
|
389
|
+
```
|
|
175
390
|
|
|
176
391
|
## Examples
|
|
177
392
|
|
|
@@ -182,3 +397,6 @@ See the [examples/](examples/) directory:
|
|
|
182
397
|
- `03_chunks.rb` — Chunk put/get
|
|
183
398
|
- `04_files.rb` — File upload and download
|
|
184
399
|
- `06_private_data.rb` — Private data put/get
|
|
400
|
+
- `07_external_signer.rb` — External-signer prepare/pay/finalize with a bounded partial-upload retry
|
|
401
|
+
- `08_pointers.rb` — Create a pointer, update it, resolve it and check its finality. Run with `ANTD_POINTER_OWNER=<address>` (one of `pointer_keys`); `ANTD_BASE_URL` overrides the daemon URL. Each write pays
|
|
402
|
+
- `09_manifests.rb` — Store two pieces of public data, build a manifest of them, print its link and entries, and decode the link with `resolve`. `ANTD_BASE_URL` overrides the daemon URL. The two uploads pay; the manifest calls do not
|
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,
|
|
@@ -387,8 +417,250 @@ module Antd
|
|
|
387
417
|
parse_finalize_response(j)
|
|
388
418
|
end
|
|
389
419
|
|
|
420
|
+
# --- Pointers ---
|
|
421
|
+
#
|
|
422
|
+
# Owner-signed, paid, mutable references (antd 0.16.0 or
|
|
423
|
+
# later). The daemon signs with owner keys it loaded at
|
|
424
|
+
# startup (+antd --pointer-keys <file-or-dir>+), so a write names the
|
|
425
|
+
# pointer by its address (+owner+); +pointer_keys+ lists the pointers it
|
|
426
|
+
# can write.
|
|
427
|
+
#
|
|
428
|
+
# Every write pays, about the price of one chunk, and every attempt pays
|
|
429
|
+
# again. This client never retries a request, and callers must not retry
|
|
430
|
+
# +pointer_create+, +pointer_update+, +pointer_transfer+ or
|
|
431
|
+
# +pointer_finalize+ automatically (the one exception: a finalize that
|
|
432
|
+
# raised +CloseGroupShortfallError+ with +retryable+). Reads are free to
|
|
433
|
+
# retry.
|
|
434
|
+
#
|
|
435
|
+
# Addresses are 64 hex characters. The daemon accepts a 0x prefix but
|
|
436
|
+
# always returns bare hex, so normalise before comparing.
|
|
437
|
+
|
|
438
|
+
# The addresses of the pointers this daemon holds owner keys for. Only
|
|
439
|
+
# these can be written.
|
|
440
|
+
# @return [Array<String>]
|
|
441
|
+
def pointer_keys
|
|
442
|
+
j = do_json(:get, "/v1/pointers/keys")
|
|
443
|
+
(j["keys"] || []).map { |k| k["address"] || "" }
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Create the pointer at +owner+, pointing at +target+, paid by the daemon
|
|
447
|
+
# wallet. Pays; do not retry automatically.
|
|
448
|
+
# @param owner [String] address of the pointer to write
|
|
449
|
+
# @param target [String] address to point at
|
|
450
|
+
# @param kind [String, nil] +PointerKind::CHUNK+ (the daemon's default when
|
|
451
|
+
# nil) or +PointerKind::POINTER+
|
|
452
|
+
# @return [PointerWriteResult]
|
|
453
|
+
# @raise [AlreadyExistsError] before paying, if the pointer exists
|
|
454
|
+
# @raise [BadRequestError] if the daemon holds no key for +owner+
|
|
455
|
+
def pointer_create(owner, target, kind: nil)
|
|
456
|
+
pointer_write("/v1/pointers", owner, target, kind)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# Point the pointer at +owner+ at +target+, paid by the daemon wallet,
|
|
460
|
+
# creating it if it does not exist. Pays; do not retry automatically.
|
|
461
|
+
# @param owner [String]
|
|
462
|
+
# @param target [String]
|
|
463
|
+
# @param kind [String, nil] see +pointer_create+
|
|
464
|
+
# @return [PointerWriteResult]
|
|
465
|
+
# @raise [PointerFinalError] if the pointer was handed over or frozen
|
|
466
|
+
# (usually before paying; see +PointerFinalError+)
|
|
467
|
+
# @raise [ConflictError] if a prepared write to it awaits payment (refused
|
|
468
|
+
# before paying), or the update lost to a newer state while in flight
|
|
469
|
+
# (possibly after paying); read the pointer before writing again
|
|
470
|
+
def pointer_update(owner, target, kind: nil)
|
|
471
|
+
pointer_write("/v1/pointers/update", owner, target, kind)
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# Hand the pointer at +owner+ over to the pointer at +recipient+, for
|
|
475
|
+
# good, paid by the daemon wallet. Readers of +owner+'s address are
|
|
476
|
+
# redirected to the recipient's pointer, which only its owner can move.
|
|
477
|
+
# Irreversible once stored. Pays; do not retry automatically.
|
|
478
|
+
#
|
|
479
|
+
# Refused before paying if +recipient+ does not exist or leads back
|
|
480
|
+
# (+BadRequestError+), and usually if the pointer is already final
|
|
481
|
+
# (+PointerFinalError+). A +CloseGroupShortfallError+ means too few of the
|
|
482
|
+
# close group answered or stored it. That can happen before paying (the
|
|
483
|
+
# transfer checks every peer first) or after, so check the pointer with
|
|
484
|
+
# +pointer_finality+ before paying again.
|
|
485
|
+
#
|
|
486
|
+
# @param owner [String]
|
|
487
|
+
# @param recipient [String] address of the recipient's pointer
|
|
488
|
+
# @return [PointerTransferResult]
|
|
489
|
+
def pointer_transfer(owner, recipient)
|
|
490
|
+
j = do_json(:post, "/v1/pointers/transfer", { owner: owner, recipient: recipient })
|
|
491
|
+
PointerTransferResult.new(
|
|
492
|
+
address: j["address"] || "",
|
|
493
|
+
state_id: j["state_id"] || "",
|
|
494
|
+
finality: j["finality"].is_a?(Hash) ? parse_pointer_finality(j["finality"]) : nil,
|
|
495
|
+
finality_error: presence(j["finality_error"])
|
|
496
|
+
)
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Read the pointer at +address+.
|
|
500
|
+
# @param address [String]
|
|
501
|
+
# @return [PointerRecord]
|
|
502
|
+
# @raise [NotFoundError] if no pointer exists there
|
|
503
|
+
def pointer_get(address)
|
|
504
|
+
j = do_json(:get, "/v1/pointers/#{address}")
|
|
505
|
+
PointerRecord.new(
|
|
506
|
+
address: j["address"] || "",
|
|
507
|
+
owner: j["owner"] || "",
|
|
508
|
+
counter: Antd.parse_pointer_counter("counter", j["counter"]),
|
|
509
|
+
kind: j["kind"] || "",
|
|
510
|
+
kind_tag: (j["kind_tag"] || 0).to_i,
|
|
511
|
+
target: j["target"] || "",
|
|
512
|
+
state_id: j["state_id"] || "",
|
|
513
|
+
is_final: j["is_final"] == true,
|
|
514
|
+
transferred_to: presence(j["transferred_to"])
|
|
515
|
+
)
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# Follow a chain of pointers from +address+ to the target at its end.
|
|
519
|
+
# @param address [String]
|
|
520
|
+
# @return [PointerTarget]
|
|
521
|
+
# @raise [NotFoundError] if a pointer on the chain does not exist
|
|
522
|
+
def pointer_resolve(address)
|
|
523
|
+
j = do_json(:get, "/v1/pointers/#{address}/resolve")
|
|
524
|
+
PointerTarget.new(kind: j["kind"] || "", kind_tag: (j["kind_tag"] || 0).to_i, target: j["target"] || "")
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# Ask the whole close group where the pointer at +address+ stands. Check
|
|
528
|
+
# it before relying on a transfer: only +PointerFinality#final?+ means the
|
|
529
|
+
# transfer is beyond dispute.
|
|
530
|
+
# @param address [String]
|
|
531
|
+
# @return [PointerFinality]
|
|
532
|
+
def pointer_finality(address)
|
|
533
|
+
parse_pointer_finality(do_json(:get, "/v1/pointers/#{address}/finality"))
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Follow the transfers from +address+ to the pointer whose owner now
|
|
537
|
+
# decides what it resolves to.
|
|
538
|
+
# @param address [String]
|
|
539
|
+
# @return [PointerController]
|
|
540
|
+
def pointer_controller(address)
|
|
541
|
+
j = do_json(:get, "/v1/pointers/#{address}/controller")
|
|
542
|
+
PointerController.new(
|
|
543
|
+
pointer: j["pointer"] || "",
|
|
544
|
+
transfers: (j["transfers"] || []).grep(String),
|
|
545
|
+
frozen: j["frozen"] == true
|
|
546
|
+
)
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# External signer: sign a pointer write with the daemon's loaded owner key
|
|
550
|
+
# and quote it, without paying. The external signer pays +payments+ with
|
|
551
|
+
# +payForQuotes()+ and then calls +pointer_finalize+. The daemon needs no
|
|
552
|
+
# wallet. While the prepared write awaits payment (up to an hour), other
|
|
553
|
+
# writes to the same pointer raise +ConflictError+.
|
|
554
|
+
#
|
|
555
|
+
# @param owner [String] address of the pointer to write
|
|
556
|
+
# @param op [String] +PointerOp::CREATE+, +UPDATE+ or +TRANSFER+
|
|
557
|
+
# @param target [String, nil] create / update: the address to point at
|
|
558
|
+
# @param kind [String, nil] create / update: see +pointer_create+
|
|
559
|
+
# @param recipient [String, nil] transfer: the recipient's pointer
|
|
560
|
+
# @param include_signed_quotes [Boolean] also return each quote in full
|
|
561
|
+
# signed form (+PointerPrepareResult#signed_quotes+)
|
|
562
|
+
# @return [PointerPrepareResult]
|
|
563
|
+
def pointer_prepare(owner, op, target: nil, kind: nil, recipient: nil, include_signed_quotes: false)
|
|
564
|
+
body = { owner: owner, op: op }
|
|
565
|
+
body[:target] = target unless target.nil?
|
|
566
|
+
body[:kind] = kind unless kind.nil?
|
|
567
|
+
body[:recipient] = recipient unless recipient.nil?
|
|
568
|
+
body[:include_signed_quotes] = true if include_signed_quotes
|
|
569
|
+
j = do_json(:post, "/v1/pointers/prepare", body)
|
|
570
|
+
PointerPrepareResult.new(
|
|
571
|
+
upload_id: j["upload_id"] || "",
|
|
572
|
+
op: j["op"] || "",
|
|
573
|
+
address: j["address"] || "",
|
|
574
|
+
state_id: j["state_id"] || "",
|
|
575
|
+
counter: Antd.parse_pointer_counter("counter", j["counter"]),
|
|
576
|
+
payment_type: j["payment_type"] || "",
|
|
577
|
+
payments: (j["payments"] || []).map do |p|
|
|
578
|
+
PaymentInfo.new(quote_hash: p["quote_hash"], rewards_address: p["rewards_address"], amount: p["amount"])
|
|
579
|
+
end,
|
|
580
|
+
total_amount: j["total_amount"] || "",
|
|
581
|
+
payment_vault_address: j["payment_vault_address"] || "",
|
|
582
|
+
payment_token_address: j["payment_token_address"] || "",
|
|
583
|
+
rpc_url: j["rpc_url"] || "",
|
|
584
|
+
signed_quotes: (j["signed_quotes"] || []).map do |q|
|
|
585
|
+
SignedQuoteEntry.new(
|
|
586
|
+
quote_hash: q["quote_hash"] || "",
|
|
587
|
+
quote: q["quote"] || "",
|
|
588
|
+
commitment_sidecar: presence(q["commitment_sidecar"])
|
|
589
|
+
)
|
|
590
|
+
end
|
|
591
|
+
)
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
# External signer: store a prepared pointer write once it is paid.
|
|
595
|
+
#
|
|
596
|
+
# A write that falls short after payment raises +CloseGroupShortfallError+
|
|
597
|
+
# with +retryable+ set: the daemon kept the paid state, so call this again
|
|
598
|
+
# with the same +upload_id+ (+tx_hashes+ is then ignored) to store it
|
|
599
|
+
# against the same payment. Bound that loop; the session expires with the
|
|
600
|
+
# daemon's one-hour pending TTL. Any other failure ends the session.
|
|
601
|
+
#
|
|
602
|
+
# @param upload_id [String] from +pointer_prepare+
|
|
603
|
+
# @param tx_hashes [Hash<String, String>] each +quote_hash+ from
|
|
604
|
+
# +payments+ to the +payForQuotes()+ transaction hash
|
|
605
|
+
# @return [PointerFinalizeResult]
|
|
606
|
+
def pointer_finalize(upload_id, tx_hashes)
|
|
607
|
+
j = do_json(:post, "/v1/pointers/finalize", { upload_id: upload_id, tx_hashes: tx_hashes })
|
|
608
|
+
PointerFinalizeResult.new(
|
|
609
|
+
address: j["address"] || "",
|
|
610
|
+
state_id: j["state_id"] || "",
|
|
611
|
+
counter: Antd.parse_pointer_counter("counter", j["counter"]),
|
|
612
|
+
finality: j["finality"].is_a?(Hash) ? parse_pointer_finality(j["finality"]) : nil
|
|
613
|
+
)
|
|
614
|
+
end
|
|
615
|
+
|
|
390
616
|
private
|
|
391
617
|
|
|
618
|
+
# POST a pointer create / update. +kind+ is omitted when nil, so the
|
|
619
|
+
# daemon applies its default (chunk).
|
|
620
|
+
def pointer_write(path, owner, target, kind)
|
|
621
|
+
body = { owner: owner, target: target }
|
|
622
|
+
body[:kind] = kind unless kind.nil?
|
|
623
|
+
j = do_json(:post, path, body)
|
|
624
|
+
PointerWriteResult.new(
|
|
625
|
+
address: j["address"] || "",
|
|
626
|
+
state_id: j["state_id"] || "",
|
|
627
|
+
counter: Antd.parse_pointer_counter("counter", j["counter"])
|
|
628
|
+
)
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
# Parse a pointer finality JSON object. The daemon sends +null+ for every
|
|
632
|
+
# unset optional (+counter+, +state+, +majority+, +transferred_to+); each
|
|
633
|
+
# reads as nil.
|
|
634
|
+
def parse_pointer_finality(j)
|
|
635
|
+
PointerFinality.new(
|
|
636
|
+
address: j["address"] || "",
|
|
637
|
+
group: (j["group"] || 0).to_i,
|
|
638
|
+
answered: (j["answered"] || 0).to_i,
|
|
639
|
+
status: j["status"] || "",
|
|
640
|
+
counter: j["counter"].nil? ? nil : Antd.parse_pointer_counter("finality counter", j["counter"]),
|
|
641
|
+
state: j["state"].is_a?(Hash) ? parse_pointer_final_state(j["state"]) : nil,
|
|
642
|
+
states: (j["states"] || []).grep(Hash).map { |s| parse_pointer_final_state(s) },
|
|
643
|
+
majority: j["majority"].is_a?(Hash) ? parse_pointer_final_state(j["majority"]) : nil,
|
|
644
|
+
transferred_to: presence(j["transferred_to"])
|
|
645
|
+
)
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def parse_pointer_final_state(j)
|
|
649
|
+
PointerFinalState.new(
|
|
650
|
+
state_id: j["state_id"] || "",
|
|
651
|
+
kind: j["kind"] || "",
|
|
652
|
+
kind_tag: (j["kind_tag"] || 0).to_i,
|
|
653
|
+
target: j["target"] || "",
|
|
654
|
+
holders: (j["holders"] || 0).to_i,
|
|
655
|
+
transferred_to: presence(j["transferred_to"])
|
|
656
|
+
)
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
# A wire string, or nil when absent, +null+ or empty.
|
|
660
|
+
def presence(value)
|
|
661
|
+
value.is_a?(String) && !value.empty? ? value : nil
|
|
662
|
+
end
|
|
663
|
+
|
|
392
664
|
# Parse a /v1/upload/finalize JSON response into a FinalizeUploadResult.
|
|
393
665
|
#
|
|
394
666
|
# +data_map_address+ is populated only when prepare was called with
|
|
@@ -483,6 +755,10 @@ module Antd
|
|
|
483
755
|
end
|
|
484
756
|
|
|
485
757
|
# Perform a JSON HTTP request and return the parsed response body.
|
|
758
|
+
#
|
|
759
|
+
# A non-2xx response is raised via +Antd.error_for_response+, which maps
|
|
760
|
+
# the body's +code+ / +error+ (and the PARTIAL_UPLOAD counts) onto the
|
|
761
|
+
# typed +AntdError+ hierarchy.
|
|
486
762
|
def do_json(method, path, body = nil)
|
|
487
763
|
uri = build_uri(path)
|
|
488
764
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
@@ -505,14 +781,7 @@ module Antd
|
|
|
505
781
|
code = response.code.to_i
|
|
506
782
|
|
|
507
783
|
unless (200...300).cover?(code)
|
|
508
|
-
|
|
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)
|
|
784
|
+
raise Antd.error_for_response(code, response.body)
|
|
516
785
|
end
|
|
517
786
|
|
|
518
787
|
return {} if response.body.nil? || response.body.empty?
|
|
@@ -524,9 +793,9 @@ module Antd
|
|
|
524
793
|
# they arrive. Reuses the same base-URL / timeout / error-mapping plumbing
|
|
525
794
|
# as +do_json+, but never buffers the success body in memory.
|
|
526
795
|
#
|
|
527
|
-
# On a non-2xx response the (short) body is read fully
|
|
528
|
-
# +
|
|
529
|
-
#
|
|
796
|
+
# On a non-2xx response the (short) body is read fully and raised via
|
|
797
|
+
# +Antd.error_for_response+ — mirroring +do_json+. On 2xx, chunks are
|
|
798
|
+
# streamed straight to the block.
|
|
530
799
|
def do_stream(method, path, body = nil)
|
|
531
800
|
uri = build_uri(path)
|
|
532
801
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
@@ -550,14 +819,7 @@ module Antd
|
|
|
550
819
|
code = response.code.to_i
|
|
551
820
|
|
|
552
821
|
unless (200...300).cover?(code)
|
|
553
|
-
|
|
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)
|
|
822
|
+
raise Antd.error_for_response(code, response.body)
|
|
561
823
|
end
|
|
562
824
|
|
|
563
825
|
response.read_body do |chunk|
|
|
@@ -599,14 +861,7 @@ module Antd
|
|
|
599
861
|
code = response.code.to_i
|
|
600
862
|
|
|
601
863
|
unless (200...300).cover?(code)
|
|
602
|
-
|
|
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)
|
|
864
|
+
raise Antd.error_for_response(code, response.body)
|
|
610
865
|
end
|
|
611
866
|
|
|
612
867
|
buffer = +""
|