feat(ffi): re-add external-signer upload surface (wave-batch) - #199
Conversation
Adds the WalletConnect/external-signer flow back to ant-ffi (dropped in #150 during the ant-core refresh). The app builds the on-chain payment, an external wallet signs it, and no private key is held by the client. New Client API: - connect_for_external_signer(peers, rpc_url, token, vault): EVM network configured, NO wallet — quotes work, payment is signed off-device. - prepare_data_upload / prepare_file_upload(visibility) -> PreparedUploadInfo: encrypts + collects quotes, returns the payForQuotes payment entries and an opaque upload_id. The heavy PreparedUpload state (chunk content) stays in Rust in an in-memory session map keyed by upload_id (it can't cross FFI). - finalize_upload(upload_id, {quote_hash: tx_hash}): stores the chunks after the external wallet has paid, returns data map / address. New records: PaymentEntry, PreparedUploadInfo, ExternalUploadResult. Wave-batch only for now; merkle prepares return a clear InvalidInput error (follow-up). Verified: ant-ffi compiles; Swift + Kotlin bindings generate the new methods + types. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ost dylib build-android.sh generated Kotlin bindings from target/release/libant_ffi.dylib, but the script never builds that host cdylib (it only cross-compiles the Android .so's and builds the bindgen *binary*). So the dylib could be stale, producing bindings that lag the Rust source — the new external-signer methods were silently missing from the AAR. Point uniffi-bindgen at the freshly cross-compiled aarch64 Android .so (UniFFI metadata is arch-independent). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…connect
- Bump the pinned ant-core rev to latest main (0.2.9) + ant-protocol 2.2.2,
so the mobile client matches a devnet built from current ant-client.
- Add Client.connect_from_devnet_manifest_external_signer(path): same
devnet-tuned node config as the wallet variant but attaches only the EVM
network (no wallet) — required for the Sepolia devnet, whose manifest has an
empty wallet_private_key ("bring your own wallet").
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The static-library xcframework copied its `module.modulemap` into the consumer's shared `BUILT_PRODUCTS_DIR/include/`, colliding with any other static xcframework doing the same — notably Reown AppKit's `yttrium`: "Multiple commands produce .../include/module.modulemap". This blocked AntFfi from coexisting with WalletConnect (and likely other native crypto SDKs) in one iOS app. `build-swift.sh` now packages each slice as a DYNAMIC `.framework` built from the cdylib (flat layout for iOS, versioned bundle for macOS): the modulemap lives in `Modules/`, `install_name` is set to `@rpath`, and a generated `Info.plist` carries a valid bundle id. Multiple such frameworks coexist cleanly in one app. Verified end-to-end: a demo app that links Reown AppKit builds for the iPhone simulator against the pipeline-produced xcframework — collision gone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a UniFFI callback interface so consumers get real % progress:
- ProgressUpdate{phase,done,total} record + ProgressListener trait
- Client.finalize_upload_with_progress / download_public_to_file /
download_private_to_file bridge ant-core's UploadEvent / DownloadEvent
(tokio mpsc) to the listener; the download variants stream to a file.
- tokio "sync" feature for the mpsc bridge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Self-review notes from a pass over this branch (build confirmed green:
Verified locally; not posting a formal review (leaving the merge to a human). |
dirvine
left a comment
There was a problem hiding this comment.
Hermes review
Requesting changes for one introduced local-check failure. The core FFI build and binding generation are basically there, but this should be fixed before merge.
Blocking
cargo clippy -p ant-ffi --all-targets -- -D warningsnow fails on the PR head withclippy::doc_lazy_continuationinffi/rust/ant-ffi/src/lib.rs:107-108. Baselineorigin/mainpasses the same clippy command, so this is introduced by this PR. Indent those continuation doc lines or add a blank doc line before the paragraph.
Warnings / follow-ups
cargo fmt --checkfails on the PR head.origin/mainalready has some pre-existingwallet.rsformatting drift, but the PR adds additional formatting diffs in changed files (client.rs,lib.rs,data.rs). Worth runningcargo fmtbefore merge, even if you keep unrelated baseline cleanup separate.finalize_uploadremoves the prepared upload from the in-memory session map before tx-hash parsing and before the network finalization succeeds (client.rs:750-778). That mirrors the daemon flow, but it means a malformed tx hash or transient store failure consumes the only copy of the prepared chunks. For an external-wallet/mobile flow this is a nasty retry story after the user has paid. At minimum, parse/validate before removing; ideally document or add a retry/cancel contract if the corePreparedUploadownership model prevents retaining it through a failed finalize.download_private_to_filedecodes the caller-provided DataMap hex without the size guard used bydata_get_private(client.rs:724-738vs existing cap atclient.rs:522-532). If this FFI surface is callable from app/UI input, reuse the same cap to avoid an avoidable memory spike.
Verified locally
cargo build -p ant-ffi✅cargo test -p ant-ffi --no-fail-fast✅ (0 tests)cargo build --release -p ant-ffi && cargo build --release --bin uniffi-bindgen✅- Swift and Kotlin UniFFI generation from
target/release/libant_ffi.dylib✅; generated methods includeconnectForExternalSigner,prepareDataUpload,finalizeUpload,finalizeUploadWithProgress, and the download-to-file/progress APIs. gh pr checks 199reports no checks on this branch, so there is no CI signal to lean on here.
One small mercy: no private key appears to be introduced in the external-signer constructor path. Quietly behaving as advertised.
|
Follow-up from the parallel review pass: my requested-changes review already covers the destructive
I’d keep the destructive finalize behaviour as the merge blocker. These are follow-up quality/resource-safety issues, but they are real enough to avoid surprising mobile consumers later. Tiny foot-gun, large boot. |
…t, caps) Hermes review follow-ups on the external-signer surface: - finalize_upload: parse & validate ALL tx hashes BEFORE removing the prepared upload from the session map. The caller has already paid on-chain, so a malformed hash no longer destroys the only in-memory copy of the prepared chunks — bad input now leaves the upload intact and retryable. Documented the remaining constraint: ant-core consumes PreparedUpload by value, so a *network* finalize failure still consumes it (re-prepare; the same payment is reusable). - add cancel_upload(upload_id) so a caller can discard a prepared upload that will never be finalized and free its payload buffer; documented the sessions map lifecycle + memory cost on the field. - download_private_to_file: apply the same 20MB hex input cap as data_get_private (reachable from app/UI input). - narrow ProgressUpdate docs: external-signer upload progress currently covers the "storing" phase only (finalize); encrypting/quoting are not yet surfaced (prepare_* takes no listener). - fix clippy::doc_lazy_continuation and run cargo fmt on the changed files (client.rs, lib.rs, data.rs, wallet.rs). Verified: cargo clippy -p ant-ffi --all-targets -- -D warnings (clean), cargo fmt -p ant-ffi --check (clean), cargo build -p ant-ffi (ok). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @dirvine — addressed in Blocking
Warnings / follow-ups
Verified locally: clippy |
dirvine
left a comment
There was a problem hiding this comment.
Hermes re-review after aae287b
Thanks for the update. The local check failures and several API polish points are fixed:
cargo build -p ant-ffi✅cargo test -p ant-ffi --no-fail-fast✅ (still 0 tests)cargo clippy -p ant-ffi --all-targets -- -D warnings✅cargo fmt -p ant-ffi --check✅- Swift/Kotlin UniFFI generation from the fresh release dylib ✅, including
cancelUpload.
I’m still not comfortable approving yet because the remaining network-failure path is more serious than the inline comment suggests.
Still blocking: network finalize failure still consumes the only reusable proof material
finalize_inner now validates tx hash syntax before removing the prepared upload, which fixes malformed-input retry. But after that it still removes the PreparedUpload and passes it by value into ant-core:
ffi/rust/ant-ffi/src/client.rs:806-843
The new inline comment says a network store failure consumes it and “the caller must then re-prepare (safe: the same on-chain payment can be reused).” I don’t think that safety claim is established. In evmlib 0.8.1, PaymentQuote::hash() includes the quote timestamp, node public key, and node signature (data_payments.rs:101-107, with timestamp included in bytes_for_signing at :111-124). A later re-prepare can collect fresh quotes with different quote hashes, so the caller’s old {quote_hash: tx_hash} map may not match the new PreparedUpload. The original prepared quotes/proofs are exactly the material being dropped.
So the bad-input case is fixed, but a transient storage/network failure after the user has paid can still strand the paid attempt unless the original prepared upload/proofs are retained or ant-core returns enough state to retry. At minimum, the public FFI contract should not say or imply that re-prepare safely reuses the same payment. Preferably this PR should either:
- keep the prepared state retryable until storage succeeds; or
- change ant-core to return the
PreparedUpload/ retry state on finalize failure; or - explicitly expose/document this as non-retryable-after-payment and provide a follow-up blocker for the core API before depending on it in mobile flows.
The third option is less satisfying, naturally — “pay once, maybe upload once” is not the sort of UX anyone frames in a launch deck unless they enjoy incident reviews.
No other new blockers found in this pass.
The prior comment claimed a network store failure after payment was safe to recover by re-preparing (same payment reused). That is wrong: ant-core consumes the PreparedUpload/proofs by value and quote hashes are timestamp+signature-derived, so a re-prepare yields different quote hashes that won't match the already-paid tx map — the paid attempt is stranded. State the honest contract on `finalize_upload` (bad input is retryable; a post-payment storage failure is currently NOT retryable) and reference the follow-ups that add a proper retry-state API: - WithAutonomi/ant-client#140 (ant-core: return retry state on failure) - #201 (this surface: retain session + rev bump) No behaviour change — docs/comments only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
You're right, and thanks for the precise diagnosis — the timestamp+signature in Confirmed the ownership model in ant-core: Going with option 3 (honest contract) here + a tracked two-repo fix, since a proper retry-state API is out of scope for this PR:
So the PR no longer overpromises, and "pay once, maybe upload once" is now a documented, tracked limitation rather than a silent one. Happy to prioritise #140 next if you'd rather the real fix land before this merges — your call on whether the honest-contract version is mergeable in the meantime. |
dirvine
left a comment
There was a problem hiding this comment.
Hermes re-review after 63538ef
The new comment and commit address my remaining objection in the form we agreed was acceptable for this PR: it no longer claims post-payment network/storage failure is retryable, and it tracks the real core/API fix separately.
Verified at head 63538ef74ad8b75735f32d5963d56e3bfc51eaae:
cargo build -p ant-ffi✅cargo test -p ant-ffi --no-fail-fast✅ (still 0 tests)cargo clippy -p ant-ffi --all-targets -- -D warnings✅cargo fmt -p ant-ffi --check✅- Swift/Kotlin UniFFI generation from the fresh release dylib ✅; generated bindings include
cancelUploadand the updated failure-contract docs. gh pr checks 199still reports no checks on this branch.- Follow-up issues exist and match the limitation: WithAutonomi/ant-client#140 for the core retry-state API, #201 for retaining/retrying from ant-ffi once core supports it.
Approved with one explicit caveat: this is mergeable as an honest-contract FFI surface, not as production-ready retry-safe WalletConnect upload UX. Before the mobile external-signer flow depends on this in production, #140/#201 need to land so a post-payment storage failure can be retried without stranding the paid attempt.
The ffi/rust/ant-ffi subtree had no CI — ci.yml only covers antd/ and antd-rust/, go-ci.yml covers antd-go/, and release.yml builds the antd binary. So FFI PRs (#199, #202) had no automated signal; build/clippy/ fmt/test/bindgen were only ever run locally. Adds ffi-ci.yml mirroring the check-antd job (fmt --all --check, clippy --all-targets --all-features --locked -D warnings, cargo doc, cargo test), scoped to ffi/rust/**, plus a UniFFI generation smoke step (generate Swift + Kotlin from the release dylib) that catches broken #[uniffi::export] surfaces a plain build misses. No Xcode/NDK needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What
Re-adds the external-signer upload flow to
ant-ffi(dropped in #150 during the ant-core refresh). The app constructs the on-chain payment, an external wallet signs it, and the client holds no private key — the store-policy-safe path for mobile.ant-corealready has the full two-phase flow (file/data_prepare_upload_with_visibility→finalize_upload); this wires a UniFFI-friendly surface over it.New
ClientAPIconnect_for_external_signer(peers, rpc_url, token, vault)— configures the EVM network with no wallet (quotes/price queries work; payment is signed off-device).prepare_data_upload/prepare_file_upload(…, visibility)→PreparedUploadInfo— encrypts, collects quotes, returns thepayForQuotespayment entries + an opaqueupload_id.finalize_upload(upload_id, {quote_hash: tx_hash})→ExternalUploadResult— stores the chunks once the wallet has paid.cancel_upload(upload_id)— discard a prepared upload that will not be finalized, freeing its in-memory payload.finalize_upload_with_progress,download_public_to_file/download_private_to_filereport live progress via aProgressListenercallback.New records:
PaymentEntry,PreparedUploadInfo,ExternalUploadResult,ProgressUpdate.Design note
The
PreparedUploadpayload (chunk content,PreparedChunk/merkle state) can't cross the FFI boundary, soprepare_*stashes it in an in-memory session map keyed byupload_idand returns only the summary;finalize_uploadconsumes it. Same contract the antd daemon exposes over gRPC/REST.Breaking changes
ClientError.code()andWalletError.code()(previously#[uniffi::export]ed). These numeric error codes are no longer part of the generated Swift/Kotlin surface — consumers should match on the error variant instead. Pre-1.0, but calling it out so it lands in the release notes.Scope
InvalidInputerror (tracked: Linear V2-570).ant-swift/ant-androidbumps are follow-ups (the Swift xcframework rides on the dynamic-framework packaging PR).Verification
cargo build -p ant-fficlean;cargo clippy -p ant-ffi --all-targets -- -D warningsclean;cargo fmt -p ant-ffi --checkclean;uniffi-bindgengenerates the new methods + types for both Swift and Kotlin.