Skip to content

feat(swift-sdk): add production create-document flow via platform-wallet FFI#3908

Merged
QuantumExplorer merged 5 commits into
v3.1-devfrom
claude/peaceful-mahavira-0265a4
Jun 15, 2026
Merged

feat(swift-sdk): add production create-document flow via platform-wallet FFI#3908
QuantumExplorer merged 5 commits into
v3.1-devfrom
claude/peaceful-mahavira-0265a4

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Jun 15, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

Creating (broadcasting) a document in SwiftExampleApp was only reachable via Settings → Platform State Transitions → Document Create (test-signer builder, test-plan DOC-02 🧪), and the DocumentsView "Create Document" path was a local-only mock with no broadcast (DOC-09 ⚠️). There was no real, production create-document flow signed by the wallet's keychain signer.

Tracked in the iOS QA tracker #3897 (DOC-02).

What was done?

A real, schema-driven Create Document flow that broadcasts a DocumentCreate through rs-platform-wallet using the wallet's keychain-backed signer (per packages/swift-sdk/CLAUDE.md — high-level flows live in platform-wallet, Swift only renders/marshals/persists).

  • rs-platform-walletIdentityWallet::create_document_with_signer (new wallet/identity/network/document.rs): fetches the on-chain DataContract, builds a rev-1 Document via DocumentType::create_document_from_data, selects an AUTHENTICATION + ECDSA_SECP256K1 key whose security level satisfies the doc type's security_level_requirement() (mirrors the consensus rule — not a hardcoded CRITICAL), and broadcasts via the SDK's PutDocument::put_to_platform_and_wait_for_response on the 8 MB worker stack. Includes unit tests for the security-level selection.
  • rs-platform-wallet-ffiplatform_wallet_create_document_with_signer C ABI (new document.rs), a faithful mirror of platform_wallet_create_data_contract_with_signer.
  • swift-sdk — thin ManagedPlatformWallet.createDocument(...) wrapper; schema-driven CreateDocumentView (owner-identity picker + reused DocumentFieldsView) reachable from Contracts → contract → document type → New Document (DocumentTypeDetailsView). The confirmed document is persisted to SwiftData so it appears in the documents list. The local-only mock in DocumentsView was replaced by this real flow. Per-field accessibilityIdentifiers added to DocumentFieldsView for QA automation.

Bonus fix (required for the feature to be usable): the create form self-dismissed within ~1 s. Background-sync SwiftData writes invalidated the Contracts @Query list whose ForEach rows owned the details sheet, tearing down the contract → doc-type → create navigation every second (compounded by deprecated NavigationView/NavigationLink(destination:) and auto-refreshing relative-time Text). Fixed by hoisting the details .sheet(item:) to a stable container, migrating to value-based navigationDestination, making relative-time strings static (AppDate.relative), and decoupling the root TabView from SPV spvProgress ticks. This also stabilizes other Contracts drill-downs.

How Has This Been Tested?

  • cargo fmt --all clean; cargo check -p platform-wallet -p platform-wallet-ffi clean; cargo test -p platform-wallet security-level unit tests pass.
  • cd packages/swift-sdk && ./build_ios.sh --target simBUILD SUCCEEDED (warnings-as-errors).
  • End-to-end on iPhone 17 simulator (testnet): created a preorder document (32-byte saltedDomainHash) on the DPNS contract GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec from a funded identity. Result: network-confirmed (put_to_platform_and_wait_for_response returned Ok — a rejection would surface as "Create failed"), doc id 7i1hJgvVt8fJms26kGwkEZ6jVZxrfd3BrqfmAfpqXMoG, ZPERSISTENTDOCUMENT count 0 → 1, document appears in the list. After the nav fix, the form stayed presented 18s+ with no interaction (pre-fix: dismissed within ~1s). (domain was intentionally avoided as a create target — it's a contested resource that a plain DocumentCreate can't confirm.)
  • Updated TEST_PLAN.md (DOC-02 🧪 → ✅, DOC-09 retired) and QA issue iOS SwiftExampleApp — TEST_PLAN QA run status & blockers #3897.

Breaking Changes

None. SDK/example-app surface only; no consensus-affecting changes (no !).

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added platform wallet support for creating and broadcasting revision-1 identity wallet documents with schema-driven properties.
    • Added Swift SDK API ManagedPlatformWallet.createDocument(...) returning the created document identifier and canonical JSON.
  • Improvements
    • Updated example app document creation to a production broadcast flow, including preset document-type support.
    • Refined navigation and sheet behavior across contract and document detail screens; improved global sync indicator rendering.
    • Enhanced document field UX with accessibility identifiers and smarter boolean/hex input handling.
    • Added unit coverage for signing security-level rules.
  • Documentation
    • Updated QA test plan with a new “Retired” status and refreshed document catalog actions.

…let FFI

