Help Needed: Generating Unique Subaccounts for Order Payments in Motoko

Background

We’re developing a system where we need to generate a new subaccount for every new payment created. The idea is to use the orderId to generate the subaccount, ensuring that each order gets a unique payment address. This approach should make it universal for any wallet to pay.

Current Implementation

Here’s the relevant part of our Motoko code:

private func generateSubaccount(orderId : Nat32) : Blob {
    let orderIdBytes = nat32ToBytes(orderId);
    let paddingSize : Nat = 32 - orderIdBytes.size();
    let padding = Array.tabulate<Nat8>(paddingSize, func(_ : Nat) : Nat8 { 0 });
    let subaccountBytes = Array.append<Nat8>(padding, orderIdBytes);
    Blob.fromArray(subaccountBytes);
};

private func nat32ToBytes(n : Nat32) : [Nat8] {
    [
        Nat8.fromNat(Nat32.toNat((n >> 24) & 0xFF)),
        Nat8.fromNat(Nat32.toNat((n >> 16) & 0xFF)),
        Nat8.fromNat(Nat32.toNat((n >> 8 & 0xFF)),
        Nat8.fromNat(Nat32.toNat(n & 0xFF)),
    ];
};

public shared (msg) func createOrder(amount : Nat) : async Result.Result<Order, Text> {
    // ... (other code)
    let orderId = await generateRandomOrderId();
    let subaccount = generateSubaccount(orderId);

    let newOrder : Order = {
        id = orderId;
        amount = amount;
        paymentAddress = subaccount;
        status = #Pending;
        timestamp = Time.now();
    };
    // ... (rest of the function)
};

And here’s how we’re handling it on the frontend:

Javascript:

try {
  const actor: any = await window.ic.plug.createActor({
    canisterId: "CanisterId",
    interfaceFactory: idlFactoryB,
  });
  const orderResult = await actor.createOrder(BigInt(1001));
  console.log("Order Result:", orderResult);

  const order = orderResult.ok;
  const paymentPrincipal = Principal.fromUint8Array(order.paymentAddress);

  console.log("Payment Address (Text):", paymentPrincipal.toText());
  console.log("Payment Address (Hex):", paymentPrincipal.toHex());

  console.log("orderResult", orderResult);
} catch (error) {
  console.log("error", error);
}

The Problem

We’re facing an error where we’re getting a long address format that isn’t supported. Specifically, when we convert the payment address to text on the frontend, we’re getting an address like this:

mtt3j-jyaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-anknp-5im

This format seems to be incorrect for a subaccount or payment address.

Questions

  1. Is our approach to generating subaccounts from orderId correct? If not, what would be a better way?

  2. How can we ensure that the generated subaccount is in a format that’s compatible with IC ledger accounts?

  3. Are we missing any steps in converting the subaccount to a valid payment address?

  4. Is there a standard way to generate unique subaccounts for payments in Motoko that we should be using instead?

  5. Why are we getting this long principal-like address when converting the subaccount to text, and how can we get the correct format?

  6. Should we be using Principal.fromUint8Array on the frontend, or is there a more appropriate way to handle the subaccount blob?

## Additional Context

  • We’re using a Nat32 for the orderId.

  • The generateSubaccount function is creating a 32-byte Blob, which we thought would be the correct format for a subaccount.

  • We’re not sure if we need to do additional processing to turn this subaccount into a valid payment address.

  • On the frontend, we’re using the Principal class to convert the subaccount blob to text, which might not be the correct approach.

Any insights, code examples, or explanations would be greatly appreciated.

Thank you,
Blockbolt

1 Like

The principal should be the canister id that receives the payment, right now you’re creating a principal from the subaccount bytes, which makes no sense since they’re separate things.

(An account on the IC is a principal and the (32) subaccount bytes)

For ICRC-1 you can simply pass an account object with both the principal and subaccount bytes when making a transaction.

For the older ICP ledger account id you need to hash these inputs together as seen here: tokens-js/packages/tokens/src/utils/account.ts at main · slide-computer/tokens-js · GitHub

So basically, every canister and wallet account has its own principal (called owner on ICRC-1), the subaccount bytes are additional optional data that can be used in ledgers to divide tokens with the same principal.

Thank you for bringing this to our attention that you need clarity. Will follow-up internally and get back to you.

To clarify a few points:

  1. Could you be confusing a principal vs. subaccount vs. account? You can refer to the terminology section here.

As a high-level, a principal is an entity that can be authenticated by ICP. Principals are used to identify canisters, individuals, services, ledger accounts, or other components deployed on ICP. A principal can have multiple accounts because you can change the bytes of the subaccount.

A Subaccount is an optional 32-byte array. Together a principal and a subaccount form an account according to the ICRC-1 standard.

  1. With this, as @sea-snake implied, I assume that you are looking into creating a unique account for each order from your canister Principal ID and unique subaccount.

For example, a unique account could be based on your canister Principal ID, and the subaccount could be a 32-byte array that represents an order number.

Please clarify.

1 Like

Hello Jennifer,

Thank you for your response.

Yes, we are considering creating a separate sub-account for each order. @sea-snake

We would like to create a sub-account using the canister’s principal address, rather than the caller’s principal.

The sub-account must be formatted in a way that is accepted by NNS for sending ICP, ckBTC, and other currencies.

Thanks

1 Like