Local replica of Ledger canister throwing "insufficient funds" error even though the canister has funds

I’m testing the Ledger canister locally. I’ve confirmed that the canister that I sending ICP from has funds in them and am attempting to send some of those funds to another canister. When I do, i get an insufficient funds error. I suspect that this may be due to the dfx version that I’m using. I’m currently using version "0.8.3". would anyone be able to confirm or deny whether or not this version would prohibit me from being able to send ICP from canisters? I remember hearing about how only recently are canisters able to send ICP and i know i configure my project long before said feature was enabled.

Are you using the dfx ledger command, or the dfx canister call <local-ledger> command?

The command that I used to check the canister balance is:

dfx canister call ledger account_balance '(record { account = '$(python3 -c 'print("vec{" + ";".join([str(b) for b in bytes.fromhex("'$JESSE_ACC'")]) + "}")')' })'

The response I get is:

(record { e8s = 10_000_000_000 : nat64 })

also, when I render my app, it displays the correct wallet balance in the UI as well. I should clarify. the ledger transfer works when I do so using the command line. where I’m having trouble is testing the transfer method that I made in my motoko backend. The error message that I’m getting is precoded.

here is the transfer method I coded:

public func transferICP(amount: Nat64, recipientAccountId: Account.AccountIdentifier) : async Bool {

        let res = await Ledger.transfer({
          memo = Nat64.fromNat(10);
          from_subaccount = null;
          to = recipientAccountId;
          amount = { e8s = amount };
          fee = { e8s = 10_000 };
          created_at_time = ?{ timestamp_nanos = Nat64.fromNat(Int.abs(Time.now())) };
        });

        switch (res) {
          case (#Ok(blockIndex)) {
            Debug.print("Paid reward to " # debug_show principal # " in block " # debug_show blockIndex);
            return true;
          };
          case (#Err(#InsufficientFunds { balance })) {
            throw Error.reject("Top me up! The balance is only " # debug_show balance # " e8s");
            return false;
          };
          case (#Err(other)) {
            throw Error.reject("Unexpected error: " # debug_show other);
            return false;
          };
        };
    };

and the error message I’m getting is:

Reject text: Top me up! The balance is only {e8s = 0} e8s
1 Like

Maybe there’s a discrepancy between $JESSE_ACC and the accountIdentifier of your canister’s default subaccount?

The UI uses the same account identifier for rendering the balance and transferring ICP. When the UI renders the balance, it shows that there are funds there.

Right - you are having consistent success on checking a balance, but I’d first try to make sure that the balance your checking matches up to the accountIdentifier resulting from

let res = await Ledger.transfer({
  memo = Nat64.fromNat(10);
  from_subaccount = null;
  to = recipientAccountId;
  amount = { e8s = amount };
  fee = { e8s = 10_000 };
  created_at_time = ?{ timestamp_nanos = Nat64.fromNat(Int.abs(Time.now())) };
});

Maybe if you set up

  func defaultSubaccount() : Subaccount {
    Blob.fromArrayMut(Array.init(32, 0 : Nat8))
  };

  func accountIdentifier(principal: Principal, subaccount: Subaccount) : AccountIdentifier {
    let hash = SHA224.Digest();
    hash.write([0x0A]);
    hash.write(Blob.toArray(Text.encodeUtf8("account-id")));
    hash.write(Blob.toArray(Principal.toBlob(principal)));
    hash.write(Blob.toArray(subaccount));
    let hashSum = hash.sum();
    let crc32Bytes = beBytes(CRC32.ofArray(hashSum));
    let buf = Buffer.Buffer<Nat8>(32);
    Blob.fromArray(Array.append(crc32Bytes, hashSum))
  };

  public func whoami () : AccountIdentifier {
    let canisterId = Principal.fromActor(CanisterName);
    return accountIdentifier(canisterId, defaultSubaccount());
  }

You’ll have the easiest way to check

I’ll give this a try. Thanks a bunch!

I think i may have found the issue. I think the issue is how I’m defining the account-Id.

func userAccountId() : Account.AccountIdentifier {
        Account.accountIdentifier(principal, Account.defaultSubaccount())
    };

I used the user’s principal to define the account-id as opposed to using the canster’s principal. can you confirm that the ledger.transfer() function uses the canister’s principal to send ICP? if thats the case, then thats certainly the issue.

1 Like

Yes, the ledger uses the canister’s ID as the base, and the canister can only control assets under its own principal.

This is a big part of what I solve with my Invoice Canister Design

2 Likes

I have a similar problem in that I can get the account balance using Ledger.account_balance in motoko but then when I try Ledger.transfer I get #Err(#InsufficientFunds({balance = {e8s = 0}})).

I check the account balance and it has e8s, then I use the same account as the from_subaccount but I get the insufficient funds error.

Here is the code:

public func withdrawICP(defaultAccount: Principal, user: Principal, amount: Float, walletAddress: Text) : async Result.Result<(), Types.Error> {

    let e8Amount = Int64.toNat64(Float.toInt64(amount * 1e8));
    let source_account = Account.accountIdentifier(defaultAccount, Account.principalToSubaccount(user));
    let balance = await Ledger.account_balance({ account = source_account });

    Debug.print(debug_show balance);
    
    if(balance.e8s < icp_fee){
        return #err(#NotAllowed);
    };

    let withdrawable = balance.e8s - icp_fee;

    if(e8Amount > withdrawable){
        return #err(#NotAllowed);
    };

    Debug.print(debug_show balance);
    let account_id = Account.accountIdentifier(user, Account.defaultSubaccount());

    let result = await Ledger.transfer({
        memo: Nat64    = 0;
        from_subaccount = ?source_account;
        to = account_id;
        amount = { e8s = e8Amount };
        fee = { e8s = icp_fee };
        created_at_time = ?{ timestamp_nanos = Nat64.fromNat(Int.abs(Time.now())) };
    });

    Debug.print(debug_show result);

    return #ok(());
};

Ok I am able to do half of what I need by using:
let result = await Ledger.transfer({
memo: Nat64 = 0;
from_subaccount = ?Account.principalToSubaccount(user);
to = account_id;
amount = { e8s = e8Amount };
fee = { e8s = icp_fee };
created_at_time = ?{ timestamp_nanos = Nat64.fromNat(Int.abs(Time.now())) };
});

But the amount doesn’t go back to the users account in the NNS, which is defined by:
let account_id = Account.accountIdentifier(user, Account.defaultSubaccount());

Where user is the caller’s principal.

Ok I now have the functionality I need, I just convert the wallet address to a blob using:

I have a similar issue where I’m getting an “Insufficient funds” error while I’m sure the account has funds.

In my case I’m using the icrc2_approve method, like so:

public shared ({ caller }) func approve(allowance : Nat) : async Result.Result<IcpLedger.BlockIndex, Text> {
    let approveArgs : IcpLedger.ApproveArgs = {
        spender = {
            owner = caller;
            subaccount = null;
        };
        from_subaccount = null;
        amount = 10000000000000;
        expected_allowance = ?allowance;
        expires_at = null;
        fee = ?10000;
        memo = null;
        created_at_time = null;
    };
    try {
        let approvalResult : IcpLedger.ApproveResult = await IcpLedger.icrc2_approve(approveArgs);
        // check if the approval was successfull
        switch (approvalResult) {
            case (#Err(error)) {
                return #err("Couldn't approve funds: " # debug_show (error));
            };
            case (#Ok(blockIndex)) { return #ok(Nat64.fromNat(blockIndex)) };
        };

    } catch (error) {
        return #err("Reject message: " # Error.message(error));
    }
};

I’m getting Couldn't approve funds: #InsufficientFunds({balance = 0}).

The method is called from the ICP ledger canister, but in the arguments, the spender is the caller which is supposed to be the user :thinking:

Has anybody run into this before ?

P.S: I’ve tried to trigger my approve method both from the frontend and from the CLI, with different accounts that all have some tokens (i.e account_balance returns a nat) and each time it’s giving me balance = 0.

P.P.S: is there a difference between ICP and LICP locally?