Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 0 additions & 61 deletions crates/dsm-android-anchor/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions crates/dsm-android-anchor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ edition = "2021"
license = "MIT OR Apache-2.0"
description = "On-device glue for the DSM Software-Authority / Hardware-Identity anchor: installs the sender USB anchor appliance (UsbAnchorAppliance over the opaque Kotlin picoUsbTransceive) into the dsm_sdk bridge seam, plus gated device-setup ops (counter birth, slot-0 birth cage) and read-only chip diagnostics. Depends on dsm_sdk + dsm-anchor-hw-verifier in ONE direction (no dependency cycle); transitively pulls tropic01, so it is EXCLUDED from the workspace and built only in the Android cargo-ndk pipeline — the default CI workspace stays tropic01-free."

# Its own workspace root. The repository workspace excludes this crate, and cargo keeps
# climbing past an excluding root: from a checkout nested under another checkout (a git
# worktree under `.claude/worktrees/`) it reaches the outer repository's manifest, which
# neither lists nor excludes this path, and refuses to build. Declaring the root here stops
# the climb wherever the checkout sits.
[workspace]

[lib]
# rlib for host tests; staticlib/cdylib so the Android build can link the on-device JNI shell (H3).
crate-type = ["rlib", "staticlib", "cdylib"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -748,7 +748,7 @@ async fn bearer_pair() -> (
"wallet.loadOffline",
&crate::generated::OfflineCashRequest {
token_id: "ERA".to_string(),
amount: 20,
amount: "20".to_string(),
},
)
.await;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,10 @@ impl AppRouterImpl {
locked,
..Default::default()
};
if let Err(e) = crate::handlers::wallet_routes::enrich_balance_metadata(&mut reply)
if let Err(e) =
crate::handlers::wallet_routes::enrich_balance_metadata(&mut reply, &|asset| {
self.core_sdk.offline_allocation_of(asset)
})
{
return err(format!("bitcoin balance: {e}"));
}
Expand Down Expand Up @@ -636,6 +639,10 @@ impl AppRouterImpl {
protocol_defined: false,
genesis_supply_display: String::new(),
permissions: None,
// An offline allocation leaf is keyed by the asset's
// policy commit, which native BTC does not have: no
// such leaf can exist, so there is nothing to state.
offline_allocation: None,
},
))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,18 @@ impl AppRouterImpl {
))
}
};
if req.amount == 0 {
// The amount as the user typed it, scaled by the token's decimals here:
// the parser the sends use, so one rule owns the unit in both directions.
let decimals = match super::wallet_routes::token_decimals(&req.token_id) {
Ok(d) => d,
Err(e) => return err(format!("wallet.{verb}: {e}")),
};
let amount =
match super::wallet_routes::parse_display_amount_to_base_units(&req.amount, decimals) {
Ok(a) => a,
Err(e) => return err(format!("wallet.{verb}: invalid amount: {e}")),
};
if amount == 0 {
return err(format!("wallet.{verb}: amount must be > 0"));
}

Expand All @@ -68,29 +79,38 @@ impl AppRouterImpl {

// Apply the conserved regime shift (fail-closed persist-before-install in CoreSDK).
let outcome = if is_load {
self.core_sdk.load_offline_cash(bundle, asset, req.amount)
self.core_sdk.load_offline_cash(bundle, asset, amount)
} else {
self.core_sdk.unload_offline_cash(bundle, asset, req.amount)
self.core_sdk.unload_offline_cash(bundle, asset, amount)
};
let outcome = match outcome {
Ok(o) => o,
Err(e) => return err(format!("wallet.{verb}: {e}")),
};

let online_balance = self.core_sdk.get_device_balance(&asset);
// Rendered here, in the token's units: the wallet prints these and
// computes nothing.
let render = |base_units: u64| {
super::wallet_routes::format_base_units_for_display(base_units, decimals)
};
let online_display = render(online_balance);
let allocation_display = render(outcome.amount);
let resp = generated::OfflineCashResponse {
success: true,
online_balance,
allocation_balance: outcome.amount,
device_root: outcome.new_root.to_vec(),
message: format!(
"{} {} of {} — offline allocation now {}, online {}",
"{} {} {} — offline allocation now {}, online {}",
if is_load { "loaded" } else { "unloaded" },
req.amount,
render(amount),
req.token_id,
outcome.amount,
online_balance,
allocation_display,
online_display,
),
online_display,
allocation_display,
};
pack_envelope_ok(generated::envelope::Payload::OfflineCashResponse(resp))
}
Expand All @@ -115,15 +135,15 @@ mod tests {
.expect("router init")
}

async fn invoke(router: &AppRouterImpl, method: &str) -> AppResult {
async fn invoke(router: &AppRouterImpl, method: &str, amount: &str) -> AppResult {
router
.handle_offline_cash_invoke(AppInvoke {
method: method.to_string(),
args: generated::ArgPack {
codec: generated::Codec::Proto as i32,
body: generated::OfflineCashRequest {
token_id: "ERA".to_string(),
amount: 10,
amount: amount.to_string(),
}
.encode_to_vec(),
..Default::default()
Expand All @@ -141,7 +161,7 @@ mod tests {
#[serial_test::serial]
async fn load_offline_refuses_when_the_anchor_appliance_cannot_be_reached() {
let r = router_on_a_device();
let res = invoke(&r, "wallet.loadOffline").await;
let res = invoke(&r, "wallet.loadOffline", "10").await;
assert!(!res.success, "load must refuse when no appliance answers");
let msg = res.error_message.unwrap_or_default();
assert!(
Expand All @@ -156,11 +176,23 @@ mod tests {
#[serial_test::serial]
async fn unload_offline_refuses_when_the_anchor_appliance_cannot_be_reached() {
let r = router_on_a_device();
let res = invoke(&r, "wallet.unloadOffline").await;
let res = invoke(&r, "wallet.unloadOffline", "10").await;
assert!(!res.success);
assert!(res
.error_message
.unwrap_or_default()
.contains("connect your anchor device"));
}

/// The amount is the user's decimal text, scaled by Rust: text that is not
/// an amount is refused as one, before any gate that needs the appliance.
#[tokio::test(flavor = "multi_thread")]
#[serial_test::serial]
async fn an_amount_that_is_not_a_number_is_refused_as_an_amount() {
let r = router_on_a_device();
let res = invoke(&r, "wallet.loadOffline", "ten").await;
assert!(!res.success);
let msg = res.error_message.unwrap_or_default();
assert!(msg.contains("invalid amount"), "{msg}");
}
}
Loading
Loading