Issue an asset

There is no dedicated operation to create an asset on the Bantu Blockchain. Instead, assets are created with a payment operation: an issuing account makes a payment using the asset it’s issuing, and that payment actually creates the asset on the network.

It’s a pretty simple process that requires four operations: one to create an issuing account, one to create a distribution account, one to establish a trustline, and one to make a payment.

Note: you don’t actually have to issue assets to a dedicated distribution account: you can issue them to any account with the requisite trustline. However, using a distribution account is the recommended practice, and it makes the process a lot easier to explain. So that’s how we’ll do it in this guide.

The code to create those operations and submit them as transactions is below. Here, we’ll walk through each step so that the process makes sense. You can breeze through to get a general understanding, or you can use the Bantu Blockchain laboratory, which is an interface that allows you to create and submit transactions, to actually follow along and issue a token right here right now.

One caveat: if you are creating a token on the public network, there is an additional prerequisite. You need a funded account to provide the necessary blockchain tokens to create the issuing and distribution accounts.

Create the Issuing and Distribution Accounts

The issuing account is the origin of the asset, and will be forever linked to the asset’s identity. The distribution account is the first recipient of the asset. In the final step of this process, you’ll create the asset by sending a payment from the issuing account to the distribution account.

There are two steps to account creation:

  1. Generate a keypair
  2. Fund the account using a create_account operation

You can generate a keypair for free, but an account doesn’t exist on the Bantu Blockchain until it is funded with native tokens to cover the minimum balance.

If you’re issuing an asset on the testnet, you can fund your accounts by getting free test native tokens from Friendbot. If you’re issuing an asset in production, you will need to use an existing account to send enough live native tokens to cover the minimum balance, transaction fees, and, in the case of the distribution account, a trustline.

Why Have Separate Accounts for Issuing and Distribution?

Distributing assets through a distribution account is a design pattern. Functionally, you can do away with the distribution account and distribute directly from the issuer account. A less-known fact is that you can even create a market directly from the issuing account and issue by trading.

With that said, there are two main reasons to use a distribution account:

  1. Security
  2. Auditing

The account you use to distribute your asset from is going to be a hot account, meaning that some web service out there has direct access to sign its transactions.

If the account you use to distribute your asset is also the issuing account and is compromised by a malicious actor, that actor can now issue as much of your asset as they want. This is a hostile takeover of the asset and can increase your potential off-chain liabilities. If the malicous actor redeems these newly issued tokens with the anchor service, the anchor may not have the liquidity to support customers' withdrawals.

If the account you use to distribute your asset is not the issuing account, then the stakes are lower. Once discovered, the issuer account can effectively freeze the compromised account’s asset balance and start fresh with a new distribution account. This is possible without changing the issuing account.

The second reason is bookkeeping or auditability. The issuing account can’t actually hold a balance of its own asset. If you have standing inventory of your own asset in a separate account, it is easier to track. This is a common pattern in various ledgering solutions.

As an added bonus, distribution accounts decouple our ecosystem standards from issuance. This allows ecosystem participants to come up with interesting concepts like non-issuing anchors without actually changing protocols.

Create a Trustline

The Bantu Blockchain requires accounts to explicitly opt-in to holding an asset by creating a trustline, and in this step the distribution account submits a change_trust operation to do just that. The trustline goes from the distribution account to the issuing account.

The distribution account specifies the issuer’s public key and the asset code in the change_trust operation, and since it’s the first time the asset code appears on the ledger, that means the distribution account actually names the asset, not the issuing account.

Currently, there are two supported formats for asset codes.

  • Alphanumeric 4-character maximum: Any characters from the set [a-z][a-z][0-9] are allowed. The code can be shorter than 4 characters, but the trailing characters must all be empty.
  • Alphanumeric 12-character maximum: Any characters from the set [a-z][a-z][0-9] are allowed. The code can be any number of characters from 5 to 12, but the trailing characters must all be empty.

Any asset code works provided it falls into one of those two buckets. That said, if you’re issuing a currency, you should use the appropriate ISO 4217 code, and if you’re issuing a stock or bond, the appropriate ISIN number. Doing so makes it easier for Bantu Blockchain interfaces to properly display and sort your token in their listings, and allows potential token holders to understand, at a glance, what your token represents.

Make a Payment

This is the step where the magic happens. The issuing account makes a payment to the distribution account using the newly named asset, and tokens exist where before there were none. Presto!

As long as the issuing account remains unlocked, it can continue to create new tokens by making payments to the distribution account, or to any other account with the requisite trustline.

