dfinity / ICRC

Repository to ICRC proposals
Apache License 2.0
26 stars 5 forks source link

ICRC-11: Wallet / Personal Canisters #11

Open SuddenlyHazel opened 1 year ago

SuddenlyHazel commented 1 year ago
icrc: 11
title: ICRC-11 Wallet / Personal Canister
author: Hazel Rowell <hazel@departure.dev>,
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:

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)
}
skilesare commented 1 year ago

[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.

SuddenlyHazel commented 1 year ago

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.

neeboo commented 1 year ago

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...
SuddenlyHazel commented 1 year ago

@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.

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?

benjizhai commented 1 year ago

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?
SuddenlyHazel commented 1 year ago

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 commented 1 year ago

Happy to discuss more in the identity working group!

skilesare commented 1 year ago

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.

Open to hearing thoughts.

SuddenlyHazel commented 1 year ago

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.

skilesare commented 1 year ago

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 commented 1 year ago

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.

SuddenlyHazel commented 1 year ago

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 commented 1 year ago

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.

frederikrothenberger commented 1 year ago

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

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! 🙂

SuddenlyHazel commented 1 year ago

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!

ALLiDoizCode commented 9 months ago
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
};
SuddenlyHazel commented 9 months ago

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