Wallet canister standards

Starting a thread here so we can start figuring out some standards to bring about wallet canisters. For starters I think we need a ERC-725 like standard. A common way to make calls and a small key value store is a great foundation to start building wallets / personal canisters / whatever on.

How about the following to kick off discussion :slight_smile: .

icrc5_make_call

Execute a blocking call against a canister.

type CanisterCallRequest = record {
  canister : principal;
  method : text;
  cycles : nat64;
  args : vec nat8;
};

type RejectionCode = variant {
  NoError: null,
  SysFatal: null,
  SysTransient: null,
  DestinationInvalid: null,
  CanisterReject: null,
  CanisterError: null,
  Unknown: null,
};

type CallFailure = variant {
  NotAuthorized: record { reason: text },
  Error: record { code: RejectionCode, message: text },
};

type CallResult = variant {
  Ok: vec nat8;
  Err: CallFailure;
};

service : {
  icrc5_execute_call: (CanisterCallRequest) -> (CallResult);
}

icrc5_set_data

Set a value in the store

type Value = variant {
    Text : text;
    
    Blob : blob;
    Bool : bool;
    
    Option : Value;
    
    Vec : vec Value;
    Record : vec (text, Value);

    Nat : nat;
    Nat8 : nat8;
    Nat16 : nat16;
    Nat32 : nat32;
    Nat64 : nat64;
    
    Int : int;
    Int8 : int8;
    Int16 : int16;
    Int32 : int32;
    Int64 : int64;
    
    Float32 : float32;
    Float64 : float64;
    
    Principal : principal;
};

type SetError = variant {
    NotAuthorized : null;
};

type SetResult = {
    Ok : null;
    Err : set_error;
};

service : {
  icrc5_set_data: (text, Value) -> (SetResult);
}

icrc5_get_data

Fetch a value from the store


type Value = variant {
    Text : text;
    
    Blob : blob;
    Bool : bool;
    
    Option : Value;
    
    Vec : vec Value;
    Record : vec (text, Value);

    Nat : nat;
    Nat8 : nat8;
    Nat16 : nat16;
    Nat32 : nat32;
    Nat64 : nat64;
    
    Int : int;
    Int8 : int8;
    Int16 : int16;
    Int32 : int32;
    Int64 : int64;
    
    Float32 : float32;
    Float64 : float64;
    
    Principal : principal;
};

type FetchError = variant {
    NotAuthorized : null;
    KeyNotFound : null;
};

type FetchResult = variant {
    Ok: Value;
    Err : FetchError;
};

service : {
    icrc5_get_data : (text) -> (FetchResult);
}

icrc5_authorize_user & icrc5_authorized_users

Authorize a user and fetch authorized users.

Identity is presented as a variant type to allow for additional types of identities to be added.

type Identity = variant {
    Principal : record {
        p : principal;
    }
};

type AuthRequest = variant {
    Add : record {
        identity : Identity;
    };
    Remove : record {
        identity : Identity;
    };
};

type AuthorizeError = variant {
    NotAuthorized;
};

type AuthorizeResponse = variant {
    Ok : null;
    Err : AuthorizeError;
};

type AuthorizedUser = record {
    identity : Identity;
    created_at : u64;
};

type AuthorizedError = variant {
    NotAuthorized;
};

type AuthorizedUsersResponse  = variant {
    Ok : vec AuthorizedUser;
    Err : AuthorizedError;
};

service : {
    icrc5_authorize_user : (AuthRequest)  -> (AuthorizeResponse);
    icrc5_authorized_users : () -> (AuthorizedUsersResponse)
}
8 Likes

This is super great!

A couple suggestions/questions :

  1. Why the extra layer of p?