Once you’ve done that, you can also create a sell offer to get your asset onto the Bantu Blockchain decentralized exchange, and put some effort into market making to create liquidity for it.

Sample Code

var StellarSdk = require("stellar-sdk");
var server = new StellarSdk.Server("https://expansion-testnet.bantu.network");

// Keys for accounts to issue and receive the new asset
var issuingKeys = StellarSdk.Keypair.fromSecret(
  "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4",
);
var receivingKeys = StellarSdk.Keypair.fromSecret(
  "SDSAVCRE5JRAI7UFAVLE5IMIZRD6N6WOJUWKY4GFN34LOBEEUS4W2T2D",
);

// Create an object to represent the new asset
var astroDollar = new StellarSdk.Asset("AstroDollar", issuingKeys.publicKey());

// First, the receiving account must trust the asset
server
  .loadAccount(receivingKeys.publicKey())
  .then(function (receiver) {
    var transaction = new StellarSdk.TransactionBuilder(receiver, {
      fee: 100,
      networkPassphrase: "Bantu Testnet",
    })
      // The `changeTrust` operation creates (or alters) a trustline
      // The `limit` parameter below is optional
      .addOperation(
        StellarSdk.Operation.changeTrust({
          asset: astroDollar,
          limit: "1000",
        }),
      )
      // setTimeout is required for a transaction
      .setTimeout(100)
      .build();
    transaction.sign(receivingKeys);
    return server.submitTransaction(transaction);
  })
  .then(console.log)

  // Second, the issuing account actually sends a payment using the asset
  .then(function () {
    return server.loadAccount(issuingKeys.publicKey());
  })
  .then(function (issuer) {
    var transaction = new StellarSdk.TransactionBuilder(issuer, {
      fee: 100,
      networkPassphrase: "Bantu Testnet",
    })
      .addOperation(
        StellarSdk.Operation.payment({
          destination: receivingKeys.publicKey(),
          asset: astroDollar,
          amount: "10",
        }),
      )
      // setTimeout is required for a transaction
      .setTimeout(100)
      .build();
    transaction.sign(issuingKeys);
    return server.submitTransaction(transaction);
  })
  .then(console.log)
  .catch(function (error) {
    console.error("Error!", error);
  });
Server server = new Server("https://expansion-testnet.bantu.network");
Network network = new Network("Bantu Testnet");


// Keys for accounts to issue and receive the new asset
KeyPair issuingKeys = KeyPair
  .fromSecretSeed("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4");
KeyPair receivingKeys = KeyPair
  .fromSecretSeed("SDSAVCRE5JRAI7UFAVLE5IMIZRD6N6WOJUWKY4GFN34LOBEEUS4W2T2D");

// Create an object to represent the new asset
Asset astroDollar = Asset.createNonNativeAsset("AstroDollar", issuingKeys.getAccountId());

// First, the receiving account must trust the asset
AccountResponse receiving = server.accounts().account(receivingKeys.getAccountId());
Transaction allowAstroDollars = new Transaction.Builder(receiving, network)
  .addOperation(
    // The `ChangeTrust` operation creates (or alters) a trustline
    // The second parameter limits the amount the account can hold
    new ChangeTrustOperation.Builder(astroDollar, "1000").build())
  .build();
allowAstroDollars.sign(receivingKeys);
server.submitTransaction(allowAstroDollars);

// Second, the issuing account actually sends a payment using the asset
AccountResponse issuing = server.accounts().account(issuingKeys.getAccountId());
Transaction sendAstroDollars = new Transaction.Builder(issuing, network)
  .addOperation(
    new PaymentOperation.Builder(receivingKeys.getAccountId(), astroDollar, "10").build())
  .build();
sendAstroDollars.sign(issuingKeys);
server.submitTransaction(sendAstroDollars);
package main

import (
  "github.com/stellar/go/clients/horizonclient"
  "github.com/stellar/go/keypair"
  "github.com/stellar/go/network"
  "github.com/stellar/go/txnbuild"
  "log"
)

