antd 0.1.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 +7 -0
- data/LICENSE-APACHE +201 -0
- data/LICENSE-MIT +21 -0
- data/README.md +184 -0
- data/lib/antd/client.rb +675 -0
- data/lib/antd/discover.rb +132 -0
- data/lib/antd/errors.rb +73 -0
- data/lib/antd/grpc_client.rb +549 -0
- data/lib/antd/models.rb +198 -0
- data/lib/antd/v1/chunks_pb.rb +26 -0
- data/lib/antd/v1/chunks_services_pb.rb +32 -0
- data/lib/antd/v1/common_pb.rb +21 -0
- data/lib/antd/v1/data_pb.rb +31 -0
- data/lib/antd/v1/data_services_pb.rb +36 -0
- data/lib/antd/v1/files_pb.rb +25 -0
- data/lib/antd/v1/files_services_pb.rb +31 -0
- data/lib/antd/v1/health_pb.rb +18 -0
- data/lib/antd/v1/health_services_pb.rb +24 -0
- data/lib/antd/v1/upload_pb.rb +25 -0
- data/lib/antd/v1/upload_services_pb.rb +51 -0
- data/lib/antd/v1/wallet_pb.rb +22 -0
- data/lib/antd/v1/wallet_services_pb.rb +42 -0
- data/lib/antd/version.rb +5 -0
- data/lib/antd.rb +14 -0
- metadata +145 -0
data/lib/antd/client.rb
ADDED
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "base64"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module Antd
|
|
9
|
+
DEFAULT_BASE_URL = "http://localhost:8082"
|
|
10
|
+
DEFAULT_TIMEOUT = 300 # seconds
|
|
11
|
+
|
|
12
|
+
# REST client for the antd daemon.
|
|
13
|
+
class Client
|
|
14
|
+
# Creates a client using port discovery.
|
|
15
|
+
#
|
|
16
|
+
# Reads the daemon.port file to find the REST port. Falls back to the
|
|
17
|
+
# default base URL if the port file is not found.
|
|
18
|
+
#
|
|
19
|
+
# @param kwargs [Hash] options passed to +initialize+ (e.g. +:timeout+)
|
|
20
|
+
# @return [Array(Client, String)] the client and the resolved URL
|
|
21
|
+
def self.auto_discover(**kwargs)
|
|
22
|
+
url = Antd::Discover.daemon_url
|
|
23
|
+
url = DEFAULT_BASE_URL if url.empty?
|
|
24
|
+
[new(base_url: url, **kwargs), url]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param base_url [String] Base URL of the antd daemon
|
|
28
|
+
# @param timeout [Integer] HTTP request timeout in seconds
|
|
29
|
+
def initialize(base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT)
|
|
30
|
+
@base_url = base_url.chomp("/")
|
|
31
|
+
@timeout = timeout
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# --- Health ---
|
|
35
|
+
|
|
36
|
+
# Check daemon status.
|
|
37
|
+
# @return [HealthStatus]
|
|
38
|
+
def health
|
|
39
|
+
j = do_json(:get, "/health")
|
|
40
|
+
HealthStatus.new(
|
|
41
|
+
ok: j["status"] == "ok",
|
|
42
|
+
network: j["network"],
|
|
43
|
+
version: j.fetch("version", ""),
|
|
44
|
+
evm_network: j.fetch("evm_network", ""),
|
|
45
|
+
uptime_seconds: j.fetch("uptime_seconds", 0),
|
|
46
|
+
build_commit: j.fetch("build_commit", ""),
|
|
47
|
+
payment_token_address: j.fetch("payment_token_address", ""),
|
|
48
|
+
payment_vault_address: j.fetch("payment_vault_address", "")
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# --- Data ---
|
|
53
|
+
|
|
54
|
+
# Store private encrypted data on the network. Returns the caller-held
|
|
55
|
+
# DataMap (hex). The DataMap is NOT stored on-network.
|
|
56
|
+
# @param data [String] raw bytes
|
|
57
|
+
# @param payment_mode [String] PaymentMode::AUTO | MERKLE | SINGLE
|
|
58
|
+
# @return [DataPutResult]
|
|
59
|
+
def data_put(data, payment_mode: PaymentMode::AUTO)
|
|
60
|
+
j = do_json(:post, "/v1/data", { data: b64_encode(data), payment_mode: payment_mode })
|
|
61
|
+
DataPutResult.new(
|
|
62
|
+
data_map: j["data_map"] || "",
|
|
63
|
+
chunks_stored: (j["chunks_stored"] || 0).to_i,
|
|
64
|
+
payment_mode_used: j["payment_mode_used"] || ""
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Retrieve private data from a caller-held DataMap (hex).
|
|
69
|
+
# @param data_map [String]
|
|
70
|
+
# @return [String] raw bytes
|
|
71
|
+
def data_get(data_map)
|
|
72
|
+
j = do_json(:post, "/v1/data/get", { data_map: data_map })
|
|
73
|
+
b64_decode(j["data"])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Stream private data from a caller-held DataMap (hex) — the streaming
|
|
77
|
+
# counterpart to +data_get+. Decrypted bytes arrive in chunks, keeping
|
|
78
|
+
# memory usage constant regardless of payload size.
|
|
79
|
+
#
|
|
80
|
+
# When a block is given, each raw byte chunk is yielded as it arrives and
|
|
81
|
+
# the method returns +nil+ once the body is fully consumed. When no block
|
|
82
|
+
# is given, an +Enumerator+ over the chunks is returned (lazy — the HTTP
|
|
83
|
+
# request runs when the enumerator is iterated).
|
|
84
|
+
#
|
|
85
|
+
# @param data_map [String]
|
|
86
|
+
# @yieldparam chunk [String] a raw byte chunk of the decrypted payload
|
|
87
|
+
# @return [nil, Enumerator]
|
|
88
|
+
def data_stream(data_map, &block)
|
|
89
|
+
return enum_for(:data_stream, data_map) unless block_given?
|
|
90
|
+
|
|
91
|
+
do_stream(:post, "/v1/data/stream", { data_map: data_map }, &block)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Like +data_stream+ but opts into NDJSON progress framing
|
|
95
|
+
# (+Accept: application/x-ndjson+), yielding +DownloadFrame+s so the caller
|
|
96
|
+
# can drive a *determinate* progress bar. Each frame is either a plaintext
|
|
97
|
+
# byte chunk (+frame.data+) or a +DownloadProgress+ update
|
|
98
|
+
# (+frame.progress+). The leading +meta+ frame (byte denominator) is parsed
|
|
99
|
+
# and skipped here; a terminal +error+ frame is raised as an SDK error (a
|
|
100
|
+
# raw octet-stream download cannot signal a failure mid-stream).
|
|
101
|
+
#
|
|
102
|
+
# Same block/Enumerator contract as +data_stream+.
|
|
103
|
+
#
|
|
104
|
+
# @param data_map [String]
|
|
105
|
+
# @yieldparam frame [DownloadFrame]
|
|
106
|
+
# @return [nil, Enumerator]
|
|
107
|
+
def data_stream_with_progress(data_map, &block)
|
|
108
|
+
return enum_for(:data_stream_with_progress, data_map) unless block_given?
|
|
109
|
+
|
|
110
|
+
do_stream_ndjson(:post, "/v1/data/stream", { data_map: data_map }, &block)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Store public data. The DataMap is stored on-network as an extra chunk;
|
|
114
|
+
# the returned address is the shareable retrieval handle.
|
|
115
|
+
# @param data [String] raw bytes
|
|
116
|
+
# @param payment_mode [String] PaymentMode::AUTO | MERKLE | SINGLE
|
|
117
|
+
# @return [DataPutPublicResult]
|
|
118
|
+
def data_put_public(data, payment_mode: PaymentMode::AUTO)
|
|
119
|
+
j = do_json(:post, "/v1/data/public", { data: b64_encode(data), payment_mode: payment_mode })
|
|
120
|
+
DataPutPublicResult.new(
|
|
121
|
+
address: j["address"] || "",
|
|
122
|
+
chunks_stored: (j["chunks_stored"] || 0).to_i,
|
|
123
|
+
payment_mode_used: j["payment_mode_used"] || ""
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Retrieve public data by address.
|
|
128
|
+
# @param address [String] hex address
|
|
129
|
+
# @return [String] raw bytes
|
|
130
|
+
def data_get_public(address)
|
|
131
|
+
j = do_json(:get, "/v1/data/public/#{address}")
|
|
132
|
+
b64_decode(j["data"])
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Stream public data by address — the streaming counterpart to
|
|
136
|
+
# +data_get_public+. Decrypted bytes arrive in chunks, keeping memory
|
|
137
|
+
# usage constant regardless of payload size.
|
|
138
|
+
#
|
|
139
|
+
# When a block is given, each raw byte chunk is yielded as it arrives and
|
|
140
|
+
# the method returns +nil+ once the body is fully consumed. When no block
|
|
141
|
+
# is given, an +Enumerator+ over the chunks is returned (lazy — the HTTP
|
|
142
|
+
# request runs when the enumerator is iterated).
|
|
143
|
+
#
|
|
144
|
+
# @param address [String] hex address
|
|
145
|
+
# @yieldparam chunk [String] a raw byte chunk of the payload
|
|
146
|
+
# @return [nil, Enumerator]
|
|
147
|
+
def data_stream_public(address, &block)
|
|
148
|
+
return enum_for(:data_stream_public, address) unless block_given?
|
|
149
|
+
|
|
150
|
+
do_stream(:get, "/v1/data/public/#{address}/stream", nil, &block)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Like +data_stream_public+ but opts into NDJSON progress framing. See
|
|
154
|
+
# +data_stream_with_progress+ for the contract.
|
|
155
|
+
#
|
|
156
|
+
# @param address [String] hex address
|
|
157
|
+
# @yieldparam frame [DownloadFrame]
|
|
158
|
+
# @return [nil, Enumerator]
|
|
159
|
+
def data_stream_public_with_progress(address, &block)
|
|
160
|
+
return enum_for(:data_stream_public_with_progress, address) unless block_given?
|
|
161
|
+
|
|
162
|
+
do_stream_ndjson(:get, "/v1/data/public/#{address}/stream", nil, &block)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Pre-upload cost breakdown for the given bytes.
|
|
166
|
+
# @param data [String] raw bytes
|
|
167
|
+
# @param payment_mode [String] PaymentMode::AUTO | MERKLE | SINGLE
|
|
168
|
+
# @return [UploadCostEstimate]
|
|
169
|
+
def data_cost(data, payment_mode: PaymentMode::AUTO)
|
|
170
|
+
j = do_json(:post, "/v1/data/cost", { data: b64_encode(data), payment_mode: payment_mode })
|
|
171
|
+
UploadCostEstimate.new(
|
|
172
|
+
cost: j["cost"] || "",
|
|
173
|
+
file_size: j["file_size"] || 0,
|
|
174
|
+
chunk_count: j["chunk_count"] || 0,
|
|
175
|
+
estimated_gas_cost_wei: j["estimated_gas_cost_wei"] || "",
|
|
176
|
+
payment_mode: j["payment_mode"] || ""
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# --- Chunks ---
|
|
181
|
+
|
|
182
|
+
# Store a raw chunk on the network.
|
|
183
|
+
# @param data [String] raw bytes
|
|
184
|
+
# @return [PutResult]
|
|
185
|
+
def chunk_put(data)
|
|
186
|
+
j = do_json(:post, "/v1/chunks", { data: b64_encode(data) })
|
|
187
|
+
PutResult.new(cost: j["cost"], address: j["address"])
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Retrieve a chunk by address.
|
|
191
|
+
# @param address [String] hex address
|
|
192
|
+
# @return [String] raw bytes
|
|
193
|
+
def chunk_get(address)
|
|
194
|
+
j = do_json(:get, "/v1/chunks/#{address}")
|
|
195
|
+
b64_decode(j["data"])
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Prepare a single chunk for external-signer publish.
|
|
199
|
+
#
|
|
200
|
+
# Returns either +already_stored: true+ (no payment needed) or a wave-batch
|
|
201
|
+
# payment intent. After the external signer pays, call
|
|
202
|
+
# +finalize_chunk_upload+ with the resulting tx hashes.
|
|
203
|
+
#
|
|
204
|
+
# Unlike +chunk_put+, this method does NOT require the daemon to have a
|
|
205
|
+
# wallet — all funds flow through the external signer.
|
|
206
|
+
#
|
|
207
|
+
# @param data [String] raw chunk bytes
|
|
208
|
+
# @return [PrepareChunkResult]
|
|
209
|
+
def prepare_chunk_upload(data)
|
|
210
|
+
j = do_json(:post, "/v1/chunks/prepare", { data: b64_encode(data) })
|
|
211
|
+
parse_prepare_chunk_response(j)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Submit a prepared chunk to the network after external payment.
|
|
215
|
+
#
|
|
216
|
+
# @param upload_id [String] the upload ID from +prepare_chunk_upload+
|
|
217
|
+
# @param tx_hashes [Hash<String, String>] map of quote_hash to tx_hash
|
|
218
|
+
# @return [String] network address of the stored chunk
|
|
219
|
+
# (matches +PrepareChunkResult#address+)
|
|
220
|
+
def finalize_chunk_upload(upload_id, tx_hashes)
|
|
221
|
+
j = do_json(:post, "/v1/chunks/finalize", {
|
|
222
|
+
upload_id: upload_id,
|
|
223
|
+
tx_hashes: tx_hashes
|
|
224
|
+
})
|
|
225
|
+
j["address"] || ""
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# --- Files ---
|
|
229
|
+
|
|
230
|
+
# Upload a file privately. Returns the caller-held DataMap (hex).
|
|
231
|
+
# @param path [String] local file path
|
|
232
|
+
# @param payment_mode [String] PaymentMode::AUTO | MERKLE | SINGLE
|
|
233
|
+
# @return [FilePutResult]
|
|
234
|
+
def file_put(path, payment_mode: PaymentMode::AUTO)
|
|
235
|
+
j = do_json(:post, "/v1/files", { path: path, payment_mode: payment_mode })
|
|
236
|
+
FilePutResult.new(
|
|
237
|
+
data_map: j["data_map"] || "",
|
|
238
|
+
storage_cost_atto: j["storage_cost_atto"] || "",
|
|
239
|
+
gas_cost_wei: j["gas_cost_wei"] || "",
|
|
240
|
+
chunks_stored: (j["chunks_stored"] || 0).to_i,
|
|
241
|
+
payment_mode_used: j["payment_mode_used"] || ""
|
|
242
|
+
)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Download a private file from a caller-held DataMap into +dest_path+.
|
|
246
|
+
# @param data_map [String]
|
|
247
|
+
# @param dest_path [String]
|
|
248
|
+
# @return [void]
|
|
249
|
+
def file_get(data_map, dest_path)
|
|
250
|
+
do_json(:post, "/v1/files/get", { data_map: data_map, dest_path: dest_path })
|
|
251
|
+
nil
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Upload a file publicly. The DataMap is stored on-network as an extra
|
|
255
|
+
# chunk; the returned address is the shareable retrieval handle.
|
|
256
|
+
# @param path [String] local file path
|
|
257
|
+
# @param payment_mode [String] PaymentMode::AUTO | MERKLE | SINGLE
|
|
258
|
+
# @return [FilePutPublicResult]
|
|
259
|
+
def file_put_public(path, payment_mode: PaymentMode::AUTO)
|
|
260
|
+
j = do_json(:post, "/v1/files/public", { path: path, payment_mode: payment_mode })
|
|
261
|
+
FilePutPublicResult.new(
|
|
262
|
+
address: j["address"] || "",
|
|
263
|
+
storage_cost_atto: j["storage_cost_atto"] || "",
|
|
264
|
+
gas_cost_wei: j["gas_cost_wei"] || "",
|
|
265
|
+
chunks_stored: (j["chunks_stored"] || 0).to_i,
|
|
266
|
+
payment_mode_used: j["payment_mode_used"] || ""
|
|
267
|
+
)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Download a public file from an on-network DataMap address.
|
|
271
|
+
# @param address [String]
|
|
272
|
+
# @param dest_path [String]
|
|
273
|
+
# @return [void]
|
|
274
|
+
def file_get_public(address, dest_path)
|
|
275
|
+
do_json(:post, "/v1/files/public/get", { address: address, dest_path: dest_path })
|
|
276
|
+
nil
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Pre-upload cost breakdown for the file at +path+.
|
|
280
|
+
# @param path [String]
|
|
281
|
+
# @param is_public [Boolean]
|
|
282
|
+
# @param payment_mode [String] PaymentMode::AUTO | MERKLE | SINGLE
|
|
283
|
+
# @return [UploadCostEstimate]
|
|
284
|
+
def file_cost(path, is_public, payment_mode: PaymentMode::AUTO)
|
|
285
|
+
j = do_json(:post, "/v1/files/cost", {
|
|
286
|
+
path: path,
|
|
287
|
+
is_public: is_public,
|
|
288
|
+
payment_mode: payment_mode
|
|
289
|
+
})
|
|
290
|
+
UploadCostEstimate.new(
|
|
291
|
+
cost: j["cost"] || "",
|
|
292
|
+
file_size: j["file_size"] || 0,
|
|
293
|
+
chunk_count: j["chunk_count"] || 0,
|
|
294
|
+
estimated_gas_cost_wei: j["estimated_gas_cost_wei"] || "",
|
|
295
|
+
payment_mode: j["payment_mode"] || ""
|
|
296
|
+
)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# --- Wallet ---
|
|
300
|
+
|
|
301
|
+
# Get the wallet address configured on the daemon.
|
|
302
|
+
# @return [WalletAddress]
|
|
303
|
+
def wallet_address
|
|
304
|
+
j = do_json(:get, "/v1/wallet/address")
|
|
305
|
+
WalletAddress.new(address: j["address"])
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Get the wallet balance and gas balance.
|
|
309
|
+
# @return [WalletBalance]
|
|
310
|
+
def wallet_balance
|
|
311
|
+
j = do_json(:get, "/v1/wallet/balance")
|
|
312
|
+
WalletBalance.new(balance: j["balance"], gas_balance: j["gas_balance"])
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Approve the wallet to spend tokens on payment contracts (one-time operation).
|
|
316
|
+
# @return [Boolean]
|
|
317
|
+
def wallet_approve
|
|
318
|
+
j = do_json(:post, "/v1/wallet/approve", {})
|
|
319
|
+
j["approved"] == true
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# --- External Signer (Two-Phase Upload) ---
|
|
323
|
+
|
|
324
|
+
# Prepare a file upload for external signing.
|
|
325
|
+
#
|
|
326
|
+
# @param path [String] local file path
|
|
327
|
+
# @param visibility [String, nil] +"public"+ to bundle the DataMap chunk
|
|
328
|
+
# into the same external-signer payment batch (the resulting
|
|
329
|
+
# +data_map_address+ on finalize is the shareable retrieval handle).
|
|
330
|
+
# +"private"+ or +nil+ keeps the existing private-only behaviour. When
|
|
331
|
+
# +nil+, the +visibility+ JSON field is omitted entirely to preserve
|
|
332
|
+
# the pre-public daemon wire shape.
|
|
333
|
+
# @return [PrepareUploadResult]
|
|
334
|
+
def prepare_upload(path, visibility: nil)
|
|
335
|
+
body = { path: path }
|
|
336
|
+
body[:visibility] = visibility unless visibility.nil?
|
|
337
|
+
j = do_json(:post, "/v1/upload/prepare", body)
|
|
338
|
+
parse_prepare_response(j)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Convenience wrapper: prepare a *public* file upload for external signing.
|
|
342
|
+
#
|
|
343
|
+
# Equivalent to +prepare_upload(path, visibility: "public")+. In addition
|
|
344
|
+
# to the data chunks, the daemon bundles the serialized DataMap chunk into
|
|
345
|
+
# the same payment batch — the external signer signs ONE EVM transaction
|
|
346
|
+
# covering chunks + DataMap. After +finalize_upload+, the result's
|
|
347
|
+
# +data_map_address+ is the shareable retrieval handle.
|
|
348
|
+
#
|
|
349
|
+
# @param path [String] local file path
|
|
350
|
+
# @return [PrepareUploadResult]
|
|
351
|
+
def prepare_upload_public(path)
|
|
352
|
+
prepare_upload(path, visibility: "public")
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Prepare a data upload for external signing.
|
|
356
|
+
# Takes raw bytes, base64-encodes them, and POSTs to /v1/data/prepare.
|
|
357
|
+
# @param data [String] raw bytes to upload
|
|
358
|
+
# @return [PrepareUploadResult]
|
|
359
|
+
def prepare_data_upload(data)
|
|
360
|
+
j = do_json(:post, "/v1/data/prepare", { data: b64_encode(data) })
|
|
361
|
+
parse_prepare_response(j)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Finalize an upload after an external signer has submitted payment transactions.
|
|
365
|
+
# @param upload_id [String] the upload ID from prepare_upload
|
|
366
|
+
# @param tx_hashes [Hash<String, String>] map of quote_hash to tx_hash
|
|
367
|
+
# @return [FinalizeUploadResult]
|
|
368
|
+
def finalize_upload(upload_id, tx_hashes)
|
|
369
|
+
j = do_json(:post, "/v1/upload/finalize", {
|
|
370
|
+
upload_id: upload_id,
|
|
371
|
+
tx_hashes: tx_hashes
|
|
372
|
+
})
|
|
373
|
+
parse_finalize_response(j)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Finalize a merkle-batch upload after selecting a winning pool.
|
|
377
|
+
# @param upload_id [String] the upload ID from prepare_upload
|
|
378
|
+
# @param winner_pool_hash [String] hash of the winning pool commitment
|
|
379
|
+
# @param store_data_map [Boolean] whether to store the data map on-network
|
|
380
|
+
# @return [FinalizeUploadResult]
|
|
381
|
+
def finalize_merkle_upload(upload_id, winner_pool_hash, store_data_map: false)
|
|
382
|
+
j = do_json(:post, "/v1/upload/finalize", {
|
|
383
|
+
upload_id: upload_id,
|
|
384
|
+
winner_pool_hash: winner_pool_hash,
|
|
385
|
+
store_data_map: store_data_map
|
|
386
|
+
})
|
|
387
|
+
parse_finalize_response(j)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
private
|
|
391
|
+
|
|
392
|
+
# Parse a /v1/upload/finalize JSON response into a FinalizeUploadResult.
|
|
393
|
+
#
|
|
394
|
+
# +data_map_address+ is populated only when prepare was called with
|
|
395
|
+
# visibility="public" — the DataMap chunk was paid + stored in the same
|
|
396
|
+
# external-signer batch.
|
|
397
|
+
def parse_finalize_response(j)
|
|
398
|
+
FinalizeUploadResult.new(
|
|
399
|
+
address: j["address"] || "",
|
|
400
|
+
chunks_stored: (j["chunks_stored"] || 0).to_i,
|
|
401
|
+
data_map: j["data_map"] || "",
|
|
402
|
+
data_map_address: j["data_map_address"] || ""
|
|
403
|
+
)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Parse a /v1/chunks/prepare JSON response into a PrepareChunkResult.
|
|
407
|
+
def parse_prepare_chunk_response(j)
|
|
408
|
+
payments = (j["payments"] || []).map do |p|
|
|
409
|
+
PaymentInfo.new(
|
|
410
|
+
quote_hash: p["quote_hash"],
|
|
411
|
+
rewards_address: p["rewards_address"],
|
|
412
|
+
amount: p["amount"]
|
|
413
|
+
)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
PrepareChunkResult.new(
|
|
417
|
+
address: j["address"] || "",
|
|
418
|
+
already_stored: j["already_stored"] == true,
|
|
419
|
+
upload_id: j["upload_id"] || "",
|
|
420
|
+
payment_type: j["payment_type"] || "",
|
|
421
|
+
payments: payments,
|
|
422
|
+
total_amount: j["total_amount"] || "",
|
|
423
|
+
payment_vault_address: j["payment_vault_address"] || "",
|
|
424
|
+
payment_token_address: j["payment_token_address"] || "",
|
|
425
|
+
rpc_url: j["rpc_url"] || ""
|
|
426
|
+
)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Parse a prepare-upload JSON response into a PrepareUploadResult.
|
|
430
|
+
def parse_prepare_response(j)
|
|
431
|
+
payment_type = j["payment_type"] || "wave_batch"
|
|
432
|
+
|
|
433
|
+
payments = (j["payments"] || []).map do |p|
|
|
434
|
+
PaymentInfo.new(
|
|
435
|
+
quote_hash: p["quote_hash"],
|
|
436
|
+
rewards_address: p["rewards_address"],
|
|
437
|
+
amount: p["amount"]
|
|
438
|
+
)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
pool_commitments = []
|
|
442
|
+
if payment_type == "merkle_batch"
|
|
443
|
+
(j["pool_commitments"] || []).each do |pc|
|
|
444
|
+
candidates = (pc["candidates"] || []).map do |c|
|
|
445
|
+
CandidateNodeEntry.new(
|
|
446
|
+
rewards_address: c["rewards_address"] || "",
|
|
447
|
+
amount: c["amount"] || ""
|
|
448
|
+
)
|
|
449
|
+
end
|
|
450
|
+
pool_commitments << PoolCommitmentEntry.new(
|
|
451
|
+
pool_hash: pc["pool_hash"] || "",
|
|
452
|
+
candidates: candidates
|
|
453
|
+
)
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
PrepareUploadResult.new(
|
|
458
|
+
upload_id: j["upload_id"] || "",
|
|
459
|
+
payments: payments,
|
|
460
|
+
total_amount: j["total_amount"] || "",
|
|
461
|
+
payment_vault_address: j["payment_vault_address"] || "",
|
|
462
|
+
payment_token_address: j["payment_token_address"] || "",
|
|
463
|
+
rpc_url: j["rpc_url"] || "",
|
|
464
|
+
payment_type: payment_type,
|
|
465
|
+
depth: j["depth"] || 0,
|
|
466
|
+
pool_commitments: pool_commitments,
|
|
467
|
+
merkle_payment_timestamp: j["merkle_payment_timestamp"] || 0,
|
|
468
|
+
total_chunks: j["total_chunks"] || 0,
|
|
469
|
+
already_stored_count: j["already_stored_count"] || 0
|
|
470
|
+
)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def b64_encode(data)
|
|
474
|
+
Base64.strict_encode64(data)
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def b64_decode(str)
|
|
478
|
+
Base64.strict_decode64(str)
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def build_uri(path)
|
|
482
|
+
URI("#{@base_url}#{path}")
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# Perform a JSON HTTP request and return the parsed response body.
|
|
486
|
+
def do_json(method, path, body = nil)
|
|
487
|
+
uri = build_uri(path)
|
|
488
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
489
|
+
http.use_ssl = (uri.scheme == "https")
|
|
490
|
+
http.open_timeout = @timeout
|
|
491
|
+
http.read_timeout = @timeout
|
|
492
|
+
|
|
493
|
+
request = case method
|
|
494
|
+
when :get then Net::HTTP::Get.new(uri)
|
|
495
|
+
when :post then Net::HTTP::Post.new(uri)
|
|
496
|
+
when :put then Net::HTTP::Put.new(uri)
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
if body
|
|
500
|
+
request["Content-Type"] = "application/json"
|
|
501
|
+
request.body = JSON.generate(body)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
response = http.request(request)
|
|
505
|
+
code = response.code.to_i
|
|
506
|
+
|
|
507
|
+
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)
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
return {} if response.body.nil? || response.body.empty?
|
|
519
|
+
|
|
520
|
+
JSON.parse(response.body)
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
# Perform a streaming HTTP request, yielding raw body chunks to +block+ as
|
|
524
|
+
# they arrive. Reuses the same base-URL / timeout / error-mapping plumbing
|
|
525
|
+
# as +do_json+, but never buffers the success body in memory.
|
|
526
|
+
#
|
|
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.
|
|
530
|
+
def do_stream(method, path, body = nil)
|
|
531
|
+
uri = build_uri(path)
|
|
532
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
533
|
+
http.use_ssl = (uri.scheme == "https")
|
|
534
|
+
http.open_timeout = @timeout
|
|
535
|
+
http.read_timeout = @timeout
|
|
536
|
+
|
|
537
|
+
request = case method
|
|
538
|
+
when :get then Net::HTTP::Get.new(uri)
|
|
539
|
+
when :post then Net::HTTP::Post.new(uri)
|
|
540
|
+
when :put then Net::HTTP::Put.new(uri)
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
if body
|
|
544
|
+
request["Content-Type"] = "application/json"
|
|
545
|
+
request.body = JSON.generate(body)
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
http.start do
|
|
549
|
+
http.request(request) do |response|
|
|
550
|
+
code = response.code.to_i
|
|
551
|
+
|
|
552
|
+
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)
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
response.read_body do |chunk|
|
|
564
|
+
yield chunk unless chunk.empty?
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
nil
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# Like +do_stream+ but opts into NDJSON progress framing by sending
|
|
573
|
+
# +Accept: application/x-ndjson+, splitting the streamed body into lines
|
|
574
|
+
# (buffering partial lines across byte-chunk boundaries) and yielding a
|
|
575
|
+
# parsed +DownloadFrame+ per non-skipped line. A terminal +error+ frame is
|
|
576
|
+
# raised via +Antd.error_for_status+. Non-2xx responses are handled exactly
|
|
577
|
+
# as in +do_stream+.
|
|
578
|
+
def do_stream_ndjson(method, path, body = nil)
|
|
579
|
+
uri = build_uri(path)
|
|
580
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
581
|
+
http.use_ssl = (uri.scheme == "https")
|
|
582
|
+
http.open_timeout = @timeout
|
|
583
|
+
http.read_timeout = @timeout
|
|
584
|
+
|
|
585
|
+
request = case method
|
|
586
|
+
when :get then Net::HTTP::Get.new(uri)
|
|
587
|
+
when :post then Net::HTTP::Post.new(uri)
|
|
588
|
+
when :put then Net::HTTP::Put.new(uri)
|
|
589
|
+
end
|
|
590
|
+
request["Accept"] = "application/x-ndjson"
|
|
591
|
+
|
|
592
|
+
if body
|
|
593
|
+
request["Content-Type"] = "application/json"
|
|
594
|
+
request.body = JSON.generate(body)
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
http.start do
|
|
598
|
+
http.request(request) do |response|
|
|
599
|
+
code = response.code.to_i
|
|
600
|
+
|
|
601
|
+
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)
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
buffer = +""
|
|
613
|
+
response.read_body do |chunk|
|
|
614
|
+
buffer << chunk
|
|
615
|
+
while (nl = buffer.index("\n"))
|
|
616
|
+
line = buffer.slice!(0..nl)
|
|
617
|
+
frame = parse_ndjson_frame(line)
|
|
618
|
+
yield frame unless frame.nil?
|
|
619
|
+
end
|
|
620
|
+
end
|
|
621
|
+
# Flush a trailing line with no terminating newline.
|
|
622
|
+
unless buffer.empty?
|
|
623
|
+
frame = parse_ndjson_frame(buffer)
|
|
624
|
+
yield frame unless frame.nil?
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
nil
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# Parse one NDJSON download line into a +DownloadFrame+. Maps the leading
|
|
633
|
+
# +meta+ frame to a byte-total +DownloadFrame+; returns +nil+ for blank
|
|
634
|
+
# lines and unknown frame types (forward-compat). Raises via
|
|
635
|
+
# +Antd.error_for_status+ on a terminal +error+ frame, which a raw
|
|
636
|
+
# octet-stream download cannot signal mid-stream.
|
|
637
|
+
def parse_ndjson_frame(line)
|
|
638
|
+
line = line.strip
|
|
639
|
+
return nil if line.empty?
|
|
640
|
+
|
|
641
|
+
obj = JSON.parse(line)
|
|
642
|
+
case obj["type"]
|
|
643
|
+
when "data"
|
|
644
|
+
DownloadFrame.new(data: b64_decode(obj["chunk"] || ""))
|
|
645
|
+
when "progress"
|
|
646
|
+
DownloadFrame.new(progress: DownloadProgress.new(
|
|
647
|
+
phase: obj["phase"] || "",
|
|
648
|
+
fetched: (obj["fetched"] || 0).to_i,
|
|
649
|
+
total: (obj["total"] || 0).to_i
|
|
650
|
+
))
|
|
651
|
+
when "error"
|
|
652
|
+
raise Antd.error_for_status(500, obj["message"] || "stream error")
|
|
653
|
+
when "meta"
|
|
654
|
+
# The leading "meta" frame carries the byte denominator (total_size).
|
|
655
|
+
DownloadFrame.new(meta: (obj["total_size"] || 0).to_i)
|
|
656
|
+
else
|
|
657
|
+
# Unknown frame types are ignored for forward compatibility.
|
|
658
|
+
nil
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
# Perform an HTTP HEAD request and return the status code.
|
|
663
|
+
def do_head(path)
|
|
664
|
+
uri = build_uri(path)
|
|
665
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
666
|
+
http.use_ssl = (uri.scheme == "https")
|
|
667
|
+
http.open_timeout = @timeout
|
|
668
|
+
http.read_timeout = @timeout
|
|
669
|
+
|
|
670
|
+
request = Net::HTTP::Head.new(uri)
|
|
671
|
+
response = http.request(request)
|
|
672
|
+
response.code.to_i
|
|
673
|
+
end
|
|
674
|
+
end
|
|
675
|
+
end
|