Need advice for intercanister redirection

I’m building a system where canisters can bounce an update call from a canister, to another canister repeatedly.

(1) at first, i was making the called canister to return a callback like so:

type Callback = {
  // not the destination, redirect to other canister
  #Ask: shared Nat -> async Redirect; 

  // is a destination, do work here
  #Execute: shared [(Nat, Blob)] -> Result<Ok, Err> 
}

(2) I realized we couldnt peek into the canister’s principal within the returned callback. I need to know the principal so i can prevent infinite/looping callgraph or deadlock. so i do it like this next:

type Redirect = { callback : Callback; canister_id : Principal }

but tbh i dont like it as i know the principal info is within that callback variant.

(3) so i do it like this now:

type Operation = { #Ask; #Execute };
type Redirect = { operation : Operation; canister_id };

and when i want to ask or execute, i will do the actor ("principal"): actor Interface and call the ask if operation is #Ask, and execute if operation is #Execute.

is there a better way/design than what i’m doing (3)? if no, is (2) better?

Isn’t 3) sill exposing the principal (canister_id) that you wanted to hide?

Assuming you are trying to prevent cycles in sequences of #Asks, one thing you could try is this.
Maintain a list of #Ask callbacks (shared Nat -> <whatever>?). Before extending the list, check the new #Ask callback doesn’t occur in the list, otherwise you’ve encountered a cycle.
You can do this because you can actually test equality on shared functions (unlike ordinary functions). i.e. s1 == s2 works for shared functions s1 and s2 of the same signature.

Similar to this, another solution might be to return the entire actor, not the function, in #Ask and #Execute:

type Callback = {
  // not the destination, redirect to other canister
  #Ask: actor { ask : shared Nat -> async Redirect }; 

  // is a destination, do work here
  #Execute: actor { execute: shared [(Nat, Blob)] -> Result<Ok, Err> } 
}

Then you can maintain the #Ask actors in a list and check for equality of actors to detect cycles.
This does assume that the methods are called ask and execute (or whatever you like) in the actors, unlike the previous approach.
You can do this because you can actually also test equality on actors, i.e. a1 == a2 works for actors a1 and a2 with the same interface.

hi claudio thanks for the reply

i actually want to expose the canister principal (hence the canister_id) so the caller can track which canister he has called and ensure he wont be in some kind of a loop, and the redirection has to follow a certain sequence (since each called canister tracks what should come before them and what comes after) so he also shouldnt skip a canister to ensure his track is the correct one.

wow i just knew we can do this. perhaps this can be useful in simplifying my design since we can use Principal.fromActor to do even further comparison (less or greater) using the canister id. I’ll try to change my design using this one. thanks claudio. :star_struck: … and i suppose by using this, i dont need canister_id anymore.