func main() {
  client := horizonclient.DefaultTestNetClient

  // Remember, these are just examples, so replace them with your own seeds.
  issuerSeed := "SDR4C2CKNCVK4DWMTNI2IXFJ6BE3A6J3WVNCGR6Q3SCMJDTSVHMJGC6U"
  distributorSeed := "SBUW3DVYLKLY5ZUJD5PL2ZHOFWJSVWGJA47F6FLO66UUFZLUUA2JVU5U"

  /*
   * We omit error checks here for brevity, but you should always check your
   * return values.
   */

  // Keys for accounts to issue and distribute the new asset.
  issuer, err := keypair.ParseFull(issuerSeed)
  distributor, err := keypair.ParseFull(distributorSeed)

  request := horizonclient.AccountRequest{AccountID: issuer.Address()}
  issuerAccount, err := client.AccountDetail(request)

  request = horizonclient.AccountRequest{AccountID: distributor.Address()}
  distributorAccount, err := client.AccountDetail(request)

  // Create an object to represent the new asset
  astroDollar := txnbuild.CreditAsset{Code: "AstroDollar", Issuer: issuer.Address()}

  // First, the receiving (distribution) account must trust the asset from the
  // issuer.
  tx, err := txnbuild.NewTransaction(
    txnbuild.TransactionParams{
      SourceAccount:        &distributorAccount,
      IncrementSequenceNum: true,
      BaseFee:              txnbuild.MinBaseFee,
      Timebounds:           txnbuild.NewInfiniteTimeout(),
      Operations: []txnbuild.Operation{
        &txnbuild.ChangeTrust{
          Line:  astroDollar,
          Limit: "5000",
        },
      },
    },
  )

  signedTx, err := tx.Sign("Bantu Testnet", distributor)
  resp, err := client.SubmitTransaction(signedTx)
  if err != nil {
    log.Fatal(err)
  } else {
    log.Printf("Trust: %s\n", resp.Hash)
  }

  // Second, the issuing account actually sends a payment using the asset
  tx, err = txnbuild.NewTransaction(
    txnbuild.TransactionParams{
      SourceAccount:        &issuerAccount,
      IncrementSequenceNum: true,
      BaseFee:              txnbuild.MinBaseFee,
      Timebounds:           txnbuild.NewInfiniteTimeout(),
      Operations: []txnbuild.Operation{
        &txnbuild.Payment{
          Destination: distributor.Address(),
          Asset:       astroDollar,
          Amount:      "10",
        },
      },
    },
  )

  signedTx, err = tx.Sign("Bantu Testnet", issuer)
  resp, err = client.SubmitTransaction(signedTx)

  if err != nil {
    log.Fatal(err)
  } else {
    log.Printf("Pay: %s\n", resp.Hash)
  }
}
from stellar_sdk.asset import Asset
from stellar_sdk.keypair import Keypair
from stellar_sdk.network import Network
from stellar_sdk.server import Server
from stellar_sdk.transaction_builder import TransactionBuilder

server = Server(horizon_url="https://expansion-testnet.bantu.network/")
# Use test network, if you need to use public network, please set it to `Network.PUBLIC_NETWORK_PASSPHRASE`
network_passphrase = "Bantu Testnet"

# Keys for accounts to issue and receive the new asset
issuing_keypair = Keypair.from_secret(
    "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4"
)
issuing_public = issuing_keypair.public_key

distributor_keypair = Keypair.from_secret(
    "SDSAVCRE5JRAI7UFAVLE5IMIZRD6N6WOJUWKY4GFN34LOBEEUS4W2T2D"
)
distributor_public = distributor_keypair.public_key

# Transactions require a valid sequence number that is specific to this account.
# We can fetch the current sequence number for the source account from Horizon.
distributor_account = server.load_account(distributor_public)

# Create an object to represent the new asset
astro_dollar = Asset("AstroDollar", issuing_public)

# First, the receiving account must trust the asset
trust_transaction = (
    TransactionBuilder(
        source_account=distributor_account,
        network_passphrase=network_passphrase,
        base_fee=100,
    )
    #  The `changeTrust` operation creates (or alters) a trustline
    #  The `limit` parameter below is optional
    .append_change_trust_op(
        asset_code=astro_dollar.code, asset_issuer=astro_dollar.issuer, limit="1000"
    )
    .set_timeout(100)
    .build()
)

trust_transaction.sign(distributor_keypair)
trust_transaction_resp = server.submit_transaction(trust_transaction)
print(f"Change Trust Transaction Resp:\n{trust_transaction_resp}")

issuing_account = server.load_account(issuing_public)
# Second, the issuing account actually sends a payment using the asset.
payment_transaction = (
    TransactionBuilder(
        source_account=issuing_account,
        network_passphrase=network_passphrase,
        base_fee=100,
    )
    .append_payment_op(
        destination=distributor_public,
        amount="10",
        asset_code=astro_dollar.code,
        asset_issuer=astro_dollar.issuer,
    )
    .build()
)
payment_transaction.sign(issuing_keypair)
payment_transaction_resp = server.submit_transaction(payment_transaction)
print(f"Payment Transaction Resp:\n{payment_transaction_resp}")

Naturally, the balances for the distributor’s account will now hold both native tokens and our new Astrodollars.