Add a real, schema-driven "Create Document" flow to SwiftExampleApp that
broadcasts a DocumentCreate through rs-platform-wallet using the wallet's
keychain-backed signer, replacing the local-only mock (DOC-09) and
complementing the test-signer builder path.

- rs-platform-wallet: IdentityWallet::create_document_with_signer fetches
  the on-chain contract, builds a rev-1 Document, selects an
  AUTHENTICATION+ECDSA key satisfying the doc type's
  security_level_requirement(), and broadcasts via the SDK's
  PutDocument::put_to_platform_and_wait_for_response on the 8 MB worker stack.
- rs-platform-wallet-ffi: platform_wallet_create_document_with_signer C ABI.
- swift-sdk: ManagedPlatformWallet.createDocument wrapper + schema-driven
  CreateDocumentView (owner-identity picker + DocumentFieldsView), reachable
  from Contracts -> contract -> document type -> New Document. Confirmed
  document persisted to SwiftData so it appears in the documents list.

Also fixes a navigation-stability bug that made the create flow (and other
Contracts drill-downs) unusable: background-sync SwiftData writes invalidated
the contracts @query list whose ForEach rows owned the details sheet, tearing
down the contract-details navigation ~every second (compounded by deprecated
NavigationView/NavigationLink(destination:) and auto-refreshing relative-time
Text). Hoisted the details sheet to a stable container, migrated to
value-based navigationDestination, made relative-time strings static, and
decoupled the root TabView from SPV progress ticks.

Verified end-to-end on the iPhone 17 simulator (testnet): created a preorder
document on the DPNS contract GWRSAV...S31Ec from a funded identity;
network-confirmed, doc id 7i1hJgvVt8fJms26kGwkEZ6jVZxrfd3BrqfmAfpqXMoG,
ZPERSISTENTDOCUMENT 0->1. build_ios.sh --target sim succeeds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 55578cef-6621-4db4-89e2-4091386f8a50

📥 Commits

Reviewing files that changed from the base of the PR and between a469074 and ac0a4f2.

📒 Files selected for processing (3)
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentFieldsView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentsView.swift

📝 Walkthrough

Walkthrough

Adds end-to-end document creation: a new IdentityWallet::create_document_with_signer Rust method (with SignerRef adapter and security-level logic), a C FFI entry point, a Swift ManagedPlatformWallet.createDocument wrapper, and a replacement production CreateDocumentView. Also ships SwiftUI stability fixes for sheet hoisting, value-based navigation, and sync-overlay isolation.

Changes

Document creation end-to-end + UI stability

Layer / File(s) Summary
Rust core: SignerRef, security-level mapping, create_document_with_signer
packages/rs-platform-wallet/src/wallet/identity/network/document.rs, packages/rs-platform-wallet/src/wallet/identity/network/mod.rs
Introduces SignerRef<'a, S> borrowed-signer adapter, allowed_signing_security_levels helper, and the full IdentityWallet::create_document_with_signer async method covering contract fetch, JSON parse/sanitize, Document construction, AUTHENTICATION ECDSA_SECP256K1 key selection, and platform broadcast. Includes unit tests for the security-level mapping.
Rust FFI entry point
packages/rs-platform-wallet-ffi/src/document.rs, packages/rs-platform-wallet-ffi/src/lib.rs
Adds the platform_wallet_create_document_with_signer C-exported function (imports, comprehensive documentation, pointer validation, CString decoding, VTableSigner derivation, block_on_worker dispatch, canonical JSON serialization, 32-byte id copy-out, owned C string allocation) and wires the document module into the crate root with a pub use re-export.
Swift SDK wrapper
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift
Adds ManagedPlatformWallet.createDocument(...) public async method: marshals IDs and strings into FFI types, dispatches platform_wallet_create_document_with_signer on a detached task with KeychainSigner keepalive via withExtendedLifetime, checks PlatformWalletFFIResult, frees the owned canonical JSON pointer, and returns the (Identifier, String) pair.
Production CreateDocumentView and entry points
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentsView.swift, Views/DocumentTypeDetailsView.swift, Views/DocumentFieldsView.swift
Replaces the local-only CreateDocumentView with a schema-driven production flow: DocumentFieldsView fields with accessibility identifiers and conditional boolean inclusion (only required or touched booleans), owner identity selection, propertiesJSON(from:) JSON encoding (hex-encoding Data values, parsing object fields from editor JSON), async broadcast via wallet.createDocument(...), and persistConfirmedDocument(...) for local SwiftData persistence. DocumentTypeDetailsView adds a "+" toolbar button and sheet entry point with newDocumentSection. DocumentsView injects walletManager.
SwiftUI stability: sheet hoisting, NavigationStack migration, sync overlay isolation
Views/ContractsTabView.swift, Views/LocalDataContractsView.swift, Views/DataContractDetailsView.swift, SwiftExampleApp/ContentView.swift
Hoists DataContractDetailsView sheet presentation to stable parent views in ContractsTabView and LocalDataContractsView to survive @Query re-renders. Migrates DataContractDetailsView to NavigationStack with value-based NavigationLink routes, ParsedGroup: Hashable, and new OwnerRoute. Isolates SPV sync observation from root ContentView into GlobalSyncIndicatorOverlay to prevent frequent TabView invalidation. Changes "Last used" from auto-updating Text to precomputed AppDate.relative() string.
QA test-plan update
packages/swift-sdk/SwiftExampleApp/TEST_PLAN.md
Adds Retired status to legend, retires DOC-09 (local mock flow), and promotes DOC-02 to reflecting the production broadcast flow.