‘’’
type Identity = variant {
Principal : record {
p : principal;
}
};
‘’’

  1. For the value types I’d suggest something like CandyLibrary types GitHub - icdevs/candy_library: Library for Converting Types and Creating Workable Motoko Collections that are just a bit more dynamic. It includes the #class type and the properties construct that you and @quint used back in the day. We currently have a bounty assigned building a rust version and with a bit of curation from someone with a better computer science background it could become a pretty cool data access paradigm. We already haven engineer building a candy path library as well for deep queries. GitHub - ZhenyaUsenko/motoko-candy-utils

This is what we use in the origyn nft standard and with a few conventions we’ve implemented permissioned nodes as well. Maybe we are getting a bit to complex for a first turn here, but at the same time I feel we should start using the power of this baby.

  1. I know composite queries are coming, should we include an endpoint for that as a stub? Maybe one for pure canister to canister queries as well?

  2. What do mean “blocking call”? Does this open your canister up to an attacker tricking you into calling long running calls? Does it need to block?

3 Likes

Just leaving space for additional identity types. Maybe requests come packaged with a multi-sig proof?

Could you share a candid? But, also leaning towards keeping thinks really simple. My intention here was to expose a KV that would be used for the most simple applications. Maybe some simple client configs, maybe pointing to a more powerful robust store :slight_smile: .

I honestly haven’t been following the composite queries discussions. But, this likely makes sense considering the purpose!

Blocking here means the canister should return the result to the calling client. I’ve read about this a few times, but how big is the actual threat here? The malicious canister would have to eventually return (burning their own cycles for the duration of the attack). Additionally, your canister would still be free to make additional responses. So, we just wouldn’t be able to update the canister for the duration of this, no? (Is this enforced at the protocol level or can we just remove the callback pointer by force if we need to?)

2 Likes
2 Likes

Thanks for the post.

We have our design, it’s here:

Interface would be:

type CallCanisterArgs = record {
  args : vec nat8;
  cycles : nat;
  method_name : text;
  canister : principal;
};
type CallResult = record { return : vec nat8 };
type ExpiryUser = record {
  user : principal;
  expiry_timestamp : nat64;
  timestamp : nat64;
  target_list : vec ProxyActorItem;
};
type Method = record {
  name : text;
  method_type : MethodType;
  key_operation : bool;
};
type MethodType = variant { CALL; OneWay; CompositeQuery; QUERY };
type MethodValidationType = variant { ALL; KEY; UPDATE };
type OwnerReply = variant { Approved : Result_3; NotFound; Rejected : text };
type ProxyActorItem = record {
  methods : vec record { text; Method };
  canister : principal;
};
type ProxyActorTargets = record {
  targets : vec ProxyActorItem;
  expiration : opt nat64;
};
type QueueHash = record { hash : text; user : principal; time_stamp : nat64 };
type Result = variant { Ok : nat; Err : text };
type ProxyCallResult = variant { Ok : CallResult; Err : text };
type RemoveQueueResult = variant { Ok : bool; Err : text };
service : () -> {
  add_expiry_user : (principal, ProxyActorTargets) -> (ExpiryUser);
  add_proxy_black_list : (principal) -> (text);
  balance_get : () -> (Result) query;
  get_queue_reply : (text) -> (opt OwnerReply) query;
  get_queue_unconfirmed : (principal) -> (vec QueueHash) query;
  has_queue_method : (text) -> (bool) query;
  is_proxy_black_list : (principal) -> (bool) query;
  owner_confirm : (text, bool) -> (OwnerReply);
  proxy_call : (CallCanisterArgs) -> (ProxyCallResult);
  remove_proxy_black_list : (principal) -> (opt text);
  remove_queue_method : (text) -> (RemoveQueueResult);
  set_expiry_period : (nat64) -> ();
  set_method_validate_type : (MethodValidationType) -> ();
}

I will try and explain the interface design in later post

4 Likes

@neeboo, this looks great. Mind collaborating here: ICRC-11 Wallet / Personal Canisters · Issue #11 · dfinity/ICRC · GitHub ?

3 Likes

Absolutely, I will move there and LFG

5 Likes
1 Like