ICRC icon indicating copy to clipboard operation
ICRC copied to clipboard

ICRC-11: Wallet / Personal Canisters

Open SuddenlyHazel opened this issue 2 years ago • 17 comments

icrc: 11
title: ICRC-11 Wallet / Personal Canister
author: Hazel Rowell <[email protected]>,
discussions-to: https://github.com/dfinity/ICRC/issues/11status: Deliberating 
category: ICRC
requires: None
created: 2023-02-26
updated: 2023-02-26

Moving from a forum topic to an issue..

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.

Summary

A minimum set of functions to help facilitate the development of wallet / personal canisters and clients.

References

ERC-725 Cycles Wallet

Introduction

This proposal describes a minimum set of functions and types which we hope will accelerate the development of wallet canisters and wallet canister clients as well as provide interoperability between the two. The goals of this standard are as follows:

  • Provide a well known canister method a variety of clients can use execute calls on behalf of a user.
  • Provide a key-value store for clients to discover associated WApps (Wallet Apps).
  • Provide a generalized, future proofed, authorization mechanism.

icrc11_make_call

Execute a blocking call against a canister on behalf of a caller.

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 : {
  icrc11_execute_call: (CanisterCallRequest) -> (CallResult);
}

icrc11_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 : {
  icrc11_set_data: (text, Value) -> (SetResult);
}

icrc11_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 : {
    icrc11_get_data : (text) -> (FetchResult);
}

icrc11_authorize_user & icrc11_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 : principal;
};

type AuthRequest = variant {
    Add : record {
        identity : opt Identity; // Optional to support backwards compatibility in older clients
    };
    Remove : record {
        identity : opt Identity; // Optional to support backwards compatibility in older clients
    };
};

type AuthorizeError = variant {
    NotAuthorized;
    NotSupported;
};

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 : {
    icrc11_authorize_user : (AuthRequest)  -> (AuthorizeResponse);
    icrc11_authorized_users : () -> (AuthorizedUsersResponse)
}

SuddenlyHazel avatar Feb 26 '23 16:02 SuddenlyHazel

[quote="Hazel, post:3, topic:18695"] Just leaving space for additional identity types. Maybe requests come packaged with a multi-sig proof? [/quote]

This makes sense to leave space...but isn't having it be a variant enough? Can't it just be:

type Identity = variant {
    Principal : principal
};

Could you share a candid?

Here is the .mo file

https://github.com/icdevs/candy_library/blob/main/src/types.mo

I'm thinking the thawed and frozen can go away now that we have better stable structures in motoko...and I don't think rust makes the distinction. It is basically what you have plus class and a couple explicit array helpers(one of which can be a multi-type array which is sometimes useful);

Blocking

I just wanted to make sure there wasn't some architecture reasoning that a canister wallet shouldn't call another function while waiting for one to return. This seems like it would be tricky to handle.

skilesare avatar Feb 26 '23 17:02 skilesare

This makes sense to leave space...but isn't having it be a variant enough? Can't it just be:

It definitely could. But, consider the following situation:

type Identity = variant {
    Principal : record {
       p : Principal;
       ttl : u64;
       expires_at : u64;
    };
};

For the near future this is something we definitely don't need. But, it would be awesome if we could accommodate it. However, in this case maybe this addition should have its own ICRC proposal. Or, maybe it should get its own variant..

type Identity = variant {
    Principal : principal;
    PrincipalWithExpiryAndTtl : record {
       p : Principal;
       ttl : u64;
       expires_at : u64;
    };
};

Yeah, I think you're right let's keep it simple for now.

Here is the .mo file https://github.com/icdevs/candy_library/blob/main/src/types.mo

So, right now I'm proposing the following:

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;
};

Candy, has the following candid shape

type Property = {name : text; value : CandyValue; immutable : Bool};

type Value = variant {
    #Int : int;
    #Int8: int8;
    #Int16: int16;
    #Int32: int32;
    #Int64: int64;
    #Nat : nat;
    #Nat8 : nat8;
    #Nat16 : nat16;
    #Nat32 : nat32;
    #Nat64 : nat64;
    #Float : float64;
    #Text : text;
    #Bool : bool;
    #Blob : blob;
    #Class : [Property];
    #Principal : principal;
    #Option : ?Value;
    #Array : {
      #frozen: [Value];
      #thawed: [Value]; //need to thaw when going to CandyValueUnstable
    };
    #Nats: {
      #frozen: [nat];
      #thawed: [nat]; //need to thaw when going to TrixValueUnstable
     };
    #Floats: {
      #frozen: [float64];
      #thawed: [float64]; //need to thaw when going to CandyValueUnstable
    };
    #Bytes : {
      #frozen: [nat8];
      #thawed: [nat8]; //need to thaw when going to CandyValueUnstable
    };
    #Empty;
  };

I think the simplified versions are pretty much the same thing. What do you think about removing some of the extra features for something like:

type Property = {name : text; value : Value};

type Value = variant {
    #Int : int;
    #Int8: int8;
    #Int16: int16;
    #Int32: int32;
    #Int64: int64;

    #Nat : nat;
    #Nat8 : nat8;
    #Nat16 : nat16;
    #Nat32 : nat32;
    #Nat64 : nat64;

    #Float : float64;
    #Text : text;
    #Bool : bool;
    #Blob : blob;

    #Class : [Property];
    #Principal : principal;
    #Option : opt Value;
    #Array : [Value];
    #Bytes : [nat8];
};

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

type FetchResponse = variant {
    Ok: Value;
    Err : opt FetchError;
};

type SetError = variant {
    NotAuthorized : null;
};

type SetResponse = {
    Ok : null;
    Err : opt SetError;
};

service : {
    icrc11_get_data : (text) -> (FetchResponse);
    icrc11_set_data: (text, Value) -> (SetResponse);
}

Note, I removed the #Empty value since we have the candid opt value which maps nicely in Motoko and Rust.

SuddenlyHazel avatar Feb 26 '23 18:02 SuddenlyHazel

Should we draw a diagram/graph or explain general implementation? There are a lot of things here:

  1. Users/Owner Role-based management
  2. Proxy/Delegate Call from canister
  3. Data setter/getter
  4. Function/method guard...

neeboo avatar Feb 26 '23 18:02 neeboo

@neeboo - That would definitely help. Sounds like we both had similar ideas with authorization systems.

Personally, for this standard I think we should keep the core as simple as possible.

  • a simple call interface
  • a simple key store
  • simple auth

What would be really awesome is if we introduced a proposal for a generalized resource / role based authorization interface 🤩 . Then, developers could just leverage that over the intentionally rudimentary system here.

The approach I've been taking when thinking about Wallet / Personal canisters is they're going to be the result of a bunch of isolated standards. Work towards that end goal and try to produce as many individually useful tools as possible. For example, if we can come up with a great standard for auth we can reuse that for the KV store, event systems, admin functions in DApps etc.

Thoughts?

SuddenlyHazel avatar Feb 26 '23 18:02 SuddenlyHazel

I would propose that this be discussed in the Identity Working Group.

A couple of broader questions:

  1. Why is it important to have a canister wallet standard, as opposed to treating all wallets equally (canister and external)?
  2. Is its main purpose an IDP (as suggested by ERC725, which never made it past draft) or a wallet?

benjizhai avatar Mar 01 '23 14:03 benjizhai

Why is it important to have a canister wallet standard, as opposed to treating all wallets equally (canister and external)?

Im not too familiar with the frontend wallet implementations today. But, I don't believe we can treat both the same even if we wanted to. At the very least adopting a standard at the wallet canister level would provide some unification independent of the wallets we have today. If we could unify both standards that would be excellent.

Is its main purpose an IDP (as suggested by ERC725, which never made it past draft) or a wallet?

I tried to use the nomenclature the community has adopted so far; "wallet canisters". In reality this is just a call proxy canister controllable by multiple principals (clients). I'm not familiar with idP the above links to the second iteration of the proposal which I think aligns more with my intentions.

SuddenlyHazel avatar Mar 01 '23 15:03 SuddenlyHazel

Happy to discuss more in the identity working group!

SuddenlyHazel avatar Mar 01 '23 15:03 SuddenlyHazel

I recently did a big update to the CandyLibrary to make it a bit more sane(and stable). V0.2.0 is now at: https://github.com/icdevs/candy_library/tree/0.2.0

The CandyShared type is now:

public type CandyShared = {
    #Int : Int;
    #Int8: Int8;
    #Int16: Int16;
    #Int32: Int32;
    #Int64: Int64;
    #Nat : Nat;
    #Nat8 : Nat8;
    #Nat16 : Nat16;
    #Nat32 : Nat32;
    #Nat64 : Nat64;
    #Float : Float;
    #Text : Text;
    #Bool : Bool;
    #Blob : Blob;
    #Class : [PropertyShared];
    #Principal : Principal;
    #Option : ?CandyShared;
    #Array :  [CandyShared];
    #Nats: [Nat];
    #Floats: [Float]; 
    #Bytes : [Nat8];
    #Map : [(CandyShared, CandyShared)];
    #Set : [CandyShared];
  };

I think the only difference is the addition of Map and Set which I think are helpful for holding data.

I kept the Nats, Floats, And Bytes, because those are really goof for calculating and manipulating specific data structures when you want to know that a particular type and only a particular type is in your array. The #Array theoretically allows multiple types in it and thus can get users into trouble when what they really want is something stored in a useable buffer structure of only one type.

A couple of questions/thoughts.

  • It looks like you suggested Float32 and Float 64. Motoko just has Float. How do you typically handle this? Can Rust canisters consume Motoko canisters that expose Float? (I'm assuming yes). Which one do they default to. I haven't seen a candid type of float32 or float64, so I'm guessing they default to float 64. Is there a reason to include float 32?
  • The map I use preserves insertion order while the one that @ByronBecker has made keeps things in key sorted order(binary sorted order that is) ). I'm thinking that SortedMap might be appropriate. But if that is the case...is Map a Map? A Map where you didn't care about order would be a Dictionary right? Would it be a SequentialMap? That doesn't really roll off the keyboard. Maybe a SeqMap? Maybe this isn't the right way to give the canister info, but I can see instances where we want to say "order matters" in a couple of different ways.
  • I'm thinking this pattern may make a good ICRC itself. Something along the lines of "Standard Document Object Model in Candid" or something like that.

Open to hearing thoughts.

skilesare avatar Mar 07 '23 04:03 skilesare

It looks like you suggested Float32 and Float 64. Motoko just has Float. How do you typically handle this? Can Rust canisters consume Motoko canisters that expose Float? (I'm assuming yes). Which one do they default to. I haven't seen a candid type of float32 or float64, so I'm guessing they default to float 64. Is there a reason to include float 32?

Ah! Good catch. Can edit to just support float64.

The map I use preserves insertion order while the one that @ByronBecker has made keeps things in key sorted order(binary sorted order that is) ).

Hmm, do we really need to apply that constraint here? Ultimately, the conversion from candid to Motoko or Rust will require inserting these into some real data structure. I can definitely see some of the use cases. But, as a developer I'd rather (a) stick these things into a vec or (b) explicitly attach a rank : nat field onto the value somewhere if the order is critical. Especially, since we can't enforce implementations will be behaving as intended.

I'm thinking this pattern may make a good ICRC itself. Something along the lines of "Standard Document Object Model in Candid" or something like that

This is a great idea! I was thinking a similar thing but for a generic KV store. Since our compute ceiling is so much higher this makes a lot more sense.

SuddenlyHazel avatar Mar 07 '23 15:03 SuddenlyHazel

Hmm, do we really need to apply that constraint here? Ultimately, the conversion from candid to Motoko or Rust will require inserting these into some real data structure. I can definitely see some of the use cases. But, as a developer I'd rather (a) stick these things into a vec or (b) explicitly attach a rank : nat field onto the value somewhere if the order is critical. Especially, since we can't enforce implementations will be behaving as intended.

I had this same though....and am still thinking about...What I think I'm hinting at is that it would be good to have the object composer able to give some intention about equivalence. We can't attach code in candid..which would be cool...but until we can, if the user wants to indicate that the order they are providing is important vs. not important could be important.

     type Map  = (CandyShared, CandyShared, ?Nat) //would this cover both?  If the user doesn't want the order then they can pass null and then order doesn't matter.

skilesare avatar Mar 07 '23 16:03 skilesare

This is a great idea! I was thinking a similar thing but for a generic KV store. Since our compute ceiling is so much higher this makes a lot more sense.

The nice thing about map here is that it is basically the interface for a generic kv store that supports most all of the types.

skilesare avatar Mar 07 '23 16:03 skilesare

type Map = (CandyShared, CandyShared, ?Nat) //would this cover both? If the user doesn't want the order then they can pass null and then order doesn't matter.

Wouldn't I be able to just do just a vec (Key, Value) and get the same result?

SuddenlyHazel avatar Mar 09 '23 17:03 SuddenlyHazel

Maybe more helpful..

I'm thinking the KV here should really just be used for storing some small configs. Say we have a personal canister implementing these standards.

  1. Pretend we decide to load up a client and point it to our canister. My vision is that the client would hit some known path something.wapps and use that to resolve the other information like the UIs for those components etc. As the ecosystem grows maybe this path could be a pointer to the more featured document db you're describing.
  2. Maybe some light weight Verifiable Claims could be stashed? Maybe some well known path could be used to resolve a username, etc.

So, I'm kinda thinking about this as the smallest possible building block for the much larger set of potential future features.

SuddenlyHazel avatar Mar 09 '23 17:03 SuddenlyHazel

Hi @SuddenlyHazel

Thanks a lot for starting this great discussion! I would love to have a discussion about this in the identity & wallets working group.

One thing I noticed is that the goals are quite orthogonal, no? I.e. each of those

  • Provide a well known canister method a variety of clients can use execute calls on behalf of a user.
  • Provide a key-value store for clients to discover associated WApps (Wallet Apps).
  • Provide a generalized, future proofed, authorization mechanism.

could easily be its own ICRC. And I would argue, that it probably should, because otherwise unrelated discussions could slow down or hinder the adoption of the standard.

And as @benjizhai has pointed out, I think it would make sense to have a wallet standard that is agnostic to the type of wallet.

Also another point not covered is the whole user-interaction part, i.e. how to present authorization requests to the user. I have also spent some time thinking about this part, resulting in the following draft spec: proxied canister calls

(Note: I'm not that happy anymore with how the draft is structured and I would like to separate parts of it into their own ICRCs, in particular the user consent message mechanism. But this will be discussed in the next working group meeting on April 4.)

Please join the working group session if possible and let's push forward on this together! 🙂

frederikrothenberger avatar Mar 31 '23 07:03 frederikrothenberger

The timing of the working group is a bit challenging for me. But, I'll see if I can rework my schedule to make it.

One thing I noticed is that the goals are quite orthogonal, no?

Completely agree here. Originally, I was thinking about breaking these into separate proposals. Maybe that should still be the case?

And as @benjizhai has pointed out, I think it would make sense to have a wallet standard that is agnostic to the type of wallet.

I think the naming of this proposal might be misleading. In my mind this canister would be called by wallets; acting more like a client canister to interface with other canisters. But, this could still apply, I'm just not sure there would be much to gain (It sure wouldn't hurt though). An interesting question.. Invoking a plugin wallet, fetching an identity from II, and making the ingress call here are all wildly different paths.. How much can we unify.. An interesting question for sure!

Anyways, I'll try to make it as soon as I can. Thanks!

SuddenlyHazel avatar Mar 31 '23 12:03 SuddenlyHazel

type AuthRequest = variant {
    Add : record {
        identity : opt Identity; // Optional to support backwards compatibility in older clients
    };
    Remove : record {
        identity : opt Identity; // Optional to support backwards compatibility in older clients
    };
};

Why use records since they deviate from the pattern can't we just use the opt Identity for the value?

type AuthRequest = variant {
    Add : opt Identity; // Optional to support backwards compatibility in older clients
    Remove : opt Identity; // Optional to support backwards compatibility in older clients
};

ALLiDoizCode avatar Sep 22 '23 04:09 ALLiDoizCode

I'm no longer championing this proposal. Feel free to take it over.

SuddenlyHazel avatar Sep 22 '23 18:09 SuddenlyHazel