Follow-up from Copilot review of #46.
Context
Every flat-event parser extracts required fields with .as_str().unwrap_or(""). If the server response contains a matching event envelope but is missing or has non-string values for contractId, templateId, createdEventBlob, or fields inside createArgument, the parsers silently produce empty strings and continue. Downstream callers then fail with misleading errors — e.g., a missing contractId from create_deposit_account produces:
Created DepositAccount not found in active contracts
…instead of a clear parse-time error like \contractId` missing or not a string`.
This pattern is pre-existing — the tree-shape parsers used the same unwrap_or("") shape, and #41 / #46 carried it forward unchanged. It is not a regression from either of those PRs, but it is a real shipping-confidence issue.
Copilot flagged it on PR #46:
Affected sites (all 7 parsers)
src/mint_redeem/mint.rs:114 parse_created_deposit_account_cid — contractId
src/mint_redeem/redeem.rs:138 parse_created_withdraw_account_cid — contractId
src/mint_redeem/redeem.rs:165 parse_submit_withdraw_response — contractId, plus createdEventBlob and createArgument go into JsActiveContract without validation
src/transfer.rs:607 parse_transfer_response_value — senderChangeCids filter-maps via as_str() (drops non-strings silently); output.value.transferInstructionCid uses as_str() early-return is OK but doesn't distinguish missing-vs-not-yet
src/consolidate.rs parse_consolidate_response — extracts receiverHoldingCids with similar lenient pattern
src/split.rs parse_split_response — output_cid returns from as_str().ok_or(...) (already strict); senderChangeCids lenient
src/credentials.rs:448 parse_accept_credential_offer_response — contractId, createdEventBlob, createArgument
The strictness varies site-by-site today. A consistent treatment would be helpful.
Proposal
Replace as_str().unwrap_or("") with as_str().ok_or_else(|| format!(\"{} missing or not a string in <Event>\", field)) for every required field across all 7 parsers. Specifically:
contractId is always required → strict.
templateId is used in suffix-match — empty matches nothing, so the existing pattern accidentally produces correct behavior. Still strict is clearer.
createdEventBlob — sometimes empty in practice (Canton omits it when irrelevant), so it should remain unwrap_or(\"\"). Document inline.
createArgument is required for the from_active_contract calls in submit_withdraw and accept_credential_offer.
Plus: extend the fixture tests in each mod parser_tests block with one more case: malformed_event_missing_required_field_returns_err — fixture has the matching template but a missing contractId, parser should Err, not Ok(\"\").
Risk
Small behavioral change: callers that previously received a misleading downstream error now receive a clearer parse-time error. No code path that currently returns Ok would start returning Err for a previously-working response — only malformed responses change behavior.
Out of scope
Follow-up from Copilot review of #46.
Context
Every flat-event parser extracts required fields with
.as_str().unwrap_or(""). If the server response contains a matching event envelope but is missing or has non-string values forcontractId,templateId,createdEventBlob, or fields insidecreateArgument, the parsers silently produce empty strings and continue. Downstream callers then fail with misleading errors — e.g., a missingcontractIdfromcreate_deposit_accountproduces:…instead of a clear parse-time error like
\contractId` missing or not a string`.This pattern is pre-existing — the tree-shape parsers used the same
unwrap_or("")shape, and #41 / #46 carried it forward unchanged. It is not a regression from either of those PRs, but it is a real shipping-confidence issue.Copilot flagged it on PR #46:
Affected sites (all 7 parsers)
src/mint_redeem/mint.rs:114parse_created_deposit_account_cid—contractIdsrc/mint_redeem/redeem.rs:138parse_created_withdraw_account_cid—contractIdsrc/mint_redeem/redeem.rs:165parse_submit_withdraw_response—contractId, pluscreatedEventBlobandcreateArgumentgo intoJsActiveContractwithout validationsrc/transfer.rs:607parse_transfer_response_value—senderChangeCidsfilter-maps viaas_str()(drops non-strings silently);output.value.transferInstructionCidusesas_str()early-return is OK but doesn't distinguish missing-vs-not-yetsrc/consolidate.rsparse_consolidate_response— extractsreceiverHoldingCidswith similar lenient patternsrc/split.rsparse_split_response—output_cidreturns fromas_str().ok_or(...)(already strict);senderChangeCidslenientsrc/credentials.rs:448parse_accept_credential_offer_response—contractId,createdEventBlob,createArgumentThe strictness varies site-by-site today. A consistent treatment would be helpful.
Proposal
Replace
as_str().unwrap_or("")withas_str().ok_or_else(|| format!(\"{} missing or not a string in <Event>\", field))for every required field across all 7 parsers. Specifically:contractIdis always required → strict.templateIdis used in suffix-match — empty matches nothing, so the existing pattern accidentally produces correct behavior. Still strict is clearer.createdEventBlob— sometimes empty in practice (Canton omits it when irrelevant), so it should remainunwrap_or(\"\"). Document inline.createArgumentis required for thefrom_active_contractcalls in submit_withdraw and accept_credential_offer.Plus: extend the fixture tests in each
mod parser_testsblock with one more case:malformed_event_missing_required_field_returns_err— fixture has the matching template but a missingcontractId, parser shouldErr, notOk(\"\").Risk
Small behavioral change: callers that previously received a misleading downstream error now receive a clearer parse-time error. No code path that currently returns
Okwould start returningErrfor a previously-working response — only malformed responses change behavior.Out of scope