Skip to content
This repository was archived by the owner on Feb 23, 2023. It is now read-only.

Latest commit

 

History

History
226 lines (161 loc) · 12.6 KB

muxed-accounts.mdx

File metadata and controls

226 lines (161 loc) · 12.6 KB
title order
Muxed Accounts

Muxed Accounts

import { Alert } from "components/Alert"; import { CodeExample } from "components/CodeExample";

A muxed (or multiplexed) account is an account that exists "virtually" under a traditional Stellar account address. It combines the familiar GABC... address with a 64-bit integer ID and can be used to distinguish multiple "virtual" accounts that share an underlying "real" account.

Warning: This feature is experimental. Muxed accounts are not widely supported yet. Many wallets may not display a muxed account address and instead display the account ID address, ignoring the 64-bit ID. Adoption efforts are actively in progress.

Muxed accounts were defined in CAP-27, introduced in Protocol 13, and their string representation is described in SEP-23. They have their own address format that starts with an M prefix. For example, from the account address GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ, we can create new muxed accounts with two different IDs:

  • MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAACJUQ has the ID 0, while
  • MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAABUTGI4 has the ID 420.

Both of these addresses, when used with one of the supported operations, will act on the underlying GA7Q... address.

With muxed accounts, we can natively support the ecosystem-level abstraction of account memo requirements (see SEP-29), where transaction memos were often used to distinguish between users backed by a single custodial account. Now, the embedded ID can be used for this distinction, and guides like this one can be made superfluous. In other words, virtual accounts are now a first-class citizen on the network.

There are many other benefits to embedding this abstraction into the protocol:

  • "Share-ability": Rather than worrying about error-prone things like copy-pasting memo IDs, you can just share your M... address.
  • SDK support: The various SDKs support this abstraction natively, letting you create, manage, and work with muxed accounts easily.
  • Efficiency: By combining related virtual accounts under a single account's umbrella, you can avoid holding reserves and paying fees for all of them to exist in the ledger individually.

Terminology Tip: The term muxing comes from computer networking and telecommunications, where multiple signals are combined into one signal over a shared medium. We are doing the same thing here with muxed accounts: combining multiple "virtual" accounts into a single shared account in the Stellar ledger.

It's worth noting that this feature is intended to be a high-level abstraction, merely embedded into the protocol for convenience and standardization. There's no validation on IDs: as far as the Stellar Network is concerned, all of the supported operations operate exactly as if you did not used a muxed account. For example, if you make two payments from two muxed accounts that share an underlying Stellar account (i.e. muxed accounts with the same underlying G... account but two different IDs), this is exactly the same as that single Stellar account sending two payments, as far as the ledger is concerned.

Even though only the underlying G... account truly exists in the Stellar ledger, the Horizon API will make some effort to interpret and track the muxed accounts responsible for certain actions.

Supporting Opt-In

Because muxed account support is still experimental, you need to explicitly opt-in to creating operations with muxed properties. Eventually, muxed support will be the default, but for now, if you do not pass these opt-in flags in the necessary places, SDKs will throw errors. This is done in an effort to ensure that muxed accounts are not "accidentally" working in your application and thus may be misleading to your users.

Each SDK implements this a little differently, but generally-speaking you need to explicitly call out muxing support when creating each operation and/or transaction that uses muxed accounts.

Supported Operations

Not all operations can be used with muxed accounts. For example, you cannot set the destination of a CreateAccount operation to be a muxed account, because only their shared, underlying G... account exists in the ledger. However, you can use them with:

We'll demonstrate some of these in the Examples.

Examples

In this section, we'll demonstrate how to create muxed accounts and how seamlessly they interface with their supported operations. To drive home the fact that custodial account workarounds based on transaction memos are superfluous now, we'll use that as a skeleton for our example structure.

After preparing some supporting code, we'll demonstrate three scenarios:

  • normal, "full" Stellar account payments (i.e. G to G),
  • mixed payments (i.e. M to G), and
  • fully muxed payments (i.e. M to M)

but use a shared function for all of them that does the real work, highlighting the ease of implementing muxed account support.

Warning: In the following code samples, proper error checking is omitted for brevity. However, you should always validate your results, as there are many ways that requests can fail. You can refer to the guide on Handling Errors Gracefully for tips on error management strategies.

Preamble

First, let's create two accounts and then a handful of virtual accounts representing "custodial customers" that the parent account manages:

const sdk = require("stellar-sdk");

const passphrase = "Test SDF Network ; September 2015";
const url = "https://horizon-testnet.stellar.org";
let server = new sdk.Server(url);

const custodian = sdk.Keypair.fromSecret("SAQLZCQA6AYUXK6JSKVPJ2MZ5K5IIABJOEQIG4RVBHX4PG2KMRKWXCHJ");
const outsider  = sdk.Keypair.fromSecret("SAAY2H7SANIS3JLFBFPLJRTYNLUYH4UTROIKRVFI4FEYV4LDW5Y7HDZ4");

async function preamble() {
  [ custodianAcc, outsiderAcc ] = await Promise.all([
    server.loadAccount(custodian.publicKey()),
    server.loadAccount(outsider.publicKey()),
  ]);

  customers = ["1", "22", "333", "4444"].map(
    (id) => new sdk.MuxedAccount(custodianAcc, id)
  );

  console.log("Custodian:\n       ", custodian.publicKey());
  console.log("Customers:")
  customers.forEach((customer) => {
    console.log(" " + customer.id().padStart(4, ' ') + ":",
                customer.accountId());
  });
  console.log();
}

