Using call_with_payment
in Pocket IC Integration Tests
I have a canister with a subscribe
method (Candid update call) that requires payment from subscribers to top up their balances and access the canister’s functionality.
In a real-world scenario, subscribers call subscribe
with payment, updating the main canister state correctly and granting them access.
Currently, in Pocket IC, I am setting up the test like this:
let pic = PocketIc::new().await;
// Install the subscriber canister
pic.install_canister(
subscriber_canister_id,
subscriber_wasm_bytes.to_vec(),
vec![],
None,
)
.await;
// Call the `subscribe` method
pic.update_call(
main_canister_id,
subscriber_canister_id,
"subscribe",
candid::encode_one(subscription_registration.clone()).unwrap(),
)
.await;
However, I need to attach cycles to this call (similar to call_with_payment()
) to properly simulate real behavior.
How can I achieve this in Pocket IC integration tests?
1 Like
Cycles cannot be attached to ingress messages (both on the ICP mainnet and in PocketIC) and using pic.update_call
to impersonate a canister as the sender of the message doesn’t lift this requirement. Hence, you can only attach cycles to such an (inter-canister) call if you make the call from within a sender canister that you invoke using pic.update_call
. Does that make sense?
3 Likes
Using pic.js I always have to make a mock canister to do this kind of stuff.
Of we can make canisters with arbitrary cycles, why can’t we make calls with arbitrary cycles and a fakeish identity. It would really clean up my testing code when I’m doing a “unit” test on my canister.
1 Like
This is because creating canisters with arbitrary cycles is supported by a dedicated mgmt canister API only available in test environments: such an API is necessary to bootstrap some canisters on a clean state in a newly started test environment. If we wanted to enable ingress calls with arbitrary cycles, then a dedicated API would need to be provided by the replica. Alternatively, you can use what you call a “mock” canister to “proxy” your cycles while attaching cycles to them.
1 Like
A built-in canister with call_raw_with_cycles as a proxy might be a nice addition to the SDK…Perhaps the cycle wallet is already this? Wrapping it in a helper function of some kind so that is deployed if not present and used if it is might be nice. This may make more sense for pic.js.
1 Like
Yes, that makes sense—thank you for your response!
Though, a proxy canister implemented in ICP CDK could be a viable solution for other developers, as it eliminates the need to create additional methods or canisters manually.
I agree with @skilesare
1 Like
Indeed, the cycles wallet already offers this functionality.
Note that caller cannot be impersonated though (the caller of an inter-canister call proxied via a wallet is that wallet) and certain aspects (e.g., message size limits) depend on whether the wallet is installed on the same subnet as the callee or not. So there are quite some choices to be made here and thus the PocketIC library API would have to be generic over those choices (and thus maybe not as convenient as if test authors write their own helpers). Hence, we haven’t provided such API, but we can reconsider that, of course, if there’s demand.
1 Like
Well, if the function took a preferred canister ID, I’m sure it would fail as gracefully as it does when I try to stick something on the wrong subnet currently. 


It would just be some sugar and it would be nice to have, but I imagine there’s also a pretty easy ride up on how to do this across the different libraries that would be useful in a guide until we can get that put in.
Currently we have
pub fn update_call(
&self,
canister_id: CanisterId,
sender: Principal,
method: &str,
payload: Vec<u8>,
) -> Result<Vec<u8>, RejectResponse>
and the PocketIC library API could indeed also offer
pub fn update_call_with_cycles(
&self,
canister_id: CanisterId,
sender: Principal,
method: &str,
payload: Vec<u8>,
cycles: u128,
) -> Result<Vec<u8>, RejectResponse>
There are some open questions though:
- Do we require the sender canister to exist? If not, we would need to restrict the sender principal to be a mainnet canister ID since canister creation with specified canister IDs is only supported for mainnet canister IDs.
- Do we require the sender canister to be empty?
- If so, then we can temporarily install a wallet canister to proxy the call, but this could have unexpected side-effects (e.g., calls to the “empty” canister might unexpectedly succeed while the wallet canister is temporarily installed or fail for an unexpected reason).
- If not, then we would need to temporarily replace the existing canister by the wallet canister which could also lead to unexpected side-effects (e.g., calls to the existing canister might be rejected by the wallet which has a different API) and restoring the existing canister afterwards is only possible if a snapshot could be taken (i.e., if no snapshot already exists for that canister).
So it seems infeasible to impersonate a sender canister transparently and it is not clear in general what trade-off to take while avoiding surprises.
Personally, I feel like proxying calls through an existing empty canister is the least surprising option (only calls to that “empty” canister could lead to surprises, but presumably calls to empty canisters are rare). In the above example, this option would not apply though because the sender canister (the subscriber canister) is not empty (it has been installed just before the update call) unless there’s a typo and the sender is supposed to be the “main” canister. Hence, I would like to hear more opinions or alternative suggestions before committing to a particular trade-off in the PocketIC library.