Sequence Diagram(s)

sequenceDiagram
  participant View as CreateDocumentView
  participant Wallet as ManagedPlatformWallet
  participant SDK as Swift SDK
  participant RustFFI as Rust FFI
  participant RustCore as IdentityWallet
  View->>View: propertiesJSON(from: fields)
  View->>Wallet: createDocument(ownerIdentityId, contractId, documentType, propertiesJSON, KeychainSigner)
  Wallet->>Wallet: marshal to FFI bytes and C strings
  Wallet->>RustFFI: platform_wallet_create_document_with_signer
  RustFFI->>RustCore: create_document_with_signer(owner_id, contract_id, type, json, VTableSigner)
  RustCore->>RustCore: fetch DataContract, parse JSON, build Document
  RustCore->>RustCore: select AUTHENTICATION ECDSA_SECP256K1 key via allowed_signing_security_levels
  RustCore->>RustCore: put_to_platform_and_wait_for_response
  RustCore-->>RustFFI: confirmed Document id + canonical JSON
  RustFFI-->>Wallet: PlatformWalletFFIResult + id bytes + JSON C string
  Wallet-->>View: (Identifier, String)
  View->>View: persistConfirmedDocument, update UI state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • Platform #3897: Directly implements the document creation feature and broadcast flow being tested and documented, including platform_wallet_create_document_with_signer FFI function, IdentityWallet::create_document_with_signer method, and production CreateDocumentView UI.

Suggested reviewers

  • shumkov
  • llbartekll
  • ZocoLini
  • lklimek
  • thepastaclaw

Poem

🐇 Hop, hop, the document flies,
Through Rust and Swift to Platform skies!
A signer VTable leads the way,
Bytes marshalled neatly, come what may.
The sheets now hoist and nav stacks gleam—
One broadcast doc, a bunny's dream! 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(swift-sdk): add production create-document flow via platform-wallet FFI' accurately reflects the main change: implementing a production-ready document creation flow across Rust/C FFI and Swift SDK layers.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/peaceful-mahavira-0265a4

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@thepastaclaw

thepastaclaw commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

✅ Review complete (commit a469074)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift`:
- Around line 2606-2635: The `_ = signer` discard at the beginning of the
Task.detached closure is an unreliable pattern for keeping signer alive across
the FFI call to platform_wallet_create_document_with_signer, especially in async
detached tasks where the optimizer may elide it in -O builds, causing a
use-after-free. Replace the `_ = signer` pattern by wrapping the entire nested
withUnsafeBufferPointer and withCString marshalling chain in
`withExtendedLifetime(signer) { ... }`. This matches the established pattern
used elsewhere in the file like in registerIdentityWithFundingSigner and
guarantees signer remains alive through the complete FFI call and return.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentFieldsView.swift`:
- Around line 77-82: The boolean Toggle control in the "boolean" case lacks an
accessibility label for assistive technologies like VoiceOver. Although the
property name is visible in the parent view and an accessibility identifier is
present, the Toggle itself has a hidden empty label that prevents screen readers
from understanding its purpose. Add an explicit accessibilityLabel modifier to
the Toggle that provides the property name as the label, ensuring assistive
technologies can identify what the toggle controls.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentsView.swift`:
- Around line 476-484: The persistConfirmedDocument function is swallowing
errors from modelContext.save() which allows the success message "Document
created" to be reported even when local persistence fails. Modify the
persistConfirmedDocument function to properly handle errors from the
modelContext.save() call by catching any exceptions thrown and ensuring the
function fails gracefully without reporting success when persistence fails, so
the document list reflects the actual state of persistence.
- Around line 446-449: For object fields, the fieldValues are stored as JSON
strings from DocumentFieldsView, but are being passed directly to the
propertiesJSON method where they get double-encoded as nested JSON strings
instead of proper objects. Modify the code around the propertiesJSON call to
decode the JSON string values back to dictionaries before encoding, so that
object fields are properly nested in the final properties JSON. This same issue
also exists in the related field encoding logic, so apply the same fix there as
well to ensure consistent handling of object field types.
- Around line 328-332: When the selected schema (docType parameter) changes in
the schemaSection method, the parent fieldValues state needs to be cleared to
prevent submitting values from the previous schema with the newly selected
document type. Add an onChange handler that monitors the docType parameter and
resets the fieldValues dictionary to an empty state whenever the docType
changes. This ensures a clean slate when switching between different schemas in
the no-preset flow.
- Around line 262-267: The selectionSection view displays all persisted
contracts in the Picker without filtering by the active network, which allows
users to pair a current-network owner with a contract from another network.
Filter the contracts array by appState.currentNetwork in the ForEach loop that
iterates over contracts (similar to how owners are scoped to
appState.currentNetwork at line 407), so only contracts matching the currently
active network are available for selection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 17088e4d-e3e8-4693-a7e2-4dac132b0c71

📥 Commits

Reviewing files that changed from the base of the PR and between 792fd44 and f647d58.

📒 Files selected for processing (13)
  • packages/rs-platform-wallet-ffi/src/document.rs
  • packages/rs-platform-wallet-ffi/src/lib.rs
  • packages/rs-platform-wallet/src/wallet/identity/network/document.rs
  • packages/rs-platform-wallet/src/wallet/identity/network/mod.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/ContentView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/ContractsTabView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DataContractDetailsView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentFieldsView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentTypeDetailsView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentsView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/LocalDataContractsView.swift
  • packages/swift-sdk/SwiftExampleApp/TEST_PLAN.md

Comment thread packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentsView.swift Outdated

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Production create-document flow is structurally sound: Rust create_document_with_signer reproduces the consensus security-level rule, the FFI bindings mirror the established platform-wallet signer pattern with correct lifetime/keepalive handling, and the Swift wrapper is appropriately thin. Three in-scope improvements remain in the SwiftExampleApp form/persistence layer; nothing blocks merge.

🟡 3 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentFieldsView.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentFieldsView.swift:100-108: Object-typed properties cross the boundary as JSON strings, not objects
  For schema `type: "object"` properties, the form renders a `TextEditor` bound to `textFields` (String), and `updateFieldValues` stores the edited text as a String in `fieldValues`. `DocumentsView.propertiesJSON(from:)` then emits `"field":"{\"foo\":1}"` instead of `"field":{"foo":1}`. On the Rust side, `serde_json::from_value::<BTreeMap<String, Value>>` parses it as `Value::Text`, and `sanitize_document_properties` only dispatches to `property_type.sanitize_value_mut`, which won't reshape a string into a map. The resulting document fails schema validation for any document type with an object-valued property. Parse object-field text into a JSON object before placing it into `fieldValues` so the existing `JSONSerialization` step emits a nested object.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentsView.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentsView.swift:329-339: Switching document types leaves stale field values in the payload
  `DocumentFieldsView` holds its field dictionaries in `@State` and only repopulates them in `.onAppear` via `initializeFields()`. When the user changes `selectedDocumentTypeName`, SwiftUI reuses the same `DocumentFieldsView` instance (same position in the tree), so values typed for the previous type stay in `textFields`/`numberFields`/etc. and flow into `fieldValues`. The next submit can broadcast leftover fields, triggering `additionalProperties` validation failures or sending wrong values. Key the child view by the document type id so SwiftUI tears it down and rebuilds fresh state whenever the schema changes.
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentsView.swift:510-525: PersistentDocument stores the pre-broadcast form JSON, not the canonical confirmed document
  `persistConfirmedDocument` writes the form-built `propertiesJSON` (hex-encoded byte arrays, base58-encoded identifiers) as `PersistentDocument.data`. The confirmed `Document` returned by `put_to_platform_and_wait_for_response` is discarded at the FFI boundary — `platform_wallet_create_document_with_signer` only returns the 32-byte id. For any document type with byteArray or identifier properties (e.g. DPNS `preorder.saltedDomainHash`), the persisted SwiftData row's body structurally diverges from what Platform actually stores, which will mislead later reads and re-sync logic. Either extend the FFI to return the canonical serialized document, or re-fetch the document by id before persisting.

Comment thread packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentsView.swift Outdated
- ManagedPlatformWallet.createDocument: pin the signer with
  withExtendedLifetime(signer) across the FFI call instead of the
  unreliable `_ = signer` discard (optimizer can elide it in -O →
  use-after-free), matching the other *_with_signer wrappers.
- DocumentFieldsView: add accessibilityLabel(property.name) to the
  boolean Toggle (labelsHidden() hid it from VoiceOver).
- CreateDocumentView: scope the contract picker to the active network
  (activeContracts) so an owner can't be paired with an off-network
  contract; reset field state when the selected document type changes
  (.id + onChange) so the no-preset flow can't submit stale values.
- propertiesJSON: schema-aware — decode object-typed fields from their
  editor JSON string into nested objects instead of double-encoding
  them as JSON strings.
- persistConfirmedDocument: propagate modelContext.save() errors
  instead of swallowing with try?; on local-save failure still report
  the confirmed broadcast (doc is on-chain) but surface a warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@QuantumExplorer

Copy link
Copy Markdown
Member Author

Note on the failing Rust workspace tests / Tests (macOS) check

The one failing test — fetch::document_query_v0_v1::sdk_builder_default_seeds_atomic_to_floor — is pre-existing on v3.1-dev and unrelated to this PR.

Fixing an unrelated rs-sdk test is out of scope for this PR. Once #3900 lands on v3.1-dev, rebasing this branch will turn the check green; alternatively this PR can merge ahead of it since the failure is orthogonal to the change here.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Cumulative re-review of head 92a5ce4. Two of three prior findings are FIXED (object-typed properties now parse as nested JSON via the schema-aware propertiesJSON; document-type switches reset field state via .id(docType.id) + onChange). Prior #3 (local cache stores form JSON, not canonical confirmed document) is STILL VALID and carried forward — multiple agents independently confirmed the FFI returns only the 32-byte id and the Swift side persists the pre-broadcast form JSON. New latest-delta findings: optional booleans are always serialized as false (pre-existing in DocumentFieldsView, but now load-bearing because this PR turns this path into the production broadcast flow), plus two low-severity UX/state-hygiene nitpicks. Codex's blocking claim that the preview container can broadcast and persist to a throwaway store is not reachable — .modelContainer(preview.container) rebinds the @Query scope so ownerIdentities is empty and canSubmit returns false; dropped as a false positive. No in-scope blockers.

🟡 1 suggestion(s) | 💬 2 nitpick(s)

1 additional finding(s) omitted (not in diff).

1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentFieldsView.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentFieldsView.swift:158-238: Optional boolean fields are always serialized as `false` in the broadcast payload
  `initializeFields()` seeds every boolean property with `false` (DocumentFieldsView.swift:168), and `updateFieldValues()` unconditionally copies every entry from `boolFields` into the payload (DocumentFieldsView.swift:236-238). String/number/array/object branches all skip empty values, so an untouched optional field of those types is omitted. Booleans have no equivalent skip, so an optional boolean the user never toggled is broadcast as `false` rather than absent. For schemas where absence and `false` are distinct (audit, default-driven, or storage-cost-sensitive document types), this changes the actual document content and storage cost compared to what the user supplied. Pre-PR this code path fed only the Settings test-signer flow, so the divergence was harmless; this PR makes it the production broadcast path. Track which boolean fields the user actually interacted with (e.g. drop the seed and key on user-touched only), or omit non-required booleans whose state hasn't changed from the schema default.

- DocumentFieldsView: don't broadcast untouched optional booleans as
  `false`. The seed gives booleans no "empty" state, so they were always
  serialized; now only required booleans or ones the user actually
  toggled (tracked via `touchedBoolFields`) are included, keeping
  untouched optional booleans absent (absence ≠ false for some schemas).
- DocumentsView.persistConfirmedDocument: on a failed `modelContext.save()`,
  delete the inserted row before rethrowing so a later save elsewhere
  can't silently flush it — matching the "not saved locally" warning.
- DocumentsView selection: clear `selectedDocumentTypeName` when the
  contract changes so the document-type picker can't show a stale
  selection (or silently resolve a same-named type on another contract).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Cumulative re-review of ae6ec62. Latest delta is small and targeted: it resolves three of the four prior findings (prior-2 boolean payload gating via touchedBoolFields, prior-3 modelContext.delete on save failure, prior-4 onChange clearing the doc-type picker). Carried forward: prior-1 — the FFI still discards the canonical confirmed Document and surfaces only its 32-byte id, so the SwiftData cache row stores pre-broadcast form JSON rather than Platform's canonical document. No new findings in the latest delta. CodeRabbit returned a rate-limited summary with zero inline findings, so no CR reactions are produced.

🟡 1 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-platform-wallet-ffi/src/document.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/document.rs:70-90: [Carried forward — STILL VALID] FFI discards canonical confirmed Document; SwiftData cache stores pre-broadcast form JSON instead of Platform's canonical document
  Re-validated at head ae6ec628. The Rust side of the boundary still throws away the confirmed document and only returns its 32-byte id: `create_document_with_signer` returns the confirmed `Document` from `put_to_platform_and_wait_for_response`, but the FFI collapses it to `.map(|document| document.id())` (packages/rs-platform-wallet-ffi/src/document.rs:81) and writes only 32 bytes through `out_document_id` (lines 87-89). Swift therefore has no canonical bytes to persist and writes the user's form-built `propertiesJSON` (hex-encoded byte arrays, base58-encoded identifiers, raw object subtrees) directly into `PersistentDocument.data` with a hardcoded revision of 1 (packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentsView.swift:546-555). Any Rust/DPP-side sanitization, default-filling, byte-level normalization, revision data, or timestamps Platform actually accepted are invisible to Swift — the SwiftData row reads back the user's submitted strings rather than the canonical document. The newly-added `persistWarning` only covers SwiftData save failures, not representation divergence after a successful save. This is owned by this PR because it is the moment the persistence path went from a Settings test-signer flow to the production broadcast path. Not a security/consensus issue (broadcast bytes are produced inside Rust from the sanitized value map), but a documented local-cache fidelity bug. Fix by either (a) extending the FFI to return canonical document bytes (e.g. a serialized `Document`) or the sanitized properties map, or (b) adding a sibling FFI that refetches by id and returns canonical bytes so the cache row reflects what Platform stored.

Comment thread packages/rs-platform-wallet-ffi/src/document.rs Outdated
platform_wallet_create_document_with_signer now also returns the
confirmed Document serialized to canonical JSON (DPP
to_json_with_identifiers_using_bytes — $id/$ownerId as base58 +
normalized properties), via a new out_document_json C string freed with
platform_wallet_string_free. The Swift wrapper returns (Identifier,
String) and persists the canonical JSON into PersistentDocument.data
instead of the user's form-built propertiesJSON (which had hex byteArrays
/ base58 identifiers and no system fields). The local cache row now
reflects what Platform stored.

Verified on testnet: created a preorder doc on GWRSAV…S31Ec from idx1;
persisted body = {"$id":"AFLkAA…mnZ2","$ownerId":"BjJz…Dx91",
"saltedDomainHash":[66,31,249,…]} — canonical id/owner + normalized
byteArray, matching a DOC-01 query rather than the form input.

Addresses the carried-forward review finding (local-cache fidelity).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentFieldsView.swift (1)

306-311: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Refresh fieldValues after sanitizing byte-array input.

The binding setter calls updateFieldValues() before this sanitizer runs. When .onChange then rewrites textFields[property.name] directly, the visible field is cleaned but the submitted fieldValues can still contain the pre-sanitized value until another edit occurs.

Proposed sync fix
                     .onChange(of: currentValue) { _, newValue in
                         // Remove any non-hex characters and convert to lowercase
                         let cleaned = newValue.lowercased().filter { "0123456789abcdef".contains($0) }
                         if cleaned != newValue {
                             textFields[property.name] = cleaned
+                            updateFieldValues()
                         }
                     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentFieldsView.swift`
around lines 306 - 311, The `.onChange` handler sanitizes the byte-array input
by rewriting `textFields[property.name]` with the cleaned value, but this change
is not reflected back into the `fieldValues` dictionary that gets submitted.
After the assignment of the cleaned value to `textFields[property.name]`, call
`updateFieldValues()` again to ensure the sanitized value is synchronized into
`fieldValues` so the submitted data contains only valid hex characters.
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentsView.swift (1)

241-247: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Disable swipe-to-dismiss while the broadcast is in flight.

The Cancel button is disabled, but the sheet can still be interactively dismissed during the non-idempotent DocumentCreate broadcast. If the task later succeeds or only local persistence fails, the user loses the result/warning and may retry a document that already exists on-chain.

Proposed dismissal guard
             .toolbar {
                 ToolbarItem(placement: .navigationBarLeading) {
                     Button("Cancel") { dismiss() }
                         .disabled(isSubmitting)
                 }
             }
+            .interactiveDismissDisabled(isSubmitting)
             .alert(item: $submitError) { err in
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentsView.swift`
around lines 241 - 247, While the Cancel button in the toolbar is disabled when
isSubmitting is true, the sheet can still be dismissed via swipe gesture during
the non-idempotent DocumentCreate broadcast, which risks losing the result. Add
the .interactiveDismissalDisabled(isSubmitting) modifier to the sheet to prevent
interactive swipe-to-dismiss gestures while the broadcast is in flight, ensuring
the user cannot accidentally dismiss the view until the submission is complete.
🧹 Nitpick comments (1)
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift (1)

2594-2598: ⚡ Quick win

Update the signer lifetime contract docs to match the implementation.

This method no longer uses _ = signer; the safe pattern here is withExtendedLifetime(signer) around the full FFI call. Keeping the old wording makes future signer wrappers more likely to copy the unsafe keepalive pattern.

Proposed doc update
-    /// Lifetime contract: the `signer` instance MUST stay alive for
-    /// the duration of the `await` (Rust holds a `passUnretained`
-    /// ctx pointer to the underlying `KeychainSigner`). A
-    /// `_ = signer` keepalive at the call site is the canonical way
-    /// to pin it.
+    /// Lifetime contract: the `signer` instance MUST stay alive for
+    /// the duration of the synchronous FFI call inside this async
+    /// wrapper (Rust holds a `passUnretained` ctx pointer to the
+    /// underlying `KeychainSigner`). The wrapper pins it with
+    /// `withExtendedLifetime(signer)` around the full marshalling chain.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift`
around lines 2594 - 2598, The lifetime contract documentation comment for the
signer parameter describes the keepalive pattern as `_ = signer`, but the actual
implementation uses `withExtendedLifetime(signer)` for safe lifetime extension.
Update the documentation comment to correctly describe the actual safe pattern
being used (withExtendedLifetime) instead of the old keepalive approach, and
remove the reference to `_ = signer` to prevent future implementations from
copying the unsafe pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift`:
- Around line 2656-2663: The code currently silently falls back to an empty
string when documentJsonPtr is null, but on a successful broadcast (after
result.check()), Rust guarantees to return valid canonical JSON. A null pointer
at this point indicates an FFI/ABI contract violation that should fail loudly
rather than be hidden. Replace the fallback logic in the canonicalJSON
assignment (currently using the ?? operator to default to "") with a guard
statement that explicitly handles the null case as an error, ensuring callers
cannot persist an empty document body as if it were canonical and contract
violations are properly detected.

---

Outside diff comments:
In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentFieldsView.swift`:
- Around line 306-311: The `.onChange` handler sanitizes the byte-array input by
rewriting `textFields[property.name]` with the cleaned value, but this change is
not reflected back into the `fieldValues` dictionary that gets submitted. After
the assignment of the cleaned value to `textFields[property.name]`, call
`updateFieldValues()` again to ensure the sanitized value is synchronized into
`fieldValues` so the submitted data contains only valid hex characters.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentsView.swift`:
- Around line 241-247: While the Cancel button in the toolbar is disabled when
isSubmitting is true, the sheet can still be dismissed via swipe gesture during
the non-idempotent DocumentCreate broadcast, which risks losing the result. Add
the .interactiveDismissalDisabled(isSubmitting) modifier to the sheet to prevent
interactive swipe-to-dismiss gestures while the broadcast is in flight, ensuring
the user cannot accidentally dismiss the view until the submission is complete.

---

Nitpick comments:
In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift`:
- Around line 2594-2598: The lifetime contract documentation comment for the
signer parameter describes the keepalive pattern as `_ = signer`, but the actual
implementation uses `withExtendedLifetime(signer)` for safe lifetime extension.
Update the documentation comment to correctly describe the actual safe pattern
being used (withExtendedLifetime) instead of the old keepalive approach, and
remove the reference to `_ = signer` to prevent future implementations from
copying the unsafe pattern.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c72fa307-6676-4ca2-b209-dbef66a4d70f

📥 Commits

Reviewing files that changed from the base of the PR and between f647d58 and a469074.

📒 Files selected for processing (4)
  • packages/rs-platform-wallet-ffi/src/document.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentFieldsView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentsView.swift

- ManagedPlatformWallet.createDocument: a null canonical-JSON pointer
  after a successful broadcast is now thrown as a walletOperation error
  (FFI/ABI contract violation) instead of silently persisting an empty
  document body. Updated the signer lifetime-contract doc comment to
  describe withExtendedLifetime rather than the unsafe `_ = signer`.
- DocumentFieldsView: re-sync fieldValues after the byte-array hex
  sanitizer rewrites the field (direct @State mutation bypasses the
  binding setter), so the submitted value reflects the cleaned hex.
- CreateDocumentView: interactiveDismissDisabled(isSubmitting) so the
  sheet can't be swipe-dismissed during the non-idempotent broadcast.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@QuantumExplorer

Copy link
Copy Markdown
Member Author

Addressed the remaining CodeRabbit items from the last review in ac0a4f2 (the inline null-JSON one is replied/resolved on its thread; the rest were outside-diff / nitpick with no inline thread):

  • Null canonical JSON → throw (inline): now throw PlatformWalletError.walletOperation(...) instead of ?? "".
  • DocumentFieldsView byte-array sanitize (outside-diff): call updateFieldValues() after the .onChange hex cleaner rewrites the field, so the submitted value reflects the cleaned hex.
  • CreateDocumentView swipe-dismiss (outside-diff): added .interactiveDismissDisabled(isSubmitting) so the sheet can't be dismissed mid-broadcast.
  • Signer lifetime doc comment (nitpick): updated to describe withExtendedLifetime(signer) rather than the unsafe _ = signer.

build_ios.sh --target sim → BUILD SUCCEEDED.

@QuantumExplorer QuantumExplorer merged commit 4f25567 into v3.1-dev Jun 15, 2026
2 of 3 checks passed
@QuantumExplorer QuantumExplorer deleted the claude/peaceful-mahavira-0265a4 branch June 15, 2026 23:01

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Cumulative verification at head a469074. Prior finding prior-ae6ec628-1 is FIXED: the FFI now returns the canonical confirmed document JSON via an out-param and Swift persists that verbatim into PersistentDocument.data (verified in document.rs:96-145, ManagedPlatformWallet.swift:2599-2665, and DocumentsView.swift propertiesJSON / persistConfirmedDocument). No new blocking issues in the latest delta. Three new low-severity findings on the latest diff: a near-impossible CString::new failure that would silently drop a confirmed broadcast (nitpick), a defensive empty-string fallback in Swift that masks contract drift (nitpick), and a UI-layer limitation where non-byte arrays are always broadcast as string arrays regardless of schema item type (suggestion).

🟡 1 suggestion(s) | 💬 2 nitpick(s)

1 additional finding(s) omitted (not in diff).

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentFieldsView.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentFieldsView.swift:266-272: Non-byte array fields are broadcast as `[String]` regardless of schema item type
  The new production create flow builds `propertiesJSON` from whatever `DocumentFieldsView.fieldValues` emits, and for `array` fields that are not detected as byte arrays the value is always a comma-split `[String]` (line 269). `CreateDocumentView.propertiesJSON` then forwards those strings unchanged into the JSON payload. For any document type whose `items` schema is `integer`, `number`, `boolean`, `object`, or an identifier shape not classified as a byte array, the broadcast carries quoted strings where DPP expects typed elements; DPP's sanitize step does not coerce `"1"` into an integer array element, so otherwise-valid document types become uncreatable through this new production path. Parse array entries per the schema's `items.type` (or fall back to a raw JSON array editor for non-byte arrays so the submitted JSON preserves item types).

Comment on lines +130 to +145
let result = unwrap_option_or_return!(option);
let (document_id, document_json) = unwrap_result_or_return!(result);

// Allocate the owned C string for the JSON body. A NUL byte inside
// the JSON would be a serializer bug, but guard against it rather
// than panicking across the FFI boundary.
let json_cstring = unwrap_result_or_return!(CString::new(document_json));

let bytes = document_id.to_buffer();
let dst = slice::from_raw_parts_mut(out_document_id, 32);
dst.copy_from_slice(&bytes);
// Transfer ownership of the JSON to the caller (freed via
// `platform_wallet_string_free`). Written last so the id out-param
// and the JSON are populated together on the success path.
*out_document_json = json_cstring.into_raw();
PlatformWalletFFIResult::ok()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: Successful broadcast is reported as an error if CString::new sees an interior NUL; document id is never written

By the time CString::new(document_json) (line 136) runs, the state transition has already been broadcast and the confirmed document returned from Platform. If CString::new were to fail (only possible if the serialized JSON contains an interior NUL byte — a serde_json::to_string output bug, effectively never), unwrap_result_or_return! exits the FFI with an error and the 32-byte id copy on lines 138-140 never runs, so Swift sees a failed result for an on-chain success and has no id to recover from. Reorder so the id is written before the JSON allocation (or write the id even on JSON marshalling failure with a distinct error code), keeping the success/error signal honest in that degenerate case. Not blocking.

source: ['claude']

Comment on lines +2658 to +2663
defer { if let p = documentJsonPtr { platform_wallet_string_free(p) } }
// Defensive: on a successful broadcast the Rust side always
// writes the canonical JSON, but fall back to an empty
// string rather than crashing if it is unexpectedly null.
let canonicalJSON = documentJsonPtr.map { String(cString: $0) } ?? ""
return (Data(documentIdBytes), canonicalJSON)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: Silent empty-string fallback masks a future regression in the Rust JSON contract

After try result.check() confirms success, the wrapper returns documentJsonPtr.map { String(cString: $0) } ?? "". The documented Rust post-condition is that *out_document_json is non-null on success, so substituting an empty string when it is unexpectedly null hides any future drift in that contract — persistConfirmedDocument would then persist an empty dataBlob into SwiftData without surfacing anything via the existing persistWarning channel. Surface the null case explicitly (precondition, throw, or warning) so a contract violation is caught instead of silently degrading local-cache fidelity. Not blocking; today's Rust side does guarantee the post-condition.

source: ['claude']

@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.19%. Comparing base (e2039e5) to head (ac0a4f2).
⚠️ Report is 19 commits behind head on v3.1-dev.

Additional details and impacted files
@@             Coverage Diff              @@
##           v3.1-dev    #3908      +/-   ##
============================================
+ Coverage     71.20%   72.19%   +0.99%     
============================================
  Files            20       21       +1     
  Lines          2837     2946     +109     
============================================
+ Hits           2020     2127     +107     
- Misses          817      819       +2     
Components Coverage Δ
dpp ∅ <ø> (∅)
drive ∅ <ø> (∅)
drive-abci ∅ <ø> (∅)
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value ∅ <ø> (∅)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier ∅ <ø> (∅)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants