From 6db29c018de502a39441a23e0d6b8a3dcbd161a7 Mon Sep 17 00:00:00 2001 From: scolear Date: Fri, 15 May 2026 14:43:56 +0200 Subject: [PATCH 1/5] chore: bump canton-api-client to 3.6.0-0.1.0 Note: this PR also requires a new canton-lib tag containing the same canton-api-client bump in its `common` crate. Blocked on DLC-link/canton-lib#18 and the follow-up release. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5ca43b8..a5adead 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ futures = "0.3" dotenvy = "0.15" base64 = "0.22" log = "0.4" -canton-api-client = "3.3.0-0.1.0" +canton-api-client = "3.6.0-0.1.0" reqwest = { version = "0.12.24", features = ["json"] } zip = "2" semver = "1" From a357cb97f4f275d313192fcd667df4f3de307b40 Mon Sep 17 00:00:00 2001 From: scolear Date: Fri, 15 May 2026 15:14:24 +0200 Subject: [PATCH 2/5] chore: adapt to canton-api-client 3.6.0 spec changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adapts cbtc-lib to the spec deltas introduced in Canton 3.6.0 — many previously required fields are now optional, `create_argument` is no longer double-wrapped (`Option>` → `Option`), `transaction.events` and `package_ids` are now required (no longer `Option`), `created_event_blob` is now `Option`, `CreatedEvent` gained required `representativePackageId`/`acsDelta` fields, and `ExercisedEvent` gained `acsDelta`. Mechanical changes: - accept/cancel_offers/utils/list_incoming_offers/list_outgoing_offers: drop the outer `Some(...)` from `if let Some(Some(create_arg)) = ...` patterns on `create_argument`. - credentials/mint_redeem/models: drop the `.and_then(|opt| opt.as_ref())` middle step from the `create_argument.as_ref().and_then(...).and_then(|v| v.as_object())` chains used to read the create-argument map. - credentials/mint_redeem/models: `created_event.created_event_blob` is now `Option`; `unwrap_or_default()` to preserve the existing `String` surface on the local types. - consolidate/split/transfer/mint_redeem/credentials parsers: `transaction.events` is no longer `Option>`; drop the `.as_ref().ok_or("Failed to find events")?` indirection and iterate the Vec directly. - consolidate/split/transfer parsers: `ExercisedEvent.exercise_result` is `Option>` in 3.6 — match `Some(Some(result))` rather than the single-layer pattern that used to compile against 3.3. - dar_check: `ListPackagesResponse.package_ids` is now a plain `Vec`; drop the `.ok_or_else(...)` that handled the previous optional wrapping. - utils test fixtures: include the new required `representativePackageId`/`acsDelta` fields on `CreatedEvent`, `acsDelta` on `ExercisedEvent`, and always emit `events: []` (rather than omitting the field) since `JsTransaction.events` is required. - parser unit tests for the "missing events" case: update assertions to the new error messages each parser returns when given an empty events list (the old "Failed to find events" branch no longer exists, since the typed model guarantees events is present). NOTE: this commit temporarily points the four canton-lib deps at the `chore/bump-canton-api-client-3.6.0` branch so the workspace can resolve end-to-end against the matching canton-api-client 3.6.0 bump there. The maintainer should swap these back to a real v0.6.0 tag before merging. --- Cargo.lock | 111 +++++++++++++++++++++++-------- Cargo.toml | 8 +-- examples/list_incoming_offers.rs | 2 +- examples/list_outgoing_offers.rs | 2 +- src/accept.rs | 2 +- src/cancel_offers.rs | 2 +- src/consolidate.rs | 12 ++-- src/credentials.rs | 21 +++--- src/dar_check.rs | 9 +-- src/mint_redeem/mint.rs | 11 ++- src/mint_redeem/models.rs | 11 ++- src/mint_redeem/redeem.rs | 20 +++--- src/split.rs | 12 ++-- src/transfer.rs | 12 ++-- src/utils.rs | 16 +++-- 15 files changed, 142 insertions(+), 109 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a94097a..729682a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -243,15 +243,18 @@ dependencies = [ [[package]] name = "canton-api-client" -version = "3.3.0-0.1.1" +version = "3.6.0-0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3e641f161ca1dddca37296cfa43c14d28772864ca66631a38e3db79d3f3ddd" +checksum = "1702122d98d165e2c77a1c6532e8be8a80d5bf7f25eac71beed5587a61fcaf01" dependencies = [ - "reqwest", + "chrono", + "reqwest 0.13.3", "serde", "serde_json", "serde_repr", "serde_with", + "tokio", + "tokio-util", "url", ] @@ -271,7 +274,7 @@ dependencies = [ "ledger", "log", "registry", - "reqwest", + "reqwest 0.12.26", "semver", "serde", "serde_json", @@ -337,7 +340,7 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "common" version = "0.5.0" -source = "git+ssh://git@github.com/DLC-link/canton-lib?tag=v0.5.0#e5b459f62715ee903d9cd8fdd0c4411252356e4e" +source = "git+ssh://git@github.com/DLC-link/canton-lib?branch=chore%2Fbump-canton-api-client-3.6.0#e3260f744c5b780fb0d405adbeee85119bdf8520" dependencies = [ "canton-api-client", "rust_decimal", @@ -1165,10 +1168,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -1176,10 +1181,10 @@ dependencies = [ [[package]] name = "keycloak" version = "0.5.0" -source = "git+ssh://git@github.com/DLC-link/canton-lib?tag=v0.5.0#e5b459f62715ee903d9cd8fdd0c4411252356e4e" +source = "git+ssh://git@github.com/DLC-link/canton-lib?branch=chore%2Fbump-canton-api-client-3.6.0#e3260f744c5b780fb0d405adbeee85119bdf8520" dependencies = [ "base64", - "reqwest", + "reqwest 0.12.26", "serde", "serde_json", ] @@ -1187,7 +1192,7 @@ dependencies = [ [[package]] name = "ledger" version = "0.5.0" -source = "git+ssh://git@github.com/DLC-link/canton-lib?tag=v0.5.0#e5b459f62715ee903d9cd8fdd0c4411252356e4e" +source = "git+ssh://git@github.com/DLC-link/canton-lib?branch=chore%2Fbump-canton-api-client-3.6.0#e3260f744c5b780fb0d405adbeee85119bdf8520" dependencies = [ "base64", "canton-api-client", @@ -1195,7 +1200,7 @@ dependencies = [ "futures-util", "log", "rand", - "reqwest", + "reqwest 0.12.26", "serde", "serde_json", "tokio-tungstenite", @@ -1592,10 +1597,10 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "registry" version = "0.5.0" -source = "git+ssh://git@github.com/DLC-link/canton-lib?tag=v0.5.0#e5b459f62715ee903d9cd8fdd0c4411252356e4e" +source = "git+ssh://git@github.com/DLC-link/canton-lib?branch=chore%2Fbump-canton-api-client-3.6.0#e3260f744c5b780fb0d405adbeee85119bdf8520" dependencies = [ "common", - "reqwest", + "reqwest 0.12.26", "serde", "serde_json", ] @@ -1619,7 +1624,6 @@ dependencies = [ "bytes", "encoding_rs", "futures-core", - "futures-util", "h2", "http", "http-body", @@ -1631,6 +1635,43 @@ dependencies = [ "js-sys", "log", "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "reqwest" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-tls", + "hyper-util", + "js-sys", + "log", "mime_guess", "native-tls", "percent-encoding", @@ -1642,12 +1683,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", ] @@ -2477,9 +2520,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" dependencies = [ "cfg-if", "once_cell", @@ -2491,22 +2534,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2514,9 +2554,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ "bumpalo", "proc-macro2", @@ -2527,18 +2567,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index a5adead..9c176af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,10 +8,10 @@ autoexamples = false uninlined_format_args = "allow" [dependencies] -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" } +ledger = { git = "ssh://git@github.com/DLC-link/canton-lib", branch = "chore/bump-canton-api-client-3.6.0" } +keycloak = { git = "ssh://git@github.com/DLC-link/canton-lib", branch = "chore/bump-canton-api-client-3.6.0" } +registry = { git = "ssh://git@github.com/DLC-link/canton-lib", branch = "chore/bump-canton-api-client-3.6.0" } +common = { git = "ssh://git@github.com/DLC-link/canton-lib", branch = "chore/bump-canton-api-client-3.6.0" } tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread"] } serde_json = "1" chrono = "0.4.42" diff --git a/examples/list_incoming_offers.rs b/examples/list_incoming_offers.rs index 0a94884..be925c5 100644 --- a/examples/list_incoming_offers.rs +++ b/examples/list_incoming_offers.rs @@ -72,7 +72,7 @@ async fn main() -> Result<(), String> { println!(" Full ID: {}", contract_id); // Extract transfer details - if let Some(Some(create_arg)) = &transfer.created_event.create_argument { + if let Some(create_arg) = &transfer.created_event.create_argument { if let Some(transfer_data) = create_arg.get("transfer") { if let Some(sender) = transfer_data.get("sender") { println!(" From: {}", sender.as_str().unwrap_or("unknown")); diff --git a/examples/list_outgoing_offers.rs b/examples/list_outgoing_offers.rs index fee2f95..510ba4c 100644 --- a/examples/list_outgoing_offers.rs +++ b/examples/list_outgoing_offers.rs @@ -72,7 +72,7 @@ async fn main() -> Result<(), String> { println!(" Full ID: {}", contract_id); // Extract transfer details - if let Some(Some(create_arg)) = &transfer.created_event.create_argument { + if let Some(create_arg) = &transfer.created_event.create_argument { if let Some(transfer_data) = create_arg.get("transfer") { if let Some(receiver) = transfer_data.get("receiver") { println!(" To: {}", receiver.as_str().unwrap_or("unknown")); diff --git a/src/accept.rs b/src/accept.rs index 1050731..acfbbc2 100644 --- a/src/accept.rs +++ b/src/accept.rs @@ -241,7 +241,7 @@ pub async fn accept_all(params: AcceptAllParams) -> Result Result Result, fn parse_consolidate_response( response: &JsSubmitAndWaitForTransactionResponse, ) -> Result, String> { - let events = response - .transaction - .events - .as_ref() - .ok_or("Failed to find events")?; + let events = &response.transaction.events; let mut result_cids = Vec::new(); for event in events { if let Some(exercised) = crate::event_helpers::as_exercised_event(event) { - if let Some(result) = exercised.exercise_result.as_ref() { + if let Some(Some(result)) = exercised.exercise_result.as_ref() { // Extract receiverHoldingCids from the Daml-encoded payload if let Some(receiver_cids) = result["output"]["value"]["receiverHoldingCids"].as_array() @@ -499,10 +495,12 @@ mod parser_tests { #[test] fn missing_events_returns_err() { + // `events` is required on the wire now, so an empty list stands in for + // "missing events"; the parser falls through to its post-loop check. let response = transaction_response("tx-x", json!(null)); let err = parse_consolidate_response(&response).unwrap_err(); assert!( - err.contains("Failed to find events"), + err.contains("Failed to extract result holding CIDs"), "unexpected error: {err}" ); } diff --git a/src/credentials.rs b/src/credentials.rs index 24879dd..34600da 100644 --- a/src/credentials.rs +++ b/src/credentials.rs @@ -41,13 +41,16 @@ impl CredentialOffer { pub fn from_active_contract(contract: &JsActiveContract) -> Result { let contract_id = contract.created_event.contract_id.clone(); let template_id = contract.created_event.template_id.clone(); - let created_event_blob = contract.created_event.created_event_blob.clone(); + let created_event_blob = contract + .created_event + .created_event_blob + .clone() + .unwrap_or_default(); let args = contract .created_event .create_argument .as_ref() - .and_then(|opt| opt.as_ref()) .and_then(|v| v.as_object()) .ok_or("createArgument is not an object")?; @@ -116,7 +119,6 @@ impl UserCredential { .created_event .create_argument .as_ref() - .and_then(|opt| opt.as_ref()) .and_then(|v| v.as_object()) .ok_or("createArgument is not an object")?; @@ -240,7 +242,6 @@ pub async fn list_credential_offers( .created_event .create_argument .as_ref() - .and_then(|opt| opt.as_ref()) .and_then(|v| v.as_object()) .and_then(|args| args.get("holder")) .and_then(|v| v.as_str()) @@ -290,7 +291,6 @@ pub async fn list_credentials( .created_event .create_argument .as_ref() - .and_then(|opt| opt.as_ref()) .and_then(|v| v.as_object()) .and_then(|args| args.get("holder")) .and_then(|v| v.as_str()) @@ -339,7 +339,6 @@ pub async fn find_user_service(params: FindUserServiceParams) -> Result a, @@ -431,11 +430,7 @@ pub async fn accept_credential_offer( fn parse_accept_credential_offer_response( response: &JsSubmitAndWaitForTransactionResponse, ) -> Result { - let events = response - .transaction - .events - .as_ref() - .ok_or("Failed to find events in transaction")?; + let events = &response.transaction.events; for event in events { if let Some(created) = crate::event_helpers::as_created_event(event) { @@ -635,10 +630,12 @@ mod parser_tests { #[test] fn missing_events_returns_err() { + // `events` is required on the wire now, so an empty list stands in for + // "missing events"; the parser falls through to its post-loop check. let response = transaction_response("tx-x", json!(null)); let err = parse_accept_credential_offer_response(&response).unwrap_err(); assert!( - err.contains("Failed to find events"), + err.contains("No Credential contract was created"), "unexpected error: {err}" ); } diff --git a/src/dar_check.rs b/src/dar_check.rs index c49fefd..658d42f 100644 --- a/src/dar_check.rs +++ b/src/dar_check.rs @@ -53,14 +53,7 @@ pub async fn check(params: Params) -> Result { .await .map_err(|e| format!("Failed to fetch packages from participant: {}", e))?; - let participant_packages: HashSet = response - .package_ids - .ok_or_else(|| { - "Failed to fetch packages from participant: response missing 'package_ids' field" - .to_string() - })? - .into_iter() - .collect(); + let participant_packages: HashSet = response.package_ids.into_iter().collect(); info!( "Fetched {} packages from participant", diff --git a/src/mint_redeem/mint.rs b/src/mint_redeem/mint.rs index af1440c..78df241 100644 --- a/src/mint_redeem/mint.rs +++ b/src/mint_redeem/mint.rs @@ -106,11 +106,7 @@ pub async fn list_deposit_accounts( fn parse_created_deposit_account_cid( response: &JsSubmitAndWaitForTransactionResponse, ) -> Result { - let events = response - .transaction - .events - .as_ref() - .ok_or("Failed to find events in transaction")?; + let events = &response.transaction.events; for event in events { if let Some(created) = crate::event_helpers::as_created_event(event) { @@ -455,12 +451,13 @@ mod parser_tests { #[test] fn returns_err_when_events_missing() { - // Envelope is missing `transaction.events` entirely. + // `events` is required on the wire now; pass an empty list and verify + // the parser falls through to its post-loop check. let response = transaction_response("tx-3", json!(null)); let err = parse_created_deposit_account_cid(&response).unwrap_err(); assert!( - err.contains("Failed to find events"), + err.contains("No DepositAccount was created"), "unexpected error message: {err}" ); } diff --git a/src/mint_redeem/models.rs b/src/mint_redeem/models.rs index 089287f..d197b21 100644 --- a/src/mint_redeem/models.rs +++ b/src/mint_redeem/models.rs @@ -96,7 +96,6 @@ impl DepositAccount { .created_event .create_argument .as_ref() - .and_then(|opt| opt.as_ref()) .and_then(|v| v.as_object()) .ok_or("createArgument is not an object")?; @@ -189,13 +188,16 @@ impl WithdrawAccount { pub fn from_active_contract(contract: &JsActiveContract) -> Result { let contract_id = contract.created_event.contract_id.clone(); let template_id = contract.created_event.template_id.clone(); - let created_event_blob = contract.created_event.created_event_blob.clone(); + let created_event_blob = contract + .created_event + .created_event_blob + .clone() + .unwrap_or_default(); let args = contract .created_event .create_argument .as_ref() - .and_then(|opt| opt.as_ref()) .and_then(|v| v.as_object()) .ok_or("createArgument is not an object")?; @@ -274,7 +276,6 @@ impl WithdrawRequest { .created_event .create_argument .as_ref() - .and_then(|opt| opt.as_ref()) .and_then(|v| v.as_object()) .ok_or("createArgument is not an object")?; @@ -342,7 +343,6 @@ impl Holding { .created_event .create_argument .as_ref() - .and_then(|opt| opt.as_ref()) .and_then(|v| v.as_object()) .ok_or("createArgument is not an object")?; @@ -382,7 +382,6 @@ impl Holding { .created_event .create_argument .as_ref() - .and_then(|opt| opt.as_ref()) .and_then(|v| v.as_object()) .and_then(|args| args.get("lock")) .is_some_and(|lock| !lock.is_null()) diff --git a/src/mint_redeem/redeem.rs b/src/mint_redeem/redeem.rs index b6878f1..e0ec17b 100644 --- a/src/mint_redeem/redeem.rs +++ b/src/mint_redeem/redeem.rs @@ -129,11 +129,7 @@ pub async fn list_withdraw_accounts( fn parse_created_withdraw_account_cid( response: &JsSubmitAndWaitForTransactionResponse, ) -> Result { - let events = response - .transaction - .events - .as_ref() - .ok_or("Failed to find events in transaction")?; + let events = &response.transaction.events; for event in events { if let Some(created) = crate::event_helpers::as_created_event(event) { @@ -155,11 +151,7 @@ fn parse_created_withdraw_account_cid( fn parse_submit_withdraw_response( response: &JsSubmitAndWaitForTransactionResponse, ) -> Result { - let events = response - .transaction - .events - .as_ref() - .ok_or("Failed to find events in transaction")?; + let events = &response.transaction.events; for event in events { if let Some(created) = crate::event_helpers::as_created_event(event) { @@ -757,10 +749,12 @@ mod parser_tests { #[test] fn parse_created_withdraw_account_cid_missing_events() { + // `events` is required on the wire now; pass an empty list and verify + // the parser falls through to its post-loop check. let response = transaction_response("tx-x", json!(null)); let err = parse_created_withdraw_account_cid(&response).unwrap_err(); assert!( - err.contains("Failed to find events"), + err.contains("No WithdrawAccount was created"), "unexpected error: {err}" ); } @@ -816,10 +810,12 @@ mod parser_tests { #[test] fn parse_submit_withdraw_response_missing_events() { + // `events` is required on the wire now; pass an empty list and verify + // the parser falls through to its post-loop check. let response = transaction_response("tx-x", json!(null)); let err = parse_submit_withdraw_response(&response).unwrap_err(); assert!( - err.contains("Failed to find events"), + err.contains("No updated WithdrawAccount was found"), "unexpected error: {err}" ); } diff --git a/src/split.rs b/src/split.rs index 1fb8769..adc8bb5 100644 --- a/src/split.rs +++ b/src/split.rs @@ -136,16 +136,12 @@ async fn split_once( fn parse_split_response( response: &JsSubmitAndWaitForTransactionResponse, ) -> Result<(String, Vec), String> { - let events = response - .transaction - .events - .as_ref() - .ok_or("Failed to find events")?; + let events = &response.transaction.events; let mut exercise_result = None; for event in events { if let Some(exercised) = crate::event_helpers::as_exercised_event(event) { - if let Some(result) = exercised.exercise_result.as_ref() { + if let Some(Some(result)) = exercised.exercise_result.as_ref() { if result.is_object() { exercise_result = Some(result); break; @@ -339,10 +335,12 @@ mod parser_tests { #[test] fn missing_events_returns_err() { + // `events` is required on the wire now; pass an empty list and verify + // the parser falls through to its post-loop check. let response = transaction_response("tx-x", json!(null)); let err = parse_split_response(&response).unwrap_err(); assert!( - err.contains("Failed to find events"), + err.contains("Failed to find ExercisedEvent"), "unexpected error: {err}" ); } diff --git a/src/transfer.rs b/src/transfer.rs index e89022a..95f5b6b 100644 --- a/src/transfer.rs +++ b/src/transfer.rs @@ -615,11 +615,7 @@ fn parse_transfer_response_value( return Err("Failed to find updateId in response".to_string()); } - let events = response - .transaction - .events - .as_ref() - .ok_or("Failed to find events in response")?; + let events = &response.transaction.events; // Find the ExercisedEvent with TransferFactory_Transfer choice let mut sender_change_cids = None; @@ -628,7 +624,7 @@ fn parse_transfer_response_value( for event in events { if let Some(exercised) = crate::event_helpers::as_exercised_event(event) { if exercised.choice == "TransferFactory_Transfer" { - if let Some(result) = exercised.exercise_result.as_ref() { + if let Some(Some(result)) = exercised.exercise_result.as_ref() { // Extract senderChangeCids if let Some(change_array) = result["senderChangeCids"].as_array() { sender_change_cids = Some( @@ -810,11 +806,13 @@ mod parser_tests { #[test] fn malformed_envelope_missing_events() { + // `events` is required on the wire now; pass an empty list and verify + // the parser falls through to its post-loop check. let response = transaction_response("tx-1", json!(null)); let err = parse_transfer_response_value(&response).unwrap_err(); assert!( - err.contains("Failed to find events"), + err.contains("Failed to find senderChangeCids"), "unexpected error: {err}" ); } diff --git a/src/utils.rs b/src/utils.rs index 8583868..6384ad1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -96,7 +96,7 @@ async fn fetch_transfers( let filtered: Vec = result .into_iter() .filter(|ac| { - if let Some(Some(create_arg)) = &ac.created_event.create_argument { + if let Some(create_arg) = &ac.created_event.create_argument { if let Some(transfer) = create_arg.get("transfer") { // Check if instrumentId is CBTC let is_cbtc = if let Some(instrument_id) = transfer.get("instrumentId") { @@ -183,6 +183,8 @@ pub(crate) mod test_fixtures { "observers": [], "createdAt": "1970-01-01T00:00:00Z", "packageName": "test-pkg", + "representativePackageId": "test-pkg", + "acsDelta": true, } }) } @@ -223,30 +225,32 @@ pub(crate) mod test_fixtures { "lastDescendantNodeId": 0_i32, "exerciseResult": exercise_result, "packageName": "test-pkg", + "acsDelta": true, } }) } /// Build a `JsSubmitAndWaitForTransactionResponse` from an updateId and - /// an `events` value (use `json!(null)` to omit events entirely). + /// an `events` value. Pass `json!(null)` to construct a response with an + /// empty events list (the typed model now treats `events` as required, so + /// "no events" is represented as `[]` rather than an absent field). /// Deserializes through the typed model so fixtures fail loudly when the /// shape diverges from canton-api-client's schema. pub fn transaction_response( update_id: &str, events: Value, ) -> JsSubmitAndWaitForTransactionResponse { - let mut transaction = json!({ + let events = if events.is_null() { json!([]) } else { events }; + let transaction = json!({ "updateId": update_id, "commandId": "", "workflowId": "", "effectiveAt": "1970-01-01T00:00:00Z", + "events": events, "offset": 1_i64, "synchronizerId": "test-synchronizer", "recordTime": "1970-01-01T00:00:00Z", }); - if !events.is_null() { - transaction["events"] = events; - } let envelope = json!({ "transaction": transaction }); serde_json::from_value(envelope).expect("test fixture is not a valid response") } From 01b8b9a52905d89c3c5d1cbd40888de9f636a982 Mon Sep 17 00:00:00 2001 From: gyorgybalazsi Date: Tue, 19 May 2026 11:20:56 +0200 Subject: [PATCH 3/5] fix(integration_test): filter holdings to CBTC before splitting The step 20 split called `holdings.first()` on the result of `list_holdings`, which returns every Holding-template contract owned by the party. On devnet the sender's account holds legacy `CBTCV0RC8` instrument holdings alongside `CBTC`, so `.first()` could pick a non-CBTC holding. The split request then asserted `instrument_id = "CBTC"`, and the registry correctly rejected the mismatch with 400 "Given holdings are invalid". Filter to `instrument_id == "CBTC"` before picking, matching the pattern already used by `submit_withdraw` in `mint_redeem::redeem`. Verified end-to-end on devnet: 20/20 integration_test steps pass. Co-Authored-By: Claude Opus 4.7 --- examples/integration_test.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/integration_test.rs b/examples/integration_test.rs index 01ca4ad..3ef8afc 100644 --- a/examples/integration_test.rs +++ b/examples/integration_test.rs @@ -823,9 +823,14 @@ async fn main() -> Result<(), String> { .await .map_err(|e| format!("Failed to list holdings for split: {}", e))?; - match holdings.first() { + // Pick a CBTC holding. list_holdings returns every Holding-template contract the + // sender owns, which on devnet includes legacy `CBTCV0RC8` instruments alongside + // `CBTC`. Splitting a non-CBTC holding while asserting `instrument_id = "CBTC"` + // below would make the registry reject the request with 400 "Given holdings are + // invalid". + match holdings.iter().find(|h| h.instrument_id == "CBTC") { None => { - print_skip("(no holdings available to split)"); + print_skip("(no CBTC holdings available to split)"); passed += 1; } Some(holding) => { From 055c91da5415277ba9adfc636a260649cf673798 Mon Sep 17 00:00:00 2001 From: gyorgybalazsi Date: Tue, 19 May 2026 15:27:58 +0200 Subject: [PATCH 4/5] chore: pin canton-lib deps to v0.6.0 tag Replaces the temporary branch refs (`branch = "chore/bump-canton-api-client-3.6.0"`) on `ledger`/`keycloak`/`registry`/ `common` with `tag = "v0.6.0"`. canton-lib v0.6.0 (DLC-link/canton-lib#20) contains the 3.6.0 bump (#18) plus the keycloak Quarkus token-URL helpers (#19); the upstream PR branch was deleted on merge, so the previous refs no longer resolve. Cargo.lock updated by `cargo update -p ledger -p keycloak -p registry -p common`. `cargo check` clean. Co-Authored-By: Claude Opus 4.7 --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 729682a..b755ba0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -339,8 +339,8 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "common" -version = "0.5.0" -source = "git+ssh://git@github.com/DLC-link/canton-lib?branch=chore%2Fbump-canton-api-client-3.6.0#e3260f744c5b780fb0d405adbeee85119bdf8520" +version = "0.6.0" +source = "git+ssh://git@github.com/DLC-link/canton-lib?tag=v0.6.0#83446199920966c322c525d6a3739d2cfd2863c7" dependencies = [ "canton-api-client", "rust_decimal", @@ -1180,8 +1180,8 @@ dependencies = [ [[package]] name = "keycloak" -version = "0.5.0" -source = "git+ssh://git@github.com/DLC-link/canton-lib?branch=chore%2Fbump-canton-api-client-3.6.0#e3260f744c5b780fb0d405adbeee85119bdf8520" +version = "0.6.0" +source = "git+ssh://git@github.com/DLC-link/canton-lib?tag=v0.6.0#83446199920966c322c525d6a3739d2cfd2863c7" dependencies = [ "base64", "reqwest 0.12.26", @@ -1191,8 +1191,8 @@ dependencies = [ [[package]] name = "ledger" -version = "0.5.0" -source = "git+ssh://git@github.com/DLC-link/canton-lib?branch=chore%2Fbump-canton-api-client-3.6.0#e3260f744c5b780fb0d405adbeee85119bdf8520" +version = "0.6.0" +source = "git+ssh://git@github.com/DLC-link/canton-lib?tag=v0.6.0#83446199920966c322c525d6a3739d2cfd2863c7" dependencies = [ "base64", "canton-api-client", @@ -1596,8 +1596,8 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "registry" -version = "0.5.0" -source = "git+ssh://git@github.com/DLC-link/canton-lib?branch=chore%2Fbump-canton-api-client-3.6.0#e3260f744c5b780fb0d405adbeee85119bdf8520" +version = "0.6.0" +source = "git+ssh://git@github.com/DLC-link/canton-lib?tag=v0.6.0#83446199920966c322c525d6a3739d2cfd2863c7" dependencies = [ "common", "reqwest 0.12.26", diff --git a/Cargo.toml b/Cargo.toml index 9c176af..8e0ca17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,10 +8,10 @@ autoexamples = false uninlined_format_args = "allow" [dependencies] -ledger = { git = "ssh://git@github.com/DLC-link/canton-lib", branch = "chore/bump-canton-api-client-3.6.0" } -keycloak = { git = "ssh://git@github.com/DLC-link/canton-lib", branch = "chore/bump-canton-api-client-3.6.0" } -registry = { git = "ssh://git@github.com/DLC-link/canton-lib", branch = "chore/bump-canton-api-client-3.6.0" } -common = { git = "ssh://git@github.com/DLC-link/canton-lib", branch = "chore/bump-canton-api-client-3.6.0" } +ledger = { git = "ssh://git@github.com/DLC-link/canton-lib", tag = "v0.6.0" } +keycloak = { git = "ssh://git@github.com/DLC-link/canton-lib", tag = "v0.6.0" } +registry = { git = "ssh://git@github.com/DLC-link/canton-lib", tag = "v0.6.0" } +common = { git = "ssh://git@github.com/DLC-link/canton-lib", tag = "v0.6.0" } tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread"] } serde_json = "1" chrono = "0.4.42" From a6b3d0200aac12bc5bfb618bbd245cc2eab28dd0 Mon Sep 17 00:00:00 2001 From: gyorgybalazsi Date: Tue, 19 May 2026 15:44:28 +0200 Subject: [PATCH 5/5] chore: bump cbtc version to 0.6.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns cbtc-lib's crate version with the canton-lib v0.6.0 release this PR depends on. Skips 0.5.x — the version jump reflects the canton-api-client 3.6.0 wire bump pulled in transitively (breaking change in dependency behavior, see CHANGELOG of canton-lib v0.6.0 for the `ledger_end::get` error-path change). cbtc-lib's public API is unchanged. Co-Authored-By: Claude Opus 4.7 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b755ba0..7adc042 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -260,7 +260,7 @@ dependencies = [ [[package]] name = "cbtc" -version = "0.4.2" +version = "0.6.0" dependencies = [ "base64", "canton-api-client", diff --git a/Cargo.toml b/Cargo.toml index 8e0ca17..30ab838 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cbtc" -version = "0.4.2" +version = "0.6.0" edition = "2024" autoexamples = false