diff --git a/antd-ruby/lib/antd/grpc_client.rb b/antd-ruby/lib/antd/grpc_client.rb index 2463dc1d..4d192541 100644 --- a/antd-ruby/lib/antd/grpc_client.rb +++ b/antd-ruby/lib/antd/grpc_client.rb @@ -6,7 +6,7 @@ # -I../../antd/proto \ # --ruby_out=lib --grpc_out=lib \ # antd/v1/common.proto antd/v1/health.proto antd/v1/data.proto \ -# antd/v1/chunks.proto antd/v1/files.proto +# antd/v1/chunks.proto antd/v1/files.proto antd/v1/upload.proto # # The generated files are expected under lib/antd/v1/. @@ -15,6 +15,7 @@ require_relative "v1/data_services_pb" require_relative "v1/chunks_services_pb" require_relative "v1/files_services_pb" +require_relative "v1/upload_services_pb" module Antd DEFAULT_GRPC_TARGET = "localhost:50051" @@ -43,6 +44,7 @@ def initialize(target: DEFAULT_GRPC_TARGET) @data_stub = Antd::V1::DataService::Stub.new(target, :this_channel_is_insecure) @chunk_stub = Antd::V1::ChunkService::Stub.new(target, :this_channel_is_insecure) @file_stub = Antd::V1::FileService::Stub.new(target, :this_channel_is_insecure) + @upload_stub = Antd::V1::UploadService::Stub.new(target, :this_channel_is_insecure) end # --- Health --- @@ -214,8 +216,174 @@ def file_cost(path, is_public, payment_mode: PaymentMode::AUTO) ) end + # --- External Signer (chunks) --- + + # Prepare a single chunk for external-signer publish. + # + # When the chunk is already on-network the result has + # +already_stored: true+ and the caller can skip +finalize_chunk_upload+ + # entirely. + # + # @param data [String] raw chunk bytes + # @return [PrepareChunkResult] + def prepare_chunk_upload(data) + req = Antd::V1::PrepareChunkRequest.new(data: data.b) + resp = grpc_call { @chunk_stub.prepare_chunk(req) } + PrepareChunkResult.new( + address: resp.address, + already_stored: resp.already_stored, + upload_id: resp.upload_id, + payment_type: resp.payment_type, + payments: resp.payments.map { |p| + PaymentInfo.new( + quote_hash: p.quote_hash, + rewards_address: p.rewards_address, + amount: p.amount + ) + }, + total_amount: resp.total_amount, + payment_vault_address: resp.payment_vault_address, + payment_token_address: resp.payment_token_address, + rpc_url: resp.rpc_url + ) + end + + # Submit a prepared chunk after external payment. Returns the chunk address. + # + # @param upload_id [String] + # @param tx_hashes [Hash] + # @return [String] hex chunk address + def finalize_chunk_upload(upload_id, tx_hashes) + req = Antd::V1::FinalizeChunkRequest.new( + upload_id: upload_id, + tx_hashes: tx_hashes + ) + resp = grpc_call { @chunk_stub.finalize_chunk(req) } + resp.address + end + + # --- External Signer (uploads) --- + + # Prepare a file upload for external signing. + # + # @param path [String] local file path on the daemon host + # @param visibility [String, nil] +"private"+ (default when nil) or + # +"public"+ to bundle the DataMap chunk into the same external-signer + # payment batch + # @return [PrepareUploadResult] + def prepare_upload(path, visibility: nil) + req = Antd::V1::PrepareFileUploadRequest.new( + path: path, + visibility: visibility.to_s + ) + resp = grpc_call { @upload_stub.prepare_file_upload(req) } + build_prepare_upload_result(resp) + end + + # Convenience wrapper for +prepare_upload(path, visibility: "public")+. + # + # @param path [String] + # @return [PrepareUploadResult] + def prepare_upload_public(path) + prepare_upload(path, visibility: "public") + end + + # Prepare an in-memory data upload for external signing. + # + # @param data [String] raw bytes + # @param visibility [String, nil] same semantics as +prepare_upload+ + # @return [PrepareUploadResult] + def prepare_data_upload(data, visibility: nil) + req = Antd::V1::PrepareDataUploadRequest.new( + data: data.b, + visibility: visibility.to_s + ) + resp = grpc_call { @upload_stub.prepare_data_upload(req) } + build_prepare_upload_result(resp) + end + + # Finalize a wave-batch upload after external payment. + # + # @param upload_id [String] + # @param tx_hashes [Hash] + # @return [FinalizeUploadResult] + def finalize_upload(upload_id, tx_hashes) + req = Antd::V1::FinalizeUploadRequest.new( + upload_id: upload_id, + tx_hashes: tx_hashes + ) + resp = grpc_call { @upload_stub.finalize_upload(req) } + FinalizeUploadResult.new( + address: resp.address, + chunks_stored: resp.chunks_stored.to_i, + data_map: resp.data_map, + data_map_address: resp.data_map_address + ) + end + + # Finalize a merkle-batch upload after the winning pool has been + # determined. + # + # @param upload_id [String] + # @param winner_pool_hash [String] + # @param store_data_map [Boolean] + # @return [FinalizeUploadResult] + def finalize_merkle_upload(upload_id, winner_pool_hash, store_data_map: false) + req = Antd::V1::FinalizeUploadRequest.new( + upload_id: upload_id, + winner_pool_hash: winner_pool_hash, + store_data_map: store_data_map + ) + resp = grpc_call { @upload_stub.finalize_upload(req) } + FinalizeUploadResult.new( + address: resp.address, + chunks_stored: resp.chunks_stored.to_i, + data_map: resp.data_map, + data_map_address: resp.data_map_address + ) + end + private + # Maps a PrepareUploadResponse proto into a +PrepareUploadResult+ struct, + # populating the merkle-only fields (+depth+, +pool_commitments+, + # +merkle_payment_timestamp+) only when +payment_type+ is +"merkle"+. + def build_prepare_upload_result(resp) + is_merkle = resp.payment_type == "merkle" + pool_commitments = if is_merkle + resp.pool_commitments.map { |pc| + PoolCommitmentEntry.new( + pool_hash: pc.pool_hash, + candidates: pc.candidates.map { |c| + CandidateNodeEntry.new( + rewards_address: c.rewards_address, + amount: c.amount + ) + } + ) + } + end + PrepareUploadResult.new( + upload_id: resp.upload_id, + payments: resp.payments.map { |p| + PaymentInfo.new( + quote_hash: p.quote_hash, + rewards_address: p.rewards_address, + amount: p.amount + ) + }, + total_amount: resp.total_amount, + payment_vault_address: resp.payment_vault_address, + payment_token_address: resp.payment_token_address, + rpc_url: resp.rpc_url, + payment_type: resp.payment_type, + depth: is_merkle ? resp.depth : nil, + pool_commitments: pool_commitments, + merkle_payment_timestamp: is_merkle ? resp.merkle_payment_timestamp.to_i : nil + ) + end + + # Executes a gRPC call and translates errors to Antd error types. def grpc_call yield diff --git a/antd-ruby/lib/antd/v1/chunks_pb.rb b/antd-ruby/lib/antd/v1/chunks_pb.rb index e0ca5553..b55491d2 100644 --- a/antd-ruby/lib/antd/v1/chunks_pb.rb +++ b/antd-ruby/lib/antd/v1/chunks_pb.rb @@ -7,7 +7,7 @@ require 'antd/v1/common_pb' -descriptor_data = "\n\x14\x61ntd/v1/chunks.proto\x12\x07\x61ntd.v1\x1a\x14\x61ntd/v1/common.proto\"\"\n\x0fGetChunkRequest\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\" \n\x10GetChunkResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\x1f\n\x0fPutChunkRequest\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"@\n\x10PutChunkResponse\x12\x1b\n\x04\x63ost\x18\x01 \x01(\x0b\x32\r.antd.v1.Cost\x12\x0f\n\x07\x61\x64\x64ress\x18\x02 \x01(\t2\x86\x01\n\x0c\x43hunkService\x12:\n\x03Get\x12\x18.antd.v1.GetChunkRequest\x1a\x19.antd.v1.GetChunkResponse\x12:\n\x03Put\x12\x18.antd.v1.PutChunkRequest\x1a\x19.antd.v1.PutChunkResponseBDZ8github.com/WithAutonomi/ant-sdk/antd-go/proto/antd/v1;v1\xaa\x02\x07\x41ntd.V1b\x06proto3" +descriptor_data = "\n\x14\x61ntd/v1/chunks.proto\x12\x07\x61ntd.v1\x1a\x14\x61ntd/v1/common.proto\"\"\n\x0fGetChunkRequest\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\" \n\x10GetChunkResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\x1f\n\x0fPutChunkRequest\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"@\n\x10PutChunkResponse\x12\x1b\n\x04\x63ost\x18\x01 \x01(\x0b\x32\r.antd.v1.Cost\x12\x0f\n\x07\x61\x64\x64ress\x18\x02 \x01(\t\"#\n\x13PrepareChunkRequest\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\xf6\x01\n\x14PrepareChunkResponse\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x16\n\x0e\x61lready_stored\x18\x02 \x01(\x08\x12\x11\n\tupload_id\x18\x03 \x01(\t\x12\x14\n\x0cpayment_type\x18\x04 \x01(\t\x12\'\n\x08payments\x18\x05 \x03(\x0b\x32\x15.antd.v1.PaymentEntry\x12\x14\n\x0ctotal_amount\x18\x06 \x01(\t\x12\x1d\n\x15payment_vault_address\x18\x07 \x01(\t\x12\x1d\n\x15payment_token_address\x18\x08 \x01(\t\x12\x0f\n\x07rpc_url\x18\t \x01(\t\"\x9a\x01\n\x14\x46inalizeChunkRequest\x12\x11\n\tupload_id\x18\x01 \x01(\t\x12>\n\ttx_hashes\x18\x02 \x03(\x0b\x32+.antd.v1.FinalizeChunkRequest.TxHashesEntry\x1a/\n\rTxHashesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"(\n\x15\x46inalizeChunkResponse\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t2\xa3\x02\n\x0c\x43hunkService\x12:\n\x03Get\x12\x18.antd.v1.GetChunkRequest\x1a\x19.antd.v1.GetChunkResponse\x12:\n\x03Put\x12\x18.antd.v1.PutChunkRequest\x1a\x19.antd.v1.PutChunkResponse\x12K\n\x0cPrepareChunk\x12\x1c.antd.v1.PrepareChunkRequest\x1a\x1d.antd.v1.PrepareChunkResponse\x12N\n\rFinalizeChunk\x12\x1d.antd.v1.FinalizeChunkRequest\x1a\x1e.antd.v1.FinalizeChunkResponseBDZ8github.com/WithAutonomi/ant-sdk/antd-go/proto/antd/v1;v1\xaa\x02\x07\x41ntd.V1b\x06proto3" pool = ::Google::Protobuf::DescriptorPool.generated_pool pool.add_serialized_file(descriptor_data) @@ -18,5 +18,9 @@ module V1 GetChunkResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.GetChunkResponse").msgclass PutChunkRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.PutChunkRequest").msgclass PutChunkResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.PutChunkResponse").msgclass + PrepareChunkRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.PrepareChunkRequest").msgclass + PrepareChunkResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.PrepareChunkResponse").msgclass + FinalizeChunkRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.FinalizeChunkRequest").msgclass + FinalizeChunkResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.FinalizeChunkResponse").msgclass end end diff --git a/antd-ruby/lib/antd/v1/chunks_services_pb.rb b/antd-ruby/lib/antd/v1/chunks_services_pb.rb index 2da23bf8..7b2c62dc 100644 --- a/antd-ruby/lib/antd/v1/chunks_services_pb.rb +++ b/antd-ruby/lib/antd/v1/chunks_services_pb.rb @@ -17,6 +17,13 @@ class Service rpc :Get, ::Antd::V1::GetChunkRequest, ::Antd::V1::GetChunkResponse rpc :Put, ::Antd::V1::PutChunkRequest, ::Antd::V1::PutChunkResponse + # External-signer single-chunk publish. Mirrors REST + # `/v1/chunks/prepare` + `/v1/chunks/finalize`. Single chunks are always + # below the merkle threshold, so the payment shape is always wave-batch. + # When the chunk is already on-network, the prepare response has + # `already_stored = true` and the caller can skip the finalize step. + rpc :PrepareChunk, ::Antd::V1::PrepareChunkRequest, ::Antd::V1::PrepareChunkResponse + rpc :FinalizeChunk, ::Antd::V1::FinalizeChunkRequest, ::Antd::V1::FinalizeChunkResponse end Stub = Service.rpc_stub_class diff --git a/antd-ruby/lib/antd/v1/common_pb.rb b/antd-ruby/lib/antd/v1/common_pb.rb index f18db769..4a46ae54 100644 --- a/antd-ruby/lib/antd/v1/common_pb.rb +++ b/antd-ruby/lib/antd/v1/common_pb.rb @@ -5,7 +5,7 @@ require 'google/protobuf' -descriptor_data = "\n\x14\x61ntd/v1/common.proto\x12\x07\x61ntd.v1\"y\n\x04\x43ost\x12\x13\n\x0b\x61tto_tokens\x18\x01 \x01(\t\x12\x11\n\tfile_size\x18\x02 \x01(\x04\x12\x13\n\x0b\x63hunk_count\x18\x03 \x01(\r\x12\x1e\n\x16\x65stimated_gas_cost_wei\x18\x04 \x01(\t\x12\x14\n\x0cpayment_mode\x18\x05 \x01(\t\"\x16\n\x07\x41\x64\x64ress\x12\x0b\n\x03hex\x18\x01 \x01(\t\"\x1d\n\x0ePublicKeyProto\x12\x0b\n\x03hex\x18\x01 \x01(\t\"\x1d\n\x0eSecretKeyProto\x12\x0b\n\x03hex\x18\x01 \x01(\tBDZ8github.com/WithAutonomi/ant-sdk/antd-go/proto/antd/v1;v1\xaa\x02\x07\x41ntd.V1b\x06proto3" +descriptor_data = "\n\x14\x61ntd/v1/common.proto\x12\x07\x61ntd.v1\"y\n\x04\x43ost\x12\x13\n\x0b\x61tto_tokens\x18\x01 \x01(\t\x12\x11\n\tfile_size\x18\x02 \x01(\x04\x12\x13\n\x0b\x63hunk_count\x18\x03 \x01(\r\x12\x1e\n\x16\x65stimated_gas_cost_wei\x18\x04 \x01(\t\x12\x14\n\x0cpayment_mode\x18\x05 \x01(\t\"\x16\n\x07\x41\x64\x64ress\x12\x0b\n\x03hex\x18\x01 \x01(\t\"\x1d\n\x0ePublicKeyProto\x12\x0b\n\x03hex\x18\x01 \x01(\t\"\x1d\n\x0eSecretKeyProto\x12\x0b\n\x03hex\x18\x01 \x01(\t\"K\n\x0cPaymentEntry\x12\x12\n\nquote_hash\x18\x01 \x01(\t\x12\x17\n\x0frewards_address\x18\x02 \x01(\t\x12\x0e\n\x06\x61mount\x18\x03 \x01(\tBDZ8github.com/WithAutonomi/ant-sdk/antd-go/proto/antd/v1;v1\xaa\x02\x07\x41ntd.V1b\x06proto3" pool = ::Google::Protobuf::DescriptorPool.generated_pool pool.add_serialized_file(descriptor_data) @@ -16,5 +16,6 @@ module V1 Address = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.Address").msgclass PublicKeyProto = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.PublicKeyProto").msgclass SecretKeyProto = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.SecretKeyProto").msgclass + PaymentEntry = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.PaymentEntry").msgclass end end diff --git a/antd-ruby/lib/antd/v1/upload_pb.rb b/antd-ruby/lib/antd/v1/upload_pb.rb new file mode 100644 index 00000000..e75766f4 --- /dev/null +++ b/antd-ruby/lib/antd/v1/upload_pb.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: antd/v1/upload.proto + +require 'google/protobuf' + +require 'antd/v1/common_pb' + + +descriptor_data = "\n\x14\x61ntd/v1/upload.proto\x12\x07\x61ntd.v1\x1a\x14\x61ntd/v1/common.proto\"<\n\x18PrepareFileUploadRequest\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x12\n\nvisibility\x18\x02 \x01(\t\"<\n\x18PrepareDataUploadRequest\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x12\n\nvisibility\x18\x02 \x01(\t\"\xb7\x02\n\x15PrepareUploadResponse\x12\x11\n\tupload_id\x18\x01 \x01(\t\x12\x14\n\x0cpayment_type\x18\x02 \x01(\t\x12\'\n\x08payments\x18\x03 \x03(\x0b\x32\x15.antd.v1.PaymentEntry\x12\r\n\x05\x64\x65pth\x18\x04 \x01(\r\x12\x36\n\x10pool_commitments\x18\x05 \x03(\x0b\x32\x1c.antd.v1.PoolCommitmentEntry\x12 \n\x18merkle_payment_timestamp\x18\x06 \x01(\x04\x12\x14\n\x0ctotal_amount\x18\x07 \x01(\t\x12\x1d\n\x15payment_vault_address\x18\x08 \x01(\t\x12\x1d\n\x15payment_token_address\x18\t \x01(\t\x12\x0f\n\x07rpc_url\x18\n \x01(\t\"Y\n\x13PoolCommitmentEntry\x12\x11\n\tpool_hash\x18\x01 \x01(\t\x12/\n\ncandidates\x18\x02 \x03(\x0b\x32\x1b.antd.v1.CandidateNodeEntry\"=\n\x12\x43\x61ndidateNodeEntry\x12\x17\n\x0frewards_address\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\t\"\xce\x01\n\x15\x46inalizeUploadRequest\x12\x11\n\tupload_id\x18\x01 \x01(\t\x12?\n\ttx_hashes\x18\x02 \x03(\x0b\x32,.antd.v1.FinalizeUploadRequest.TxHashesEntry\x12\x18\n\x10winner_pool_hash\x18\x03 \x01(\t\x12\x16\n\x0estore_data_map\x18\x04 \x01(\x08\x1a/\n\rTxHashesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"l\n\x16\x46inalizeUploadResponse\x12\x10\n\x08\x64\x61ta_map\x18\x01 \x01(\t\x12\x0f\n\x07\x61\x64\x64ress\x18\x02 \x01(\t\x12\x18\n\x10\x64\x61ta_map_address\x18\x03 \x01(\t\x12\x15\n\rchunks_stored\x18\x04 \x01(\x04\x32\x92\x02\n\rUploadService\x12V\n\x11PrepareFileUpload\x12!.antd.v1.PrepareFileUploadRequest\x1a\x1e.antd.v1.PrepareUploadResponse\x12V\n\x11PrepareDataUpload\x12!.antd.v1.PrepareDataUploadRequest\x1a\x1e.antd.v1.PrepareUploadResponse\x12Q\n\x0e\x46inalizeUpload\x12\x1e.antd.v1.FinalizeUploadRequest\x1a\x1f.antd.v1.FinalizeUploadResponseBDZ8github.com/WithAutonomi/ant-sdk/antd-go/proto/antd/v1;v1\xaa\x02\x07\x41ntd.V1b\x06proto3" + +pool = ::Google::Protobuf::DescriptorPool.generated_pool +pool.add_serialized_file(descriptor_data) + +module Antd + module V1 + PrepareFileUploadRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.PrepareFileUploadRequest").msgclass + PrepareDataUploadRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.PrepareDataUploadRequest").msgclass + PrepareUploadResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.PrepareUploadResponse").msgclass + PoolCommitmentEntry = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.PoolCommitmentEntry").msgclass + CandidateNodeEntry = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.CandidateNodeEntry").msgclass + FinalizeUploadRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.FinalizeUploadRequest").msgclass + FinalizeUploadResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("antd.v1.FinalizeUploadResponse").msgclass + end +end diff --git a/antd-ruby/lib/antd/v1/upload_services_pb.rb b/antd-ruby/lib/antd/v1/upload_services_pb.rb new file mode 100644 index 00000000..e15c03eb --- /dev/null +++ b/antd-ruby/lib/antd/v1/upload_services_pb.rb @@ -0,0 +1,51 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# Source: antd/v1/upload.proto for package 'antd.v1' + +require 'grpc' +require 'antd/v1/upload_pb' + +module Antd + module V1 + module UploadService + # External-signer flow for files + in-memory data. Mirrors the REST + # `/v1/upload/prepare`, `/v1/data/prepare`, `/v1/upload/finalize` surface 1:1. + # + # Two-phase: caller submits a prepare request, daemon collects quotes from the + # network and stashes server-side state keyed by `upload_id`. Caller then + # signs + submits the EVM payment off-daemon, then calls FinalizeUpload with + # the resulting transaction artefacts. + # + # Wave-batch (files with < 64 chunks): per-quote payments via `payForQuotes()`. + # Merkle (files with >= 64 chunks): pool commitments via `payForMerkleTree2()`. + # + # File and data prepares are intentionally separate RPCs (their request shapes + # differ — filesystem path vs raw bytes), but they share one finalize RPC + # because the server-side stored state is the same shape either way. + class Service + + include ::GRPC::GenericService + + self.marshal_class_method = :encode + self.unmarshal_class_method = :decode + self.service_name = 'antd.v1.UploadService' + + # Phase 1: prepare a file upload for external signing. Returns payment + # details + an upload_id. For files with < 64 chunks returns + # `payment_type = "wave_batch"`; otherwise `payment_type = "merkle"`. + rpc :PrepareFileUpload, ::Antd::V1::PrepareFileUploadRequest, ::Antd::V1::PrepareUploadResponse + # Phase 1 (data): prepare an in-memory data upload for external signing. + # Same as PrepareFileUpload but takes raw bytes instead of a filesystem + # path. Honours visibility = "public" (requires ant-client #73, already + # merged on main since 2026-05-05). + rpc :PrepareDataUpload, ::Antd::V1::PrepareDataUploadRequest, ::Antd::V1::PrepareUploadResponse + # Phase 2: finalize an upload after the external EVM payment has landed. + # For wave-batch uploads pass `tx_hashes`; for merkle uploads pass + # `winner_pool_hash` from the `MerklePaymentMade` event. The server-side + # stored upload state is consumed (one-shot). + rpc :FinalizeUpload, ::Antd::V1::FinalizeUploadRequest, ::Antd::V1::FinalizeUploadResponse + end + + Stub = Service.rpc_stub_class + end + end +end diff --git a/antd-ruby/test/test_grpc_client.rb b/antd-ruby/test/test_grpc_client.rb index 31ebe814..f9dc4e71 100644 --- a/antd-ruby/test/test_grpc_client.rb +++ b/antd-ruby/test/test_grpc_client.rb @@ -127,6 +127,123 @@ def put(_req) def get(_req) OpenStruct.new(data: "chunkdata") end + + # Inputs starting with "EXISTS" → already-stored short-circuit. + def prepare_chunk(req) + if req.data.byteslice(0, 6) == "EXISTS" + OpenStruct.new( + address: "0xabc", + already_stored: true, + upload_id: "", + payment_type: "", + payments: [], + total_amount: "", + payment_vault_address: "", + payment_token_address: "", + rpc_url: "", + ) + else + OpenStruct.new( + address: "0xnewchunk", + already_stored: false, + upload_id: "upid_chunk_42", + payment_type: "wave_batch", + payments: [ + OpenStruct.new(quote_hash: "0xq1", rewards_address: "0xr1", amount: "100"), + ], + total_amount: "100", + payment_vault_address: "0xvault", + payment_token_address: "0xtoken", + rpc_url: "http://localhost:8545", + ) + end + end + + def finalize_chunk(req) + # Echo upload_id into address so the test can verify forwarding. + OpenStruct.new(address: "addr_for_#{req.upload_id}") + end + end + + # Mock UploadService stub. PrepareFileUpload echoes visibility into + # upload_id; PrepareDataUpload returns merkle when payload starts with + # "MERKLE"; FinalizeUpload returns merkle vs wave-batch based on which + # field is set. + class UploadStub + def prepare_file_upload(req) + OpenStruct.new( + upload_id: "upid_file_#{req.visibility}", + payment_type: "wave_batch", + payments: [ + OpenStruct.new(quote_hash: "0xqa", rewards_address: "0xra", amount: "1"), + ], + depth: 0, + pool_commitments: [], + merkle_payment_timestamp: 0, + total_amount: "1", + payment_vault_address: "0xvault", + payment_token_address: "0xtoken", + rpc_url: "http://localhost:8545", + ) + end + + def prepare_data_upload(req) + uid = "upid_data_#{req.visibility}" + if req.data.byteslice(0, 6) == "MERKLE" + OpenStruct.new( + upload_id: uid, + payment_type: "merkle", + payments: [], + depth: 7, + pool_commitments: [ + OpenStruct.new( + pool_hash: "0xpool", + candidates: [ + OpenStruct.new(rewards_address: "0xc1", amount: "5"), + ], + ), + ], + merkle_payment_timestamp: 1_700_000_000, + total_amount: "0", + payment_vault_address: "0xvault", + payment_token_address: "0xtoken", + rpc_url: "http://localhost:8545", + ) + else + OpenStruct.new( + upload_id: uid, + payment_type: "wave_batch", + payments: [ + OpenStruct.new(quote_hash: "0xqb", rewards_address: "0xrb", amount: "2"), + ], + depth: 0, + pool_commitments: [], + merkle_payment_timestamp: 0, + total_amount: "2", + payment_vault_address: "0xvault", + payment_token_address: "0xtoken", + rpc_url: "http://localhost:8545", + ) + end + end + + def finalize_upload(req) + if req.winner_pool_hash && !req.winner_pool_hash.empty? + OpenStruct.new( + data_map: "dm_merkle", + address: req.store_data_map ? "stored_on_network" : "", + data_map_address: "", + chunks_stored: 64, + ) + else + OpenStruct.new( + data_map: "dm_wave", + address: "", + data_map_address: req.upload_id.end_with?("public") ? "addr_public_dm" : "", + chunks_stored: 3, + ) + end + end end class FileStub @@ -192,6 +309,7 @@ def build_fake_client client.instance_variable_set(:@data_stub, FakeGrpc::DataStub.new) client.instance_variable_set(:@chunk_stub, FakeGrpc::ChunkStub.new) client.instance_variable_set(:@file_stub, FakeGrpc::FileStub.new) + client.instance_variable_set(:@upload_stub, FakeGrpc::UploadStub.new) client end @@ -202,6 +320,7 @@ def build_error_client(error) client.instance_variable_set(:@data_stub, stub) client.instance_variable_set(:@chunk_stub, stub) client.instance_variable_set(:@file_stub, stub) + client.instance_variable_set(:@upload_stub, stub) client end @@ -363,6 +482,97 @@ def test_error_propagates_from_file_upload assert_raises(Antd::TooLargeError) { client.file_upload_public("/tmp/big") } end + # --- External signer (prepare/finalize) --- + + def test_prepare_upload_omits_visibility_when_nil + r = @client.prepare_upload("/tmp/x.bin") + # Empty visibility = proto3 default; the mock echoes that into upload_id. + assert_equal "upid_file_", r.upload_id + assert_equal "wave_batch", r.payment_type + assert_equal 1, r.payments.length + assert_equal "0xqa", r.payments.first.quote_hash + assert_nil r.depth + assert_nil r.pool_commitments + assert_nil r.merkle_payment_timestamp + end + + def test_prepare_upload_forwards_visibility_public + r = @client.prepare_upload("/tmp/x.bin", visibility: "public") + assert_equal "upid_file_public", r.upload_id + end + + def test_prepare_upload_public_convenience + r = @client.prepare_upload_public("/tmp/x.bin") + assert_equal "upid_file_public", r.upload_id + end + + def test_prepare_data_upload_wave_batch + r = @client.prepare_data_upload("small") + assert_equal "upid_data_", r.upload_id + assert_equal "wave_batch", r.payment_type + assert_nil r.depth + end + + def test_prepare_data_upload_merkle + r = @client.prepare_data_upload("MERKLE-large-payload") + assert_equal "merkle", r.payment_type + assert_equal 7, r.depth + assert_equal 1_700_000_000, r.merkle_payment_timestamp + assert_equal 1, r.pool_commitments.length + assert_equal "0xpool", r.pool_commitments.first.pool_hash + assert_equal "0xc1", r.pool_commitments.first.candidates.first.rewards_address + end + + def test_finalize_upload_wave_batch_private_omits_data_map_address + r = @client.finalize_upload("upid_file_", { "0xq1" => "0xtx1" }) + assert_equal "dm_wave", r.data_map + assert_equal "", r.data_map_address + assert_equal 3, r.chunks_stored + end + + def test_finalize_upload_wave_batch_public_returns_data_map_address + r = @client.finalize_upload("upid_file_public", { "0xq1" => "0xtx1" }) + assert_equal "addr_public_dm", r.data_map_address + end + + def test_finalize_merkle_upload_store_data_map_true + r = @client.finalize_merkle_upload("upid_data_", "0xwinpool", store_data_map: true) + assert_equal "dm_merkle", r.data_map + assert_equal "stored_on_network", r.address + assert_equal 64, r.chunks_stored + end + + def test_finalize_merkle_upload_store_data_map_default_false + r = @client.finalize_merkle_upload("upid_data_", "0xwinpool") + assert_equal "dm_merkle", r.data_map + assert_equal "", r.address + end + + def test_prepare_chunk_upload_new_chunk + r = @client.prepare_chunk_upload("newchunk") + refute r.already_stored + assert_equal "0xnewchunk", r.address + assert_equal "upid_chunk_42", r.upload_id + assert_equal "wave_batch", r.payment_type + assert_equal 1, r.payments.length + assert_equal "0xq1", r.payments.first.quote_hash + assert_equal "100", r.total_amount + assert_equal "http://localhost:8545", r.rpc_url + end + + def test_prepare_chunk_upload_already_stored_short_circuit + r = @client.prepare_chunk_upload("EXISTS-data") + assert r.already_stored + assert_equal "0xabc", r.address + assert_equal "", r.upload_id + assert_empty r.payments + end + + def test_finalize_chunk_upload_returns_address_and_forwards_body + addr = @client.finalize_chunk_upload("upid_chunk_42", { "0xq1" => "0xtxabc" }) + assert_equal "addr_for_upid_chunk_42", addr + end + private # Build a GRPC::BadStatus-compatible error.