You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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_tree → wait_for_transaction; change response["transactionTree"]["eventsById"] (object) → response["transaction"]["events"] (array); change for (_key, event) in ... → for event in ...; rename event wrappers CreatedTreeEvent/ExercisedTreeEvent → CreatedEvent/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.0 → v0.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
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):
Expected: any HTTP status that is not000 (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.
- 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'
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.
- 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'
- 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'
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'
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'
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.
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.
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).
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());returnErr(format!("Failed at step {}: {}", step, e));}}}}}
Field-name reference (verified against current src/):
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).
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).
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());returnErr(format!("Failed at step {}: {}", step, e));}}}}}}
Field-name reference (verified against current src/credentials.rs):
FindUserServiceParams { ledger_host, party, access_token } — line 200.
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:
Confirm the tag now exists: gh release view v0.5.0 --repo DLC-link/canton-lib.
Edit Cargo.toml to swap all rev = "..." references back to tag = "v0.5.0".
cargo build to regenerate Cargo.lock.
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).
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 preservedEvery 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.
(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.
Implementation Plan
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 theparse_transfer_responsehelper incbtc-liboff the deprecated tree-shaped JSON Ledger API onto the flatwait_for_transaction(...)equivalent, bumping the four canton-lib deps fromv0.4.0tov0.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_tree→wait_for_transaction; changeresponse["transactionTree"]["eventsById"](object) →response["transaction"]["events"](array); changefor (_key, event) in ...→for event in ...; rename event wrappersCreatedTreeEvent/ExercisedTreeEvent→CreatedEvent/ExercisedEvent. Innervalue.*payloads stay byte-for-byte identical. The new helper auto-builds aTransactionFormatwithLEDGER_EFFECTSshape from the existingact_as/read_asparties, preserving event-content equivalence with the deprecated endpoint.Tech Stack: Rust 2021 edition,
tokio,serde_json,reqwest. Dependencies on local cratesledger,keycloak,registry,commonfromDLC-link/canton-lib. Tests viaexamples/integration_test.rsrun 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 from12(start) to0(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. Thebase_stepsdiffs in each are written for sequential application (18→19then19→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-libagainst a Canton devnet:rust-toolchain.toml(the repo pins it; runningcargotriggers the install if needed).ghCLI authenticated against theDLC-linkorg for the pre-flight tag check (step 1.1) and the PR-creation step (12.5).git@github.com:DLC-link/canton-lib.git(the fourCargo.tomldeps are pulled via SSH).examples/integration_test.rs(seeload_sender_configandload_receiver_configin that file for the canonical list):PARTY_ID,LEDGER_HOST,KEYCLOAK_CLIENT_ID,KEYCLOAK_USERNAME,KEYCLOAK_PASSWORD,KEYCLOAK_HOST,KEYCLOAK_REALM, plus the correspondingRECEIVER_*(where unset, fall back to the non-prefixed value), plusATTESTOR_URL,CANTON_NETWORK,BITSAFE_API_URL,REGISTRY_URL,DECENTRALIZED_PARTY_ID, and any optionalFAUCET_URL/WITHDRAW_AMOUNT/DESTINATION_BTC_ADDRESSthe existing test reads. If a devnet.envfile 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
Cargo.tomlv0.4.0 → v0.5.0Cargo.lockcargo buildsrc/accept.rssrc/cancel_offers.rssrc/transfer.rsparse_transfer_response(lines 592–639)src/mint_redeem/mint.rssrc/mint_redeem/redeem.rssrc/consolidate.rssrc/split.rssrc/credentials.rsREADME.md### Accept Transfer)examples/integration_test.rsbase_steps18 → 20, insert Step C (after step 3list_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.0→v0.5.0Goal: Update the four
Cargo.tomllines, regenerateCargo.lock, confirm baseline cleanly compiles, and quantify how many migration-warning hits remain.Files:
Modify:
Cargo.toml:11-14Modify:
Cargo.lock(regenerated)Step 1.1: Pre-flight — confirm canton-lib
v0.5.0tag existsgh 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, ORSubstitute
tag = "v0.5.0"withrev = "f68dd6fc66711d37bddc88e3b999771314ff809a"(the canton-lib#13 merge commit) throughout this task; the final commit on the migration branch must referencetag = "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):
Expected: any HTTP status that is not
000(connection failure),404(endpoint missing), or501(not implemented). Likely values for a malformed empty request are400,401,415, or422— all of which indicate the endpoint exists and is reachable. Halt and escalate only on000/404/501: the migration cannot be verified end-to-end until devnet is on a Canton build that supports the flat endpoint.Apply this exact diff to
Cargo.toml(lines 11–14):Cargo.lockExpected: 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.Expected:
12. If anything other than 12, the call-site grep in the design spec is wrong — re-grepsrc/forwait_for_transaction_treeand reconcile before proceeding.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.
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.rsGoal: 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:122andsrc/accept.rs:304Modify:
src/cancel_offers.rs:123andsrc/cancel_offers.rs:311Step 2.1: Edit
accept.rs:122(single-recipient accept)accept.rs:304(batch accept)cancel_offers.rs:123(single cancel)cancel_offers.rs:311(batch cancel)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.rsBoth should be
0.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:237transfer.rs:478parse_transfer_responsebody (lines 599–614)The remainder of
parse_transfer_response(thevalue.*reads inside theif choice == Some("TransferFactory_Transfer")block) is unchanged.Expected:
6.grep -c 'transactionTree\|wait_for_transaction_tree\|eventsById\|CreatedTreeEvent\|ExercisedTreeEvent' src/transfer.rsExpected:
0.git add src/transfer.rs git commit -m "refactor: migrate transfer flow to flat wait_for_transaction"Task 4: Migrate
mint.rsGoal: 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
mint.rs:195The underlying re-fetch workaround stays — the flat endpoint also omits
createdEventBlobin submit responses. Only the comment's reference to the old endpoint name is stale.Expected:
5.grep -c 'transactionTree\|wait_for_transaction_tree\|eventsById\|CreatedTreeEvent\|ExercisedTreeEvent' src/mint_redeem/mint.rsExpected:
0.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:189redeem.rs:457The rest of the loop body (constructing
JsActiveContractfromcreated_event_value["contractId"],createArgument,createdEventBlob, etc.) is unchanged.Expected:
3.grep -c 'transactionTree\|wait_for_transaction_tree\|eventsById\|CreatedTreeEvent\|ExercisedTreeEvent' src/mint_redeem/redeem.rsExpected:
0.git add src/mint_redeem/redeem.rs git commit -m "refactor: migrate redeem flow to flat wait_for_transaction"Task 6: Migrate
consolidate.rsGoal: Apply the full 4-edit recipe at the single submit site in
consolidate.rs.Files:
Modify:
src/consolidate.rs:246–280Step 6.1: Apply recipe
Expected:
2.grep -c 'transactionTree\|wait_for_transaction_tree\|eventsById\|CreatedTreeEvent\|ExercisedTreeEvent' src/consolidate.rsExpected:
0.git add src/consolidate.rs git commit -m "refactor: migrate consolidate to flat wait_for_transaction"Task 7: Migrate
split.rsGoal: Apply the full 4-edit recipe at the single submit site in
split.rs, including the error string at line 138 which still referencesExercisedTreeEvent.Files:
Modify:
src/split.rs:112–145Step 7.1: Apply recipe
Expected:
1.grep -c 'transactionTree\|wait_for_transaction_tree\|eventsById\|CreatedTreeEvent\|ExercisedTreeEvent' src/split.rsExpected:
0.git add src/split.rs git commit -m "refactor: migrate split to flat wait_for_transaction"Task 8: Migrate
credentials.rs— last call siteGoal: 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–445Step 8.1: Apply recipe
The rest of the loop body (the
JsActiveContract { ... }construction) is unchanged.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.
grep -rn 'transactionTree\|eventsById\|CreatedTreeEvent\|ExercisedTreeEvent' src/Expected: zero output. If any hits, apply the relevant edits to those locations before continuing.
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:571that still uses the tree endpoint and the old (unwrapped) body shape.Files:
Modify:
README.md:564–591(the### Accept Transferblock)Step 9.1: Re-grep the README for stale references
grep -n 'transactionTree\|eventsById\|submit-and-wait-for-transaction-tree\|CreatedTreeEvent\|ExercisedTreeEvent' README.mdExpected 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.Replace lines 571–591 (
# Submit acceptanceblock) with:The structural changes vs. the original block:
submit-and-wait-for-transaction-tree→submit-and-wait-for-transaction.commandsfield becomes an object (was an array) that contains the originalcommandsarray,commandId,actAs,disclosedContracts.transactionFormatwithTRANSACTION_SHAPE_LEDGER_EFFECTS,filtersByPartyfor the acting party, andverbose: true.grep -n 'transactionTree\|eventsById\|submit-and-wait-for-transaction-tree' README.mdExpected: zero output.
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::submitagainst devnet, so the migratedsplit.rsparser 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(bumpbase_steps)Modify:
examples/integration_test.rs(insert new step after the consolidate block ending around line 735)Step 10.1: Bump
base_steps(Step C in Task 11 will add another +1 later; Step S adds the first +1 here. After both tasks,
base_stepswill be 20.)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: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-libcrates/common/src/transfer.rs:26.Make sure
commonis brought into scope inintegration_test.rs(the existing transfer step already does this implicitly through thecbtc::transfer::Params { instrument_id: common::transfer::InstrumentId { ... } }builder around line 485 — if that compiles today, the new step will compile with the same path).Expected: zero errors. If field-name mismatches surface (per the note in 10.2), fix them inline and recompile.
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).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_offeragainst 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(bumpbase_stepsagain)Modify:
examples/integration_test.rs(insert new step after the existinglist_credentialsstep around line 300)Step 11.1: Bump
base_stepslist_credentialsFind the closing
});of therun_step!("Fetch Minter credentials", ...)block (around line 300). Immediately after that closing});, insert: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
Expected: zero errors. Fix any field-name mismatches that surface.
Without the env var — confirm step skips cleanly:
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:
Expected: 20-step run; the credential-accept step either passes (
(credential contract_id: ...)) or skips with(no credential offers available).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.
Expected:
0. Also check the log for any other unexpected warnings or errors:Triage anything new (warnings unrelated to the migration are acceptable if they existed before; new warnings from cbtc-lib sources are not).
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.Expected: all 20 base steps (plus 3 faucet steps if
FAUCET_URLis set) either pass or skip with documented reasons. No step fails.tag = "v0.5.0"(notrev)grep 'canton-lib' Cargo.tomlExpected: all four lines reference
tag = "v0.5.0". If any line is on arev = "..."fallback:Confirm the tag now exists:
gh release view v0.5.0 --repo DLC-link/canton-lib.Edit
Cargo.tomlto swap allrev = "..."references back totag = "v0.5.0".cargo buildto regenerateCargo.lock.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.0was tagged).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.
(Replace
<PR_URL>with the actual PR URL from step 12.5.)Out of scope (reiterated for the executor)
cbtc-lib.CHANGELOG.md(cbtc-lib doesn't maintain one).superpowers-docs/cleanup (separate operational cleanup the user flagged; not part of this PR).Risk reminders
v0.5.0is not yet tagged when you start, therevpin tof68dd6fc66711d37bddc88e3b999771314ff809ais the workaround. The final commit on this branch must referencetag = "v0.5.0"— re-verify in step 12.4 before opening the PR.cargo buildafter step 8 reports any non-zero deprecation-warning count from cbtc-lib sources, a call site was missed; re-grepsrc/forwait_for_transaction_treeand apply the 4-edit recipe at the missed location.