antd 0.2.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 +143 -9
- data/lib/antd/client.rb +242 -0
- data/lib/antd/errors.rb +133 -4
- data/lib/antd/grpc_client.rb +192 -1
- 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/data_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/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
|
|
@@ -96,6 +93,16 @@ the daemon sent — start with `Partial upload:` becomes
|
|
|
96
93
|
`ABORTED`, including one that only mentions `Partial upload:` later in its
|
|
97
94
|
text, stays a generic `Antd::AntdError`).
|
|
98
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`.
|
|
105
|
+
|
|
99
106
|
> **Note:** Wallet operations (address, balance, approve) and payment_mode are available via REST only.
|
|
100
107
|
|
|
101
108
|
## Prerequisites
|
|
@@ -153,6 +160,62 @@ client = Antd::Client.new(base_url: "http://custom-host:9090", timeout: 30)
|
|
|
153
160
|
| `file_get_public(address, dest_path)` | Download a public file by address |
|
|
154
161
|
| `file_cost(path, is_public, payment_mode: :auto)` | Estimate upload cost — returns `UploadCostEstimate` with size, chunks, gas, payment mode |
|
|
155
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
|
+
|
|
156
219
|
## Error Handling
|
|
157
220
|
|
|
158
221
|
All errors inherit from `Antd::AntdError` and can be caught by type:
|
|
@@ -180,6 +243,18 @@ end
|
|
|
180
243
|
| `InternalError` | 500 | Server error |
|
|
181
244
|
| `NetworkError` | 502 | Network unreachable |
|
|
182
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.
|
|
183
258
|
|
|
184
259
|
### Partial uploads
|
|
185
260
|
|
|
@@ -256,6 +331,63 @@ See [`docs/external-signer-flow.md` section 6](../docs/external-signer-flow.md#6
|
|
|
256
331
|
for the daemon contract and `examples/07_external_signer.rb` for a
|
|
257
332
|
`finalize_with_retry` helper.
|
|
258
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
|
+
```
|
|
390
|
+
|
|
259
391
|
## Examples
|
|
260
392
|
|
|
261
393
|
See the [examples/](examples/) directory:
|
|
@@ -266,3 +398,5 @@ See the [examples/](examples/) directory:
|
|
|
266
398
|
- `04_files.rb` — File upload and download
|
|
267
399
|
- `06_private_data.rb` — Private data put/get
|
|
268
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
|
@@ -417,8 +417,250 @@ module Antd
|
|
|
417
417
|
parse_finalize_response(j)
|
|
418
418
|
end
|
|
419
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
|
+
|
|
420
616
|
private
|
|
421
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
|
+
|
|
422
664
|
# Parse a /v1/upload/finalize JSON response into a FinalizeUploadResult.
|
|
423
665
|
#
|
|
424
666
|
# +data_map_address+ is populated only when prepare was called with
|
data/lib/antd/errors.rb
CHANGED
|
@@ -58,6 +58,66 @@ module Antd
|
|
|
58
58
|
def initialize(message) = super(message, status_code: 503)
|
|
59
59
|
end
|
|
60
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
|
+
|
|
61
121
|
# A finalize stored some chunks while others remained unstored after the
|
|
62
122
|
# daemon's retries (HTTP 502 with +code: "PARTIAL_UPLOAD"+; gRPC ABORTED).
|
|
63
123
|
# The on-chain payment persists and the stored chunks stay on the network.
|
|
@@ -222,13 +282,77 @@ module Antd
|
|
|
222
282
|
}
|
|
223
283
|
end
|
|
224
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
|
+
|
|
225
346
|
# Maps a non-2xx REST response onto a typed error, preferring the body's
|
|
226
347
|
# machine-readable +code+ over the bare HTTP status where they diverge:
|
|
227
348
|
# +PARTIAL_UPLOAD+ arrives as a 502 that would otherwise read as a generic
|
|
228
|
-
# +NetworkError
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
#
|
|
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.
|
|
232
356
|
#
|
|
233
357
|
# Never raises on a malformed body: every field is type-checked, not
|
|
234
358
|
# coerced. Only a +code+ that is exactly the string "PARTIAL_UPLOAD"
|
|
@@ -264,6 +388,11 @@ module Antd
|
|
|
264
388
|
retention_known: [true, false].include?(parsed["retryable"])
|
|
265
389
|
)
|
|
266
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
|
|
267
396
|
end
|
|
268
397
|
|
|
269
398
|
error_for_status(code, message)
|