We assume that these accounts exist on the testnet; you can replace them with your own keys and use the friendbot if you'd like.

When we run this function, we'll see the similarity in muxed account addresses among the customers, highlighting the fact that they share a public key:

Custodian:
       GCIHAQVWZH2AB5BB5NP63FBSIREG77LQZZNUVKD2LN2IOCLOT6N72MJN
Customers:
    1: MCIHAQVWZH2AB5BB5NP63FBSIREG77LQZZNUVKD2LN2IOCLOT6N72AAAAAAAAAAAAEDB4
   22: MCIHAQVWZH2AB5BB5NP63FBSIREG77LQZZNUVKD2LN2IOCLOT6N72AAAAAAAAAAAC3IHY
  333: MCIHAQVWZH2AB5BB5NP63FBSIREG77LQZZNUVKD2LN2IOCLOT6N72AAAAAAAAAABJV72I
 4444: MCIHAQVWZH2AB5BB5NP63FBSIREG77LQZZNUVKD2LN2IOCLOT6N72AAAAAAAAAARLQOKK

With the accounts out of the way, let's look at how we can manage the difference between "real" Stellar accounts (G...) and these "virtual" muxed accounts (M...).

Muxed Operations Model

The introduction of muxed addresses as a higher-level abstraction--and their experimental, opt-in nature--means there are mildly diverging branches of code depending on whether the source is a muxed account or not. We still need to, for example, load accounts by their underlying address, because the muxed versions don't actually live on the Stellar ledger:

function loadAccount(account) {
  if (account.accountId().startsWith("M")) {
    return loadAccount(account.baseAccount());
  } else {
    return server.loadAccount(account.accountId());
  }
}

function showBalance(acc) {
  console.log(`${acc.accountId().substring(0, 5)}: ${acc.balances[0].balance}`);
}

For payments--our focus for this set of examples--the divergence only matters because we want to show the balances for the custodian account.

Payments

The actual code to build payments is almost exactly the same as it would be without the muxed situation:

function doPayment(source, dest) {
  return loadAccount(source)
    .then((accountBeforePayment) => {
      showBalance(accountBeforePayment);

      let payment = sdk.Operation.payment({
        source: source.accountId(),
        destination: dest.accountId(),
        asset: sdk.Asset.native(),
        amount: "10",
        withMuxing: true,
      });

      let tx = new sdk.TransactionBuilder(accountBeforePayment, {
          networkPassphrase: StellarSdk.Networks.TESTNET,
          withMuxing: true,
          fee: 100,
        })
        .addOperation(payment)
        .setTimeout(30)
        .build();

      tx.sign(custodian);
      return server.submitTransaction(tx);
    })
    .then(() => loadAccount(source))
    .then(showBalance);
}

We can use this block to make a payment between normal Stellar accounts with ease: doPayment("GCIHA...", "GDS5N..."). The main divergence from the standard payments code--aside from the stubs to show XLM balances before and after--is the inclusion of the opt-in withMuxing flag.

Muxed to Unmuxed

The codeblock above covers all payment operations, abstracting away any need for differentiating between muxed (M...) and unmuxed (G...) addresses. From a high level, then, it's still trivial to make payments between one of our "customers" and someone outside of the "custodian"'s organization:

preamble
  .then(() => {
    const src = customers[0];
    console.log(`Sending 10 XLM from Customer ${src.id()} to ${outsiderAcc.accountId().substring(0, 5)}.`)
    return doPayment(src, outsiderAcc);
  });

Notice that we still sign the transaction with the custodian keys, because muxed accounts have no concept of secret keys. Ultimately, everything still goes through the "parent" account, and so we should see the account's balance decrease by 10 XLM accordingly:

Sending 10 XLM from Customer 1 to GDS5N.
GCIHA: 9519.9997700 XLM
GCIHA: 9509.9997600 XLM

Of course, there's also a fee charged for the transaction itself.

Muxed to Muxed

As we've mentioned, muxed account actions aren't represented in the Stellar ledger explicitly. When two muxed accounts sharing an underlying Stellar account communicate, it's as if the underlying account is talking to itself. A payment between two such accounts, then, is essentially a no-op.

preamble()
  .then(() => {
    const [ src, dst ] = customers.slice(0, 2);
    console.log(`Sending 10 XLM from Customer ${src.id()} to Customer ${dst.id()}.`)
    return doPayment(src, dst);
  });

The output should be something like the following:

Sending 10 XLM from Customer 1 to Customer 22.
GCIHA: 9579.9999800 XLM
GCIHA: 9579.9999700 XLM

Notice that the account's balance is essentially unchanged, yet it was charged a fee since this transaction is still recorded in the ledger despite doing next to nothing. You may want to detect these types of transactions in your application to avoid paying the transaction fee.

If we were to make a payment between two muxed accounts that had different underlying Stellar accounts, this would be equivalent to a payment between those two respective G... accounts.

More Examples

As is the case for most protocol-level features, you can find more usage examples and inspiration in the relevant test suite for your favorite SDK. For example, here are some of the JavaScript test cases.