Skip to content

Plan: Migrate JSON API calls to flat submit-and-wait-for-transaction (design: #39, impl: #38) #40

@gyorgybalazsi

Description

@gyorgybalazsi

Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Design spec: #39 (Design: Migrate JSON API calls to flat submit-and-wait-for-transaction).
Implementation issue: #38.
Branch (set up at execution time, not part of this plan): feature/migrate-submit-and-wait-flat.

Goal: Migrate every ledger::submit::wait_for_transaction_tree(...) call site (12 sites) and the parse_transfer_response helper in cbtc-lib off the deprecated tree-shaped JSON Ledger API onto the flat wait_for_transaction(...) equivalent, bumping the four canton-lib deps from v0.4.0 to v0.5.0, updating one README curl example, and adding two new integration-test steps (split, credentials).

Architecture: Single mechanical-sweep PR. Each call site gets the same four edits: rename wait_for_transaction_treewait_for_transaction; change response["transactionTree"]["eventsById"] (object) → response["transaction"]["events"] (array); change for (_key, event) in ...for event in ...; rename event wrappers CreatedTreeEvent/ExercisedTreeEventCreatedEvent/ExercisedEvent. Inner value.* payloads stay byte-for-byte identical. The new helper auto-builds a TransactionFormat with LEDGER_EFFECTS shape from the existing act_as/read_as parties, preserving event-content equivalence with the deprecated endpoint.

Tech Stack: Rust 2021 edition, tokio, serde_json, reqwest. Dependencies on local crates ledger, keycloak, registry, common from DLC-link/canton-lib. Tests via examples/integration_test.rs run against a Canton devnet.

Verification gate at every migration task: cargo build 2>&1 | grep -c 'use of deprecated function.*wait_for_transaction_tree' should drop from 12 (start) to 0 (after Task 8). Specific expected counts per task listed inline.

Task ordering: Tasks 1–9 are independent step-by-step. Tasks 10 and 11 modify the same line (base_steps) and must be executed in numeric order — Task 10 before Task 11. The base_steps diffs in each are written for sequential application (18→19 then 19→20). If executed out of order the second task's diff won't apply.


Prerequisites (executor environment)

The plan assumes the executor has a working development environment for cbtc-lib against a Canton devnet:

  • Rust toolchain per rust-toolchain.toml (the repo pins it; running cargo triggers the install if needed).
  • gh CLI authenticated against the DLC-link org for the pre-flight tag check (step 1.1) and the PR-creation step (12.5).
  • SSH access to git@github.com:DLC-link/canton-lib.git (the four Cargo.toml deps are pulled via SSH).
  • A devnet environment file that exports every variable consumed by examples/integration_test.rs (see load_sender_config and load_receiver_config in that file for the canonical list): PARTY_ID, LEDGER_HOST, KEYCLOAK_CLIENT_ID, KEYCLOAK_USERNAME, KEYCLOAK_PASSWORD, KEYCLOAK_HOST, KEYCLOAK_REALM, plus the corresponding RECEIVER_* (where unset, fall back to the non-prefixed value), plus ATTESTOR_URL, CANTON_NETWORK, BITSAFE_API_URL, REGISTRY_URL, DECENTRALIZED_PARTY_ID, and any optional FAUCET_URL/WITHDRAW_AMOUNT/DESTINATION_BTC_ADDRESS the existing test reads. If a devnet .env file already exists locally, source it before running any of the cargo commands. Without these env vars, the pre-flight (step 1.2) and the integration-test runs (steps 10.4, 11.4, 12.3) cannot execute.

File Structure

File Modification
Cargo.toml Bump four canton-lib dep tags v0.4.0 → v0.5.0
Cargo.lock Regenerated by cargo build
src/accept.rs Edit A at lines 122 and 304 (submit-only, no parser change)
src/cancel_offers.rs Edit A at lines 123 and 311 (submit-only)
src/transfer.rs Edit A at lines 237 and 478 + Edits B/C/D in parse_transfer_response (lines 592–639)
src/mint_redeem/mint.rs Full 4-edit recipe at lines 160–189
src/mint_redeem/redeem.rs Full 4-edit recipe at lines 189–218 and 457–498
src/consolidate.rs Full 4-edit recipe at lines 246–280
src/split.rs Full 4-edit recipe at lines 112–145 (incl. one error string update at line 138)
src/credentials.rs Full 4-edit recipe at lines 411–445
README.md Replace one curl block at line 571 (### Accept Transfer)
examples/integration_test.rs Update base_steps 18 → 20, insert Step C (after step 3 list_credentials), insert Step S (after the existing consolidate step)

No new files. No new modules. No public-API changes.


Task 1: Bump canton-lib deps v0.4.0v0.5.0

Goal: Update the four Cargo.toml lines, regenerate Cargo.lock, confirm baseline cleanly compiles, and quantify how many migration-warning hits remain.

Files:

  • Modify: Cargo.toml:11-14

  • Modify: Cargo.lock (regenerated)

  • Step 1.1: Pre-flight — confirm canton-lib v0.5.0 tag exists

gh release view v0.5.0 --repo DLC-link/canton-lib --json tagName,publishedAt -q .

Expected: tag info prints. If the command errors with "release not found," halt and either:

  • Wait for the maintainer to cut v0.5.0, OR

  • Substitute tag = "v0.5.0" with rev = "f68dd6fc66711d37bddc88e3b999771314ff809a" (the canton-lib#13 merge commit) throughout this task; the final commit on the migration branch must reference tag = "v0.5.0" before opening the PR.

  • Step 1.2: Pre-flight — confirm devnet supports the flat endpoint

Without spending any code-change effort, hit the new endpoint on the target devnet to confirm it responds (any response other than HTTP 404/501 is fine — auth or shape errors are acceptable):

curl -s -o /dev/null -w '%{http_code}\n' -X POST "$LEDGER_HOST/v2/commands/submit-and-wait-for-transaction" \
  -H 'Content-Type: application/json' \
  -d '{}'

Expected: any HTTP status that is not 000 (connection failure), 404 (endpoint missing), or 501 (not implemented). Likely values for a malformed empty request are 400, 401, 415, or 422 — all of which indicate the endpoint exists and is reachable. Halt and escalate only on 000/404/501: the migration cannot be verified end-to-end until devnet is on a Canton build that supports the flat endpoint.

  • Step 1.3: Apply the Cargo.toml diff

Apply this exact diff to Cargo.toml (lines 11–14):

- ledger = { git = "ssh://git@github.com/DLC-link/canton-lib", tag = "v0.4.0" }
- keycloak = { git = "ssh://git@github.com/DLC-link/canton-lib", tag = "v0.4.0" }
- registry = { git = "ssh://git@github.com/DLC-link/canton-lib", tag = "v0.4.0" }
- common = { git = "ssh://git@github.com/DLC-link/canton-lib", tag = "v0.4.0" }
+ ledger = { git = "ssh://git@github.com/DLC-link/canton-lib", tag = "v0.5.0" }
+ keycloak = { git = "ssh://git@github.com/DLC-link/canton-lib", tag = "v0.5.0" }
+ registry = { git = "ssh://git@github.com/DLC-link/canton-lib", tag = "v0.5.0" }
+ common = { git = "ssh://git@github.com/DLC-link/canton-lib", tag = "v0.5.0" }
  • Step 1.4: Regenerate Cargo.lock
cargo build

Expected: builds successfully (Cargo.lock updated). The build emits 12 deprecation warnings ("use of deprecated function ledger::submit::wait_for_transaction_tree") from cbtc-lib sources. Any errors indicate canton-lib v0.5.0 broke an unrelated API surface (e.g., keycloak::login::*, ledger::active_contracts, ledger::ledger_end, common::decimal::DamlDecimal) — if that happens, stop and triage the breakage before continuing.

  • Step 1.5: Quantify the migration baseline
cargo build 2>&1 | grep -c 'use of deprecated function.*wait_for_transaction_tree'

Expected: 12. If anything other than 12, the call-site grep in the design spec is wrong — re-grep src/ for wait_for_transaction_tree and reconcile before proceeding.

  • Step 1.6: cargo check on all targets (catches any other v0.5.0 breakage)
cargo check --all-targets

Expected: zero errors (warnings are OK). Any error here points at a v0.5.0 incompatibility outside of submit-related code and must be triaged before any source-file edits in later tasks.

  • Step 1.7: Commit
git add Cargo.toml Cargo.lock
git commit -m "chore(deps): bump canton-lib v0.4.0 → v0.5.0"

Task 2: Migrate submit-only sites — accept.rs + cancel_offers.rs

Goal: Apply Edit A (function rename) at the four submit-only sites whose response bodies are discarded. No parser changes required.

Files:

  • Modify: src/accept.rs:122 and src/accept.rs:304

  • Modify: src/cancel_offers.rs:123 and src/cancel_offers.rs:311

  • Step 2.1: Edit accept.rs:122 (single-recipient accept)

-    ledger::submit::wait_for_transaction_tree(ledger::submit::Params {
+    ledger::submit::wait_for_transaction(ledger::submit::Params {
         ledger_host: params.ledger_host,
         access_token: params.access_token,
         request: submission_request,
     })
     .await?;
  • Step 2.2: Edit accept.rs:304 (batch accept)
-        match ledger::submit::wait_for_transaction_tree(ledger::submit::Params {
+        match ledger::submit::wait_for_transaction(ledger::submit::Params {
             ledger_host: params.ledger_host.clone(),
             access_token: auth.access_token.clone(),
             request: submission_request,
         })
         .await
         {
             Ok(_) => {
  • Step 2.3: Edit cancel_offers.rs:123 (single cancel)
-    ledger::submit::wait_for_transaction_tree(ledger::submit::Params {
+    ledger::submit::wait_for_transaction(ledger::submit::Params {
         ledger_host: params.ledger_host,
         access_token: params.access_token,
         request: submission_request,
     })
     .await?;
  • Step 2.4: Edit cancel_offers.rs:311 (batch cancel)
-        match ledger::submit::wait_for_transaction_tree(ledger::submit::Params {
+        match ledger::submit::wait_for_transaction(ledger::submit::Params {
             ledger_host: params.ledger_host.clone(),
             access_token: auth.access_token.clone(),
             request: submission_request,
         })
         .await
         {
             Ok(_) => {
  • Step 2.5: Verify deprecation count dropped by 4
cargo build 2>&1 | grep -c 'use of deprecated function.*wait_for_transaction_tree'

Expected: 8. If not 8, identify the off-count site by re-running grep on each touched file:

grep -c 'wait_for_transaction_tree' src/accept.rs src/cancel_offers.rs

Both should be 0.

  • Step 2.6: Commit
git add src/accept.rs src/cancel_offers.rs
git commit -m "refactor: migrate accept/cancel submit sites to wait_for_transaction"

Task 3: Migrate transfer flow — transfer.rs (incl. parse_transfer_response)

Goal: Apply Edit A at the two transfer submit sites, and apply Edits B/C/D inside parse_transfer_response.

Files:

  • Modify: src/transfer.rs:237 (single transfer)

  • Modify: src/transfer.rs:478 (sequential-chained transfer)

  • Modify: src/transfer.rs:592–639 (parse_transfer_response)

  • Step 3.1: Edit transfer.rs:237

-    ledger::submit::wait_for_transaction_tree(ledger::submit::Params {
+    ledger::submit::wait_for_transaction(ledger::submit::Params {
         ledger_host: params.ledger_host,
         access_token: params.access_token,
         request: submission_request,
     })
     .await?;
  • Step 3.2: Edit transfer.rs:478
-        match ledger::submit::wait_for_transaction_tree(ledger::submit::Params {
+        match ledger::submit::wait_for_transaction(ledger::submit::Params {
             ledger_host: params.ledger_host.clone(),
             access_token: current_token,
             request: submission_request,
         })
         .await
  • Step 3.3: Edit parse_transfer_response body (lines 599–614)
     // Extract update_id from the root level
-    let update_id = response["transactionTree"]["updateId"]
+    let update_id = response["transaction"]["updateId"]
         .as_str()
         .ok_or("Failed to find updateId in response")?
         .to_string();

-    let events_by_id = response["transactionTree"]["eventsById"]
-        .as_object()
-        .ok_or("Failed to find eventsById in response")?;
+    let events = response["transaction"]["events"]
+        .as_array()
+        .ok_or("Failed to find events in response")?;

-    // Find the ExercisedTreeEvent with TransferFactory_Transfer choice
+    // Find the ExercisedEvent with TransferFactory_Transfer choice
     let mut sender_change_cids = None;
     let mut transfer_offer_cid = None;

-    for (_key, event) in events_by_id {
-        if let Some(exercised_event) = event.get("ExercisedTreeEvent") {
+    for event in events {
+        if let Some(exercised_event) = event.get("ExercisedEvent") {

The remainder of parse_transfer_response (the value.* reads inside the if choice == Some("TransferFactory_Transfer") block) is unchanged.

  • Step 3.4: Verify
cargo build 2>&1 | grep -c 'use of deprecated function.*wait_for_transaction_tree'

Expected: 6.

grep -c 'transactionTree\|wait_for_transaction_tree\|eventsById\|CreatedTreeEvent\|ExercisedTreeEvent' src/transfer.rs

Expected: 0.

  • Step 3.5: Commit
git add src/transfer.rs
git commit -m "refactor: migrate transfer flow to flat wait_for_transaction"

Task 4: Migrate mint.rs

Goal: Apply the full 4-edit recipe at the single submit site in mint.rs (lines 160–189), and rewrite one stale comment at line 195 that refers to the deprecated endpoint by name.

Files:

  • Modify: src/mint_redeem/mint.rs:160–189 (4-edit recipe)

  • Modify: src/mint_redeem/mint.rs:195 (stale-comment update)

  • Step 4.1: Apply the recipe

-    let response_raw = submit::wait_for_transaction_tree(submit::Params {
+    let response_raw = submit::wait_for_transaction(submit::Params {
         ledger_host: params.ledger_host.clone(),
         access_token: params.access_token.clone(),
         request: submission_request,
     })
     .await?;

     // Parse the response to extract the contract ID of the created DepositAccount
     let response: serde_json::Value = serde_json::from_str(&response_raw)
         .map_err(|e| format!("Failed to parse submit response: {}", e))?;

-    let events_by_id = response["transactionTree"]["eventsById"]
-        .as_object()
-        .ok_or("Failed to find eventsById in transaction")?;
+    let events = response["transaction"]["events"]
+        .as_array()
+        .ok_or("Failed to find events in transaction")?;

     let mut created_contract_id: Option<String> = None;
-    for (_key, event) in events_by_id {
-        if let Some(created_event) = event.get("CreatedTreeEvent") {
+    for event in events {
+        if let Some(created_event) = event.get("CreatedEvent") {
             let template_id = created_event["value"]["templateId"].as_str().unwrap_or("");
             if template_id.ends_with(":CBTC.DepositAccount:CBTCDepositAccount") {
                 created_contract_id = Some(
                     created_event["value"]["contractId"]
                         .as_str()
                         .unwrap_or("")
                         .to_string(),
                 );
                 break;
             }
         }
     }
  • Step 4.2: Update the stale comment at mint.rs:195
-    // (the deprecated submit-and-wait-for-transaction-tree endpoint doesn't return it)
+    // (the JSON Ledger API submit response doesn't include createdEventBlob; we re-fetch from active contracts)

The underlying re-fetch workaround stays — the flat endpoint also omits createdEventBlob in submit responses. Only the comment's reference to the old endpoint name is stale.

  • Step 4.3: Verify
cargo build 2>&1 | grep -c 'use of deprecated function.*wait_for_transaction_tree'

Expected: 5.

grep -c 'transactionTree\|wait_for_transaction_tree\|eventsById\|CreatedTreeEvent\|ExercisedTreeEvent' src/mint_redeem/mint.rs

Expected: 0.

  • Step 4.4: Commit
git add src/mint_redeem/mint.rs
git commit -m "refactor: migrate mint flow to flat wait_for_transaction"

Task 5: Migrate redeem.rs (two hunks)

Goal: Apply the full 4-edit recipe at the two submit sites in redeem.rs (lines 189 and 457).

Files:

  • Modify: src/mint_redeem/redeem.rs:189–218 (CreateWithdrawAccount)

  • Modify: src/mint_redeem/redeem.rs:457–498 (Withdraw)

  • Step 5.1: Apply recipe at redeem.rs:189

-    let response_raw = submit::wait_for_transaction_tree(submit::Params {
+    let response_raw = submit::wait_for_transaction(submit::Params {
         ledger_host: params.ledger_host.clone(),
         access_token: params.access_token.clone(),
         request: submission_request,
     })
     .await?;

     // Parse the response to extract the contract ID of the created WithdrawAccount
     let response: serde_json::Value = serde_json::from_str(&response_raw)
         .map_err(|e| format!("Failed to parse submit response: {}", e))?;

-    let events_by_id = response["transactionTree"]["eventsById"]
-        .as_object()
-        .ok_or("Failed to find eventsById in transaction")?;
+    let events = response["transaction"]["events"]
+        .as_array()
+        .ok_or("Failed to find events in transaction")?;

     let mut created_contract_id: Option<String> = None;
-    for (_key, event) in events_by_id {
-        if let Some(created_event) = event.get("CreatedTreeEvent") {
+    for event in events {
+        if let Some(created_event) = event.get("CreatedEvent") {
             let template_id = created_event["value"]["templateId"].as_str().unwrap_or("");
             if template_id.ends_with(":CBTC.WithdrawAccount:CBTCWithdrawAccount") {
  • Step 5.2: Apply recipe at redeem.rs:457
-    let response_raw = submit::wait_for_transaction_tree(submit::Params {
+    let response_raw = submit::wait_for_transaction(submit::Params {
         ledger_host: params.ledger_host.clone(),
         access_token: params.access_token.clone(),
         request: submission_request,
     })
     .await?;

     // Parse the response to extract the updated WithdrawAccount
     let response: serde_json::Value = serde_json::from_str(&response_raw)
         .map_err(|e| format!("Failed to parse submit response: {}", e))?;

-    // Extract the created WithdrawAccount from eventsById
+    // Extract the created WithdrawAccount from the flat events array
     // The Withdraw choice consumes the old account and creates a new one with updated pending_balance
-    let events_by_id = response["transactionTree"]["eventsById"]
-        .as_object()
-        .ok_or("Failed to find eventsById in transaction")?;
+    let events = response["transaction"]["events"]
+        .as_array()
+        .ok_or("Failed to find events in transaction")?;

-    for (_key, event) in events_by_id {
-        if let Some(created_event) = event.get("CreatedTreeEvent") {
+    for event in events {
+        if let Some(created_event) = event.get("CreatedEvent") {
             let template_id = created_event["value"]["templateId"].as_str().unwrap_or("");

The rest of the loop body (constructing JsActiveContract from created_event_value["contractId"], createArgument, createdEventBlob, etc.) is unchanged.

  • Step 5.3: Verify
cargo build 2>&1 | grep -c 'use of deprecated function.*wait_for_transaction_tree'

Expected: 3.

grep -c 'transactionTree\|wait_for_transaction_tree\|eventsById\|CreatedTreeEvent\|ExercisedTreeEvent' src/mint_redeem/redeem.rs

Expected: 0.

  • Step 5.4: Commit
git add src/mint_redeem/redeem.rs
git commit -m "refactor: migrate redeem flow to flat wait_for_transaction"

Task 6: Migrate consolidate.rs

Goal: Apply the full 4-edit recipe at the single submit site in consolidate.rs.

Files:

  • Modify: src/consolidate.rs:246–280

  • Step 6.1: Apply recipe

-    let response_raw = ledger::submit::wait_for_transaction_tree(ledger::submit::Params {
+    let response_raw = ledger::submit::wait_for_transaction(ledger::submit::Params {
         ledger_host: params.ledger_host,
         access_token: params.access_token,
         request: submission_request,
     })
     .await?;

     // Parse the response to extract the resulting holding CID(s)
     let response: serde_json::Value = serde_json::from_str(&response_raw)
         .map_err(|e| format!("Failed to parse submit response: {e}"))?;

-    // Find the ExercisedTreeEvent in eventsById
-    let events_by_id = response["transactionTree"]["eventsById"]
-        .as_object()
-        .ok_or("Failed to find eventsById")?;
+    // Find the ExercisedEvent in the flat events array
+    let events = response["transaction"]["events"]
+        .as_array()
+        .ok_or("Failed to find events")?;

     let mut result_cids = Vec::new();
-    for (_key, event) in events_by_id {
-        if let Some(exercised_event) = event.get("ExercisedTreeEvent") {
+    for event in events {
+        if let Some(exercised_event) = event.get("ExercisedEvent") {
             if let Some(result) = exercised_event["value"]["exerciseResult"].as_object() {
  • Step 6.2: Verify
cargo build 2>&1 | grep -c 'use of deprecated function.*wait_for_transaction_tree'

Expected: 2.

grep -c 'transactionTree\|wait_for_transaction_tree\|eventsById\|CreatedTreeEvent\|ExercisedTreeEvent' src/consolidate.rs

Expected: 0.

  • Step 6.3: Commit
git add src/consolidate.rs
git commit -m "refactor: migrate consolidate to flat wait_for_transaction"

Task 7: Migrate split.rs

Goal: Apply the full 4-edit recipe at the single submit site in split.rs, including the error string at line 138 which still references ExercisedTreeEvent.

Files:

  • Modify: src/split.rs:112–145

  • Step 7.1: Apply recipe

-    let response_raw = ledger::submit::wait_for_transaction_tree(ledger::submit::Params {
+    let response_raw = ledger::submit::wait_for_transaction(ledger::submit::Params {
         ledger_host,
         access_token,
         request: submission_request,
     })
     .await?;

     // Parse the response to extract the output and change holding CIDs
     let response: serde_json::Value = serde_json::from_str(&response_raw)
         .map_err(|e| format!("Failed to parse submit response: {e}"))?;

-    // Find the ExercisedTreeEvent in eventsById
-    let events_by_id = response["transactionTree"]["eventsById"]
-        .as_object()
-        .ok_or("Failed to find eventsById")?;
+    // Find the ExercisedEvent in the flat events array
+    let events = response["transaction"]["events"]
+        .as_array()
+        .ok_or("Failed to find events")?;

     let mut exercise_result = None;
-    for (_key, event) in events_by_id {
-        if let Some(exercised_event) = event.get("ExercisedTreeEvent") {
+    for event in events {
+        if let Some(exercised_event) = event.get("ExercisedEvent") {
             if let Some(result) = exercised_event["value"]["exerciseResult"].as_object() {
                 exercise_result = Some(result);
                 break;
             }
         }
     }

-    let exercise_result = exercise_result.ok_or("Failed to find ExercisedTreeEvent")?;
+    let exercise_result = exercise_result.ok_or("Failed to find ExercisedEvent")?;
  • Step 7.2: Verify
cargo build 2>&1 | grep -c 'use of deprecated function.*wait_for_transaction_tree'

Expected: 1.

grep -c 'transactionTree\|wait_for_transaction_tree\|eventsById\|CreatedTreeEvent\|ExercisedTreeEvent' src/split.rs

Expected: 0.

  • Step 7.3: Commit
git add src/split.rs
git commit -m "refactor: migrate split to flat wait_for_transaction"

Task 8: Migrate credentials.rs — last call site

Goal: Apply the full 4-edit recipe at the single submit site in credentials.rs, completing the migration (deprecation-warning count from cbtc-lib must reach 0).

Files:

  • Modify: src/credentials.rs:411–445

  • Step 8.1: Apply recipe

-    let response_raw = submit::wait_for_transaction_tree(submit::Params {
+    let response_raw = submit::wait_for_transaction(submit::Params {
         ledger_host: params.ledger_host.clone(),
         access_token: params.access_token.clone(),
         request: submission_request,
     })
     .await?;

     let response: serde_json::Value = serde_json::from_str(&response_raw)
         .map_err(|e| format!("Failed to parse submit response: {}", e))?;

-    let events_by_id = response["transactionTree"]["eventsById"]
-        .as_object()
-        .ok_or("Failed to find eventsById in transaction")?;
+    let events = response["transaction"]["events"]
+        .as_array()
+        .ok_or("Failed to find events in transaction")?;

-    for (_key, event) in events_by_id {
-        if let Some(created_event) = event.get("CreatedTreeEvent") {
+    for event in events {
+        if let Some(created_event) = event.get("CreatedEvent") {
             let template_id = created_event["value"]["templateId"].as_str().unwrap_or("");

             if template_id.ends_with(":Utility.Credential.V0.Credential:Credential") {

The rest of the loop body (the JsActiveContract { ... } construction) is unchanged.

  • Step 8.2: Verify deprecation count is now zero
cargo build 2>&1 | grep -c 'use of deprecated function.*wait_for_transaction_tree'

Expected: 0. This is the migration completion gate — if anything other than 0, a call site was missed.

If non-zero, locate the remaining sites:

grep -rn 'wait_for_transaction_tree' src/

…and apply Edit A (and parser edits B/C/D if applicable) to each before continuing.

  • Step 8.3: Verify all parser references are gone
grep -rn 'transactionTree\|eventsById\|CreatedTreeEvent\|ExercisedTreeEvent' src/

Expected: zero output. If any hits, apply the relevant edits to those locations before continuing.

  • Step 8.4: Commit
git add src/credentials.rs
git commit -m "refactor: migrate credentials accept to flat wait_for_transaction"

Task 9: Update README curl example

Goal: Update the single curl example at README.md:571 that still uses the tree endpoint and the old (unwrapped) body shape.

Files:

  • Modify: README.md:564–591 (the ### Accept Transfer block)

  • Step 9.1: Re-grep the README for stale references

grep -n 'transactionTree\|eventsById\|submit-and-wait-for-transaction-tree\|CreatedTreeEvent\|ExercisedTreeEvent' README.md

Expected at this moment: one hit at line 571 (submit-and-wait-for-transaction-tree). If additional hits exist in other curl blocks or sample response sections, apply analogous updates to each — those weren't pre-spec'd in the design but the design's risk row covers them.

  • Step 9.2: Apply the curl-block update

Replace lines 571–591 (# Submit acceptance block) with:

# Submit acceptance (use disclosed contracts from above)
curl -X POST $LEDGER_HOST/v2/commands/submit-and-wait-for-transaction \
  -H "Authorization: Bearer $RECEIVER_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "commands": {
      "commands": [{
        "ExerciseCommand": {
          "templateId": "#splice-api-token-transfer-instruction-v1:Splice.Api.Token.TransferInstructionV1:TransferInstruction",
          "contractId": "'$TRANSFER_OFFER_CID'",
          "choice": "TransferInstruction_Accept",
          "choiceArgument": {
            "extraArgs": {
              "context": {"values": '$CHOICE_CONTEXT_VALUES'},
              "meta": {"values": {}}
            }
          }
        }
      }],
      "commandId": "'$(uuidgen)'",
      "actAs": ["'$RECEIVER_PARTY'"],
      "disclosedContracts": '$DISCLOSED_CONTRACTS'
    },
    "transactionFormat": {
      "transactionShape": "TRANSACTION_SHAPE_LEDGER_EFFECTS",
      "eventFormat": {
        "filtersByParty": {"'$RECEIVER_PARTY'": {}},
        "verbose": true
      }
    }
  }' | jq

The structural changes vs. the original block:

  1. URL: submit-and-wait-for-transaction-treesubmit-and-wait-for-transaction.
  2. Top-level commands field becomes an object (was an array) that contains the original commands array, commandId, actAs, disclosedContracts.
  3. New sibling field transactionFormat with TRANSACTION_SHAPE_LEDGER_EFFECTS, filtersByParty for the acting party, and verbose: true.
  • Step 9.3: Verify
grep -n 'transactionTree\|eventsById\|submit-and-wait-for-transaction-tree' README.md

Expected: zero output.

  • Step 9.4: Commit
git add README.md
git commit -m "docs(readme): update Accept Transfer curl to flat endpoint"

Task 10: Integration test — Step S (split sender holding)

Goal: Add a new step that exercises split::submit against devnet, so the migrated split.rs parser path is exercised end-to-end. Insertion: after the existing consolidate step (currently the last step before the summary).

Files:

  • Modify: examples/integration_test.rs:210 (bump base_steps)

  • Modify: examples/integration_test.rs (insert new step after the consolidate block ending around line 735)

  • Step 10.1: Bump base_steps

-    let base_steps: usize = 18;
+    let base_steps: usize = 19;
     let total_steps = base_steps + if faucet_url.is_some() { 3 } else { 0 };

(Step C in Task 11 will add another +1 later; Step S adds the first +1 here. After both tasks, base_steps will be 20.)

  • Step 10.2: Insert the split step after consolidate

Find the line print_summary(passed, total_steps, start.elapsed().as_secs_f64()); that follows the consolidate block (around line 738). Immediately before that summary line, insert:

    // Step 19: Split sender holding (exercises split.rs parser path end-to-end)
    {
        step += 1;
        print_step(step, total_steps, "Split sender holding");
        let token = authenticate(&sender)
            .await
            .map_err(|e| format!("Auth failed: {}", e))?;

        // List current holdings; pick the first one to split. Skip if none available.
        let holdings = cbtc::mint_redeem::redeem::list_holdings(
            cbtc::mint_redeem::redeem::ListHoldingsParams {
                ledger_host: sender.ledger_host.clone(),
                party: sender.party_id.clone(),
                access_token: token.clone(),
            },
        )
        .await
        .map_err(|e| format!("Failed to list holdings for split: {}", e))?;

        match holdings.first() {
            None => {
                print_skip("(no holdings available to split)");
            }
            Some(holding) => {
                // Split the holding into one output worth half its value; the rest becomes change.
                let half = holding.amount.clone() / cbtc::DamlDecimal::parse("2").unwrap();

                let split_params = cbtc::split::Params {
                    party: sender.party_id.clone(),
                    // InstrumentId is { admin, id } — Holding only carries `id` as a String,
                    // so reconstruct using decentralized_party_id as admin (same as the existing
                    // transfer steps at integration_test.rs:485-488).
                    instrument_id: common::transfer::InstrumentId {
                        admin: decentralized_party_id.clone(),
                        id: "CBTC".to_string(),
                    },
                    input_holding_cids: vec![holding.contract_id.clone()],
                    amounts: vec![half],
                    ledger_host: sender.ledger_host.clone(),
                    access_token: token,
                    registry_url: registry_url.clone(),
                    decentralized_party_id: decentralized_party_id.clone(),
                };

                match cbtc::split::submit(split_params).await {
                    Ok(result) => {
                        print_ok(&format!(
                            "({} output(s), {} change UTXO(s))",
                            result.output_holding_cids.len(),
                            result.change_holding_cids.len()
                        ));
                        passed += 1;
                    }
                    Err(e) => {
                        print_fail(&e);
                        print_summary(passed, total_steps, start.elapsed().as_secs_f64());
                        return Err(format!("Failed at step {}: {}", step, e));
                    }
                }
            }
        }
    }

Field-name reference (verified against current src/):

  • cbtc::mint_redeem::redeem::ListHoldingsParams { ledger_host, party, access_token }src/mint_redeem/redeem.rs:39.
  • cbtc::mint_redeem::models::Holding { contract_id: String, amount: DamlDecimal, instrument_id: String, owner: String }src/mint_redeem/models.rs:329.
  • cbtc::split::Params { party, amounts: Vec<DamlDecimal>, instrument_id: common::transfer::InstrumentId, input_holding_cids: Vec<String>, ledger_host, access_token, registry_url, decentralized_party_id }src/split.rs:4.
  • cbtc::split::SplitResult { output_holding_cids: Vec<String>, change_holding_cids: Vec<String> }src/split.rs:15.
  • common::transfer::InstrumentId { admin: String, id: String } — canton-lib crates/common/src/transfer.rs:26.

Make sure common is brought into scope in integration_test.rs (the existing transfer step already does this implicitly through the cbtc::transfer::Params { instrument_id: common::transfer::InstrumentId { ... } } builder around line 485 — if that compiles today, the new step will compile with the same path).

  • Step 10.3: Compile-check
cargo check --example integration_test

Expected: zero errors. If field-name mismatches surface (per the note in 10.2), fix them inline and recompile.

  • Step 10.4: Run against devnet (optional but recommended before commit)
cargo run --example integration_test

Expected: all 19 steps either pass or cleanly skip. The new "Split sender holding" step should pass (or skip with (no holdings available to split) if the sender has been fully drained by earlier steps).

  • Step 10.5: Commit
git add examples/integration_test.rs
git commit -m "test(integration): add split-holding step covering split.rs migration"

Task 11: Integration test — Step C (accept free credential offer)

Goal: Add a new step that exercises credentials::accept_credential_offer against devnet, gated by an env var because credential acceptance creates persistent ledger contracts with no archive choice. Insertion: after the existing "Fetch Minter credentials" step (currently step 3 around lines 270–300).

Files:

  • Modify: examples/integration_test.rs:210 (bump base_steps again)

  • Modify: examples/integration_test.rs (insert new step after the existing list_credentials step around line 300)

  • Step 11.1: Bump base_steps

-    let base_steps: usize = 19;
+    let base_steps: usize = 20;
     let total_steps = base_steps + if faucet_url.is_some() { 3 } else { 0 };
  • Step 11.2: Insert the credential-accept step after list_credentials

Find the closing }); of the run_step!("Fetch Minter credentials", ...) block (around line 300). Immediately after that closing });, insert:

Decision on stale // Step N: comments. Inserting Step C as the new step 4 shifts every subsequent runtime step number by +1. The existing // Step 4: through // Step 17: comments throughout the file (~17 occurrences from line 261 onward) will become numerically off-by-one with respect to the runtime step counter. Leave them as-is. Rationale: (a) the dynamic step variable passed to print_step is the source of truth for what users see; (b) the existing comments are already context-dependent (lines 377 and 477 both say // Step 8: for different code paths under the faucet vs non-faucet branch); (c) renumbering 17 lines is mechanical churn unrelated to the migration. Treat the numeric prefixes as decorative labels that have been imprecise since PR #31 and stay that way.

    // Step 4: Accept free credential offer (gated by RUN_CREDENTIAL_ACCEPT)
    // Exercises credentials.rs:411 parser path. Off by default — accepting a free
    // credential creates a persistent on-ledger contract with no archive choice,
    // so repeated test runs would accumulate state. Set RUN_CREDENTIAL_ACCEPT=1
    // to opt in (same pattern as FAUCET_URL for the optional faucet steps).
    {
        step += 1;
        print_step(step, total_steps, "Accept free credential offer (sender)");
        if env::var("RUN_CREDENTIAL_ACCEPT").ok().as_deref() != Some("1") {
            print_skip("(RUN_CREDENTIAL_ACCEPT not set)");
        } else {
            let token = authenticate(&sender)
                .await
                .map_err(|e| format!("Auth failed: {}", e))?;

            let user_service = cbtc::credentials::find_user_service(
                cbtc::credentials::FindUserServiceParams {
                    ledger_host: sender.ledger_host.clone(),
                    party: sender.party_id.clone(),
                    access_token: token.clone(),
                },
            )
            .await
            .map_err(|e| format!("Failed to find UserService: {}", e))?;

            let offers = cbtc::credentials::list_credential_offers(
                cbtc::credentials::ListCredentialOffersParams {
                    ledger_host: sender.ledger_host.clone(),
                    party: sender.party_id.clone(),
                    access_token: token.clone(),
                },
            )
            .await
            .map_err(|e| format!("Failed to list credential offers: {}", e))?;

            match offers.first() {
                None => {
                    print_skip("(no credential offers available)");
                }
                Some(offer) => {
                    match cbtc::credentials::accept_credential_offer(
                        cbtc::credentials::AcceptCredentialOfferParams {
                            ledger_host: sender.ledger_host.clone(),
                            party: sender.party_id.clone(),
                            access_token: token,
                            user_service_template_id: user_service.template_id.clone(),
                            user_service_contract_id: user_service.contract_id.clone(),
                            credential_offer_cid: offer.contract_id.clone(),
                        },
                    )
                    .await
                    {
                        Ok(credential) => {
                            print_ok(&format!("(credential contract_id: {})", credential.contract_id));
                            passed += 1;
                        }
                        Err(e) => {
                            print_fail(&e);
                            print_summary(passed, total_steps, start.elapsed().as_secs_f64());
                            return Err(format!("Failed at step {}: {}", step, e));
                        }
                    }
                }
            }
        }
    }

Field-name reference (verified against current src/credentials.rs):

  • FindUserServiceParams { ledger_host, party, access_token } — line 200.

  • UserServiceInfo { contract_id: String, template_id: String, operator: String, user: String, dso: String } — line 167.

  • ListCredentialOffersParams { ledger_host, party, access_token } — line 176.

  • CredentialOffer { contract_id: String, template_id, created_event_blob, issuer, holder, id, description, claims } — line 28.

  • AcceptCredentialOfferParams { ledger_host, party, access_token, user_service_contract_id, user_service_template_id, credential_offer_cid } — line 190.

  • UserCredential { contract_id: String, template_id, issuer, holder, id, description, claims } — line 99.

  • Step 11.3: Compile-check

cargo check --example integration_test

Expected: zero errors. Fix any field-name mismatches that surface.

  • Step 11.4: Run against devnet (with and without env var)

Without the env var — confirm step skips cleanly:

cargo run --example integration_test

Expected: 20-step run; the new credential-accept step prints [SKIP] (RUN_CREDENTIAL_ACCEPT not set) and the test continues.

With the env var — confirm it works when opted in:

RUN_CREDENTIAL_ACCEPT=1 cargo run --example integration_test

Expected: 20-step run; the credential-accept step either passes ((credential contract_id: ...)) or skips with (no credential offers available).

  • Step 11.5: Commit
git add examples/integration_test.rs
git commit -m "test(integration): add credential-accept step covering credentials.rs migration"

Task 12: Final verification and PR

Goal: Run the full ship-checklist gates one last time on the migration branch, then open the PR.

  • Step 12.1: Full clean rebuild — zero cbtc-lib deprecation warnings
cargo clean
cargo build 2>&1 | tee /tmp/cbtc-migration-build.log | grep -c 'use of deprecated function.*wait_for_transaction_tree'

Expected: 0. Also check the log for any other unexpected warnings or errors:

grep -E 'error|warning' /tmp/cbtc-migration-build.log | head -50

Triage anything new (warnings unrelated to the migration are acceptable if they existed before; new warnings from cbtc-lib sources are not).

  • Step 12.2: Clippy gate (if the project enables it)
cargo clippy --all-targets -- -D warnings

Expected: zero warnings (and therefore zero errors from -D warnings). If the project doesn't use clippy in CI, this step is informational; if it does, this must pass before merging.

  • Step 12.3: Integration-test smoke run
cargo run --example integration_test

Expected: all 20 base steps (plus 3 faucet steps if FAUCET_URL is set) either pass or skip with documented reasons. No step fails.

  • Step 12.4: Confirm canton-lib pin is on tag = "v0.5.0" (not rev)
grep 'canton-lib' Cargo.toml

Expected: all four lines reference tag = "v0.5.0". If any line is on a rev = "..." fallback:

  1. Confirm the tag now exists: gh release view v0.5.0 --repo DLC-link/canton-lib.

  2. Edit Cargo.toml to swap all rev = "..." references back to tag = "v0.5.0".

  3. cargo build to regenerate Cargo.lock.

  4. Make a new follow-up commit (do not amend any previous commit — per project conventions, always create new commits rather than amending):

    git add Cargo.toml Cargo.lock
    git commit -m "chore(deps): repin canton-lib from rev to tag v0.5.0"

    The branch will then contain an extra commit at the tip; that's fine. If you want a cleaner history at merge time, the PR is squash-friendly; otherwise leave the two-commit story intact (it documents that the migration started before v0.5.0 was tagged).

  • Step 12.5: Push the branch and open the PR
git push -u origin feature/migrate-submit-and-wait-flat
gh pr create --repo DLC-link/cbtc-lib \
  --title 'feat: migrate JSON API calls off deprecated submit-and-wait-for-transaction-tree' \
  --body "$(cat <<'EOF'
Closes #38. Design: #39.

## Summary
- Bumps canton-lib v0.4.0 → v0.5.0 (all four crates).
- Migrates 12 submit call sites + `parse_transfer_response` from the deprecated `wait_for_transaction_tree` (tree endpoint) to the flat `wait_for_transaction` equivalent. Parsing switches from `transactionTree.eventsById` (object) to `transaction.events` (array); event wrappers rename `CreatedTreeEvent`/`ExercisedTreeEvent` → `CreatedEvent`/`ExercisedEvent`. Inner payloads unchanged.
- Updates the README "Accept Transfer" curl example to the flat endpoint + wrapped body shape.
- Adds two integration-test steps: split-sender-holding (covers `split.rs`) and accept-free-credential-offer (covers `credentials.rs`, gated by `RUN_CREDENTIAL_ACCEPT=1`).

## Architectural invariant preserved
Every public `cbtc-lib` function returns the same Rust types with the same data under the same error conditions. Only the network endpoint and the internal JSON parsing path change.

## Test plan
- [x] `cargo build` — zero deprecation warnings from cbtc-lib sources.
- [x] `cargo check --all-targets` — clean.
- [x] `cargo run --example integration_test` against devnet — all 20 steps pass or cleanly skip.
- [x] `RUN_CREDENTIAL_ACCEPT=1 cargo run --example integration_test` against devnet — credential-accept step runs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"

Expected: the command prints the PR URL. Open it in the browser to confirm the description renders correctly and that all 8–10 commits are present.

  • Step 12.6: Comment on the design and impl issues
gh issue comment 39 --repo DLC-link/cbtc-lib --body "Implementation PR opened: <PR_URL>"
gh issue comment 38 --repo DLC-link/cbtc-lib --body "Implementation PR opened: <PR_URL>"

(Replace <PR_URL> with the actual PR URL from step 12.5.)


Out of scope (reiterated for the executor)

  • No new public functions or types in cbtc-lib.
  • No parsing-helper extraction (keep inline parser loops at every site — they were already there, just need the 4 mechanical edits).
  • No canton-lib code changes.
  • No CHANGELOG.md (cbtc-lib doesn't maintain one).
  • No superpowers-docs/ cleanup (separate operational cleanup the user flagged; not part of this PR).
  • No dual-path / fallback / migration-window code: Canton 3.4.x keeps both endpoints alive, so we flip atomically.

Risk reminders

  • If canton-lib v0.5.0 is not yet tagged when you start, the rev pin to f68dd6fc66711d37bddc88e3b999771314ff809a is the workaround. The final commit on this branch must reference tag = "v0.5.0" — re-verify in step 12.4 before opening the PR.
  • If devnet is on a Canton build that pre-dates the flat endpoint, the pre-flight in step 1.2 catches it. Don't skip step 1.2.
  • If cargo build after step 8 reports any non-zero deprecation-warning count from cbtc-lib sources, a call site was missed; re-grep src/ for wait_for_transaction_tree and apply the 4-edit recipe at the missed location.

Metadata

Metadata

Assignees

No one assigned

    Labels

    maintenanceMaintenance, deps, and upgrade workplanImplementation plans (precede the PR)

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions