Completed: ICDevs.org - Bounty #26 - ICRC-1 Motoko - up to $10k

It’s interesting that the ledger accounts delete balances that are not zero. Does it only choose the account balances below a certain number of decimals?

@Icypee I think the balances are in the main canister because an account balance requires less bytes than a transaction. And transactions tend to grow more than the number of new accounts that buy a token.

The max size of a transaction is about 196 bytes, and an account balance is about 76 bytes.
Assuming the max size of a canister is 4 GB, it’s possible to store 56M account balances in a single canister and only about 21M transactions.


The screenshot above is of the USDT token, one of the top tokens on the ethereum network with about 4M holders and 160M transactions .

Using this example, we can see that the main canister can store 14x the number of account balances but will need to upgrade to multiple archive canisters to store all the transactions.

2 Likes

Nope…it just orders them and deletes the smallest balances until it gets to the desired number.

1 Like

Ahh I see, thats a good point. Yea in reality and esp. with I think canister now moving to 32 gb storage its almost unlikely that another canister is needed. However, my question was more of a thought excercise to see if there was a way to have multi canister balances for one token and keep evertyhing consistent. I come from a education background where we always test with extremas for both 0 and as the limit goes to infinity so having storage limits like this where I can see a potential to scale to infinity keeps me up at night to solve it even if its almost never needed :sweat_smile:

1 Like

Timo has talked about multi-canister ledgers before, but that is really the only discussion I’ve seen about it. As far as motoko ICRC-1 goes…I think we just need to pressure-test it and see how much the performance differs from rust. I’m not expecting that much of a difference given the recent streaming upgrade upgrade.

1 Like

Is this bounty finished or has ICDev or Dfinity reviewed this implementation of the ICRC1 standard?

Also am I right to think that a canister that uses this library (e.g. icrc1/main.mo at main · NatLabs/icrc1 · GitHub) can be used as the ledger canister for a SNS ? In order to be able to kick off a project creating the token first, and maybe later give the control to a SNS governance.

@tomijaga , @Iceypee any status updates? I think the last things we were waiting for were the Rosetta integrations.

1 Like

Oh @skilesare I wasnt assigned this bounty. Was just asking questions and learning.

3 Likes

Hey, @sardariuss I haven’t been keeping up with the developments of SNS but if it supports the icrc1 standard then you should be able to use this library. Maybe @skilesare can tell you for sure if it does. I’ve implemented all the functions for the icrc1 token standard but I would suggest waiting till the library has been reviewed by the IcDev org before using it in production.

Rosetta Integration

This is the function for the Rosetta API integration.

The GetTransactionsResponse type for this function has a field, archived_transactions that has a callback fn embedded in it. The problem with this is fns are considered shared types in motoko and shared functions are only allowed as public fields in actor classes for now so I keep getting errors when I implement a solution for this type.

I wrote this piece of code for the get_transactions fn but it returns an error:

Stderr:
/Users/dire.sol/Documents/dev/icp/icrc1/src/ICRC1/Canisters/Rosetta.mo:84.111-88.18: type error [M0077], a shared function is only allowed as a public field of an actor
  (This is a limitation of the current version.)

Is there any way to return a shared function from an actor without getting this error?

1 Like

Cc @claudio can you take a look?

1 Like

On mobile, but try to define function ‘callback’ as a public shared function at the same level as, and similarly to, for example, ‘get_transactions’ instead of locally within ‘get_transactions’.

You may want to protect this public function being called by any old caller by checking the caller id appropriately, if that makes sense here.

In Motoko programs targeting the IC, shared functions can only refer to public methods of actors, not locally declared or anonymous functions.

2 Likes

Thanks for your help @claudio!
I understand how to use shared functions better now.
I was able to assign the archive canister’s public get_transactions function to the callback field and return the response without any errors.

@skilesare The rosetta implementation is done and ready for a full review. Let me know if there is anything else that needs to be done.

4 Likes

Thanks to all involved in creating this. I’ll be using it soon and as a critical piece.

I have a basic question regarding ICRC and their interaction with canisters here, in case any of you know: How to get a canister's address to send ICRC tokens to, and generate further subaccounts(?) to do the same?

I’ll paste it here for ease:

I have a canister I need to send an ICRC token to. (The aim is for the canister to receive and hold an ICRC token balance).

How do I, as owner of the canister,

(a) get the canister’s default address to send ICRC tokens to, and

(b) generate additional addresses (not sure if they’re called subaccounts, whatever they are) controlled by the same canister to send ICRC tokens to?

Actually, does the ICRC-1 standard distinguish between such subaccounts (I don’t know if that’s the term), or does it only recognise one account per canister?

ICRC-1 uses the following type for accounts:

type Subaccount = blob;
type Account = record { owner : principal; subaccount : opt Subaccount; };

So the default accounts is just:

let canister account = {
   owner = Principal.fromActor(this);
   subaccount = null;
};

Additional accounts can be created by using the subaccount. You theoretically have infinity subaccount.

If you wanted to create an account that was specific for one of your users you would do:

let canister account = {
   owner = Principal.fromActor(this);
   subaccount = Principal.toBlob(msg.sender);  //maybe add some magic and hash for more privacy: see: https://github.com/ORIGYN-SA/origyn_nft/blob/99b9460eab0f841eb621deee7863d926d30421ae/src/origyn_nft_reference/utils.mo#L162
};
2 Likes

Thanks. Does this mean that if you want to send ICRC tokens to a canister’s subaccount, you have to separately specify both the principal and the subaccount?

I want to tell the user just: “transfer ICRC tokens to SOME_SINGLE_STRING because that’s the address of the account associated uniquely with you controlled by this canister.”

A bit like the deposit address centralized exchanges give you. It’s a single address, and if someone sends money to it, it is used to credit your account in the centralized exhange.

How do I get such SOME_SINGLE_STRING? Ideally I don’t want the user to deal with two distinct pieces. Just one place to send their tokens to.

type Account = record { owner : principal; subaccount : opt Subaccount; };

In other words, is it possible to represent the Account type above, in the case of a non-empty subaccount, as a single string address that I give the user for him to just send to that string? How do I do that?

The textual representation of accounts is part of the ICRC-1 spec.

Thank you. Does that mean that every developer who wants to display or read the textual representation of an account has to manually encode and decode the accounts with functions inside the dapp?

Ideally I’m looking for something along the lines of Account.toTextualRepresentation() that I can just use without implementing anything manually.

If there needs to be a dedicated implementation in every dapp, has anyone written the Motoko code to encode and decode them?

1 Like

Thanks. I think it clicked in my head now.

1 Like

Do you know why I’m getting

unbound variable this

in this context?:

type Account = {
    owner : Principal;
    subaccount : Subaccount;
  };

public shared (msg) func create_user() : async User {
    // Get caller principal
    let callerId = msg.caller;

    let repayment_account : Account = {
      owner = Principal.fromActor(this);
      subaccount = Principal.toBlob(msg.sender);
    };

That’s all inside my backend canister actor. It’s an actor class, not sure if that makes a difference.

When you declare your actor you can set this:

shared (deployer) actor class Nft_Canister(__initargs : Types.InitArgs) = this {

}

1 Like