Some questions about subAccount

I hope to use motoko to develop an ICP wallet function, which is similar to a cryptocurrency exchange. It can be recharged and withdrawn without using II. It is equivalent to, 1. Each user provides an independent ICP recharge address, 2. Users can withdraw cash to any specified ICP address.
Read related official examples:
https://github.com/dfinity/examples/blob/master/motoko/defi/src/defi_dapp/main.mo
Still have some questions:

  1. Is it possible to create a subAccount without using a real caller, but to simulate a caller parameter based on the user ID in the contract canister. (Since we are not relying on the ICP identity system, users are not required to log in via II.)
Account.accountIdentifier(Principal.fromActor(this), Account.principalToSubaccount(caller));
  1. If the deposit address generated by subAccount has ICP transfer in, how can motoko know that there is funds coming in
     public shared(msg) func getDepositAddress(): async Blob {
         Account.accountIdentifier(Principal.fromActor(this), Account.principalToSubaccount(msg.caller));
     };
  1. When funds are deposited into the contract canister from multiple subAccount addresses, whether the funds will be collected to the default main account address at this time. If so, it’s relatively easy to transfer those funds out.
Account.accountIdentifier(Principal.fromActor(this), Account.defaultSubaccount());

Because in the sample code, Account.defaultSubaccount is used directly for each transfer, and my understanding is the main account. So in this case, what will happen to the balance of other subAccounts?

         // Transfer amount back to user
         let icp_reciept = await Ledger. transfer({
             memo: Nat64 = 0;
             from_subaccount = ?Account.defaultSubaccount();
             to = account_id;
             amount = { e8s = Nat64. fromNat(amount + icp_fee) };
             fee = { e8s = Nat64. fromNat(icp_fee) };
             created_at_time = ?{ timestamp_nanos = Nat64.fromNat(Int.abs(Time.now())) };
         });

Yes. You can use any arbitrary bytes to create a subaccount.

Typically this is done through notification methods. Once the transfer happened, the canister is asked to check and perform any relevant actions. Alternatively, you can use timers, but that can get expensive quickly

Every account:subaccount combination is completely independent of each other. They do not automatically get pooled

1 Like

Thanks for your quick answer.
Regarding question 3, please help me to see if my understanding and assumptions are correct? !

    private func withdrawIcp(caller: Principal, amount: Nat, account_id: Blob) : async T.WithdrawReceipt {
        Debug.print("Withdraw...");

        // remove withdrawal amount from book
        switch (book.removeTokens(caller, ledger, amount+icp_fee)){
            case(null){
                return #Err(#BalanceLow)
            };
            case _ {};
        };

        // Transfer amount back to user
        let icp_reciept =  await Ledger.transfer({
            memo: Nat64    = 0;
            from_subaccount = ?Account.defaultSubaccount();
            to = account_id;
            amount = { e8s = Nat64.fromNat(amount + icp_fee) };
            fee = { e8s = Nat64.fromNat(icp_fee) };
            created_at_time = ?{ timestamp_nanos = Nat64.fromNat(Int.abs(Time.now())) };
        });

        switch icp_reciept {
            case (#Err e) {
                // add tokens back to user account balance
                book.addTokens(caller,ledger,amount+icp_fee);
                return #Err(#TransferFailure);
            };
            case _ {};
        };
        #Ok(amount)
    };
  1. My doubt is that the from_subaccount of the withdrawIcp function of the sample code only specifies an Account.defaultSubaccount(). If the sub-account balances are independent of each other according to what you said, then this example is problematic for realistic business scenarios. Yes ?
  2. Since the sub-account balance is independent, when designing balance management and withdrawal, it is necessary to manage and transfer the balance in a “bend”:
    2.1. Create a corresponding balance mapping table for the sub-accounts within the contract, because only in this way can we know how many balances are in which sub-accounts, so that we can manage and transfer them out conveniently. The disadvantage of this is that the implementation of the program is relatively cumbersome. When the user has a reflected amount that exceeds any sub-account, it takes multiple Ledger.transfers to complete.
    2.2. Instead of using the balance mapping table, all the amount transferred to the sub-account is transferred to the defaultSubaccount for unified management. This avoids the problems mentioned above, but the disadvantage is that each account will generate an extra handling fee .

Is my understanding and assumption correct? How so I will start to continue coding.

It all depends on the use case. For many applications, one single account is enough. But if you want to store per-user balances, then subaccounts are the way to go

In my experience it’s not a big deal and you usually don’t need such a mapping table. You can simply attempt to perform withdrawals/transfers/whatever assuming the funds are there, and if things don’t work you just return the appropriate error

Ledgers in general have large enough data types that you can have more than total_supply in a single subaccount, so that should not be a problem

Correct, but we’re not talking TX fees of 3$+ like on other chains. Current ICP TX’s cost 0.0004 USD, which I think is manageable

1 Like

I understand, thank you!