LogoLogo
  • Welcome
  • Where to Start
    • Introduction
    • list of Operations
    • Bantu Stack
  • Tutorials
    • Create Account
    • Send and Receive Payments
    • Follow Received Payments
    • Securing Web-based Projects
  • Issue Assets
    • Overview
    • Anatomy of an Asset
    • Issue an Asset
    • Publish Information About an Asset
    • Control Access to an Asset
  • Building Apps
    • Overview
    • Project Setup
    • Key Management Basics
    • Create a Basic Wallet
    • Make XBN Payments
    • Handle Custom Assets
  • Run a Core Node
    • index
    • running-node
    • network-upgrades
    • tier-1-orgs
    • installation
    • prerequisites
    • configuring
    • publishing history archives
    • commands
    • monitoring
  • Run API Server
    • prerequisites
    • quickstart
    • index
    • installing
    • monitoring
    • Running
    • configuring
  • Software and SDKs
    • index
  • Glossary
    • scp
    • Claimable Balance
    • XDR
    • Assets
    • BUDS
    • Inflation
    • Miscellaneous Core Objects
    • Testnet
    • Accounts
    • Network Passphrase
    • Ledger
    • Versioning
    • Sponsored Reserves
    • Operations
    • Decentralized Exchange
    • Fees
    • XBN Supply
    • Fee Bumps
    • Channels
    • Transactions
    • Minimum Balance
    • Multisig
  • Docs
    • Index
  • API
    • Introduction
      • Index
      • Response Format
      • Streaming
      • Rate Limiting
      • XDR
      • Pagination
        • Index
        • Page Arguments
    • Resources
      • Untitled
      • Overview
      • Ledgers
        • index
        • Object
        • Single
        • Transactions
        • Operations
        • Payments
        • Effects
        • List
      • Transactions
        • Index
        • Object
        • Single
        • Operations
        • Effects
        • List
      • Operations
        • Index
        • Object
          • Index
          • Create Account
          • Payment
          • Path Payment Strict Send
          • Path Payment Strict Receive
          • Sell Offer
          • Buy Offer
          • Passive Sell Offer
          • Set Options
          • Change Trust
          • Allow Trust
          • Account Merge
          • Manage Data
          • Bump Sequence
          • Create Claimable Balance
          • Claim Claimable Balance
          • Begin Sponsoring Future Reserves
          • End Sponsoring Future Reserves
          • Revoke Sponsorship
        • Single
        • Effects
        • List
        • List Payments
      • Effects
        • Index
        • Types
        • List
      • Accounts
        • Index
        • Object
        • Transactions
        • List
        • Single
        • Operations
        • Payments
        • Effects
        • Offers
        • Trades
        • Data
      • Offers
        • Index
        • Object
        • Single
        • List
      • Trades
        • Index
        • Object
        • List
      • Assets
        • Index
        • Object
        • List
      • Claimable Balances
        • Index
        • Object
        • Single
        • List
    • Aggregations
      • Index
      • Order Books
        • Index
        • object
        • Single
      • Paths
        • Index
        • Object
        • Strict Receive
        • Strict Send
      • Trade Aggregations
        • Index
        • Object
        • List
      • Fee Stats
        • Index
        • Object
        • Single
    • Errors
      • Index
      • Response
      • HTTP Status Codes
        • Index
        • Standard
        • Expansion Specific
          • Index
          • Transaction Failed
          • Transaction Malformed
          • Before History
          • Stale History
          • Timeout
      • Result Codes
        • Index
        • Transactions
        • Operations
        • Operation Specific
          • Index
          • Create Account
          • Payment
          • Path Payment Strict Receive
          • Path Payment Strict Send
          • Manage Sell Offer
          • Manage Buy Offer
          • Create Passive Sell Offer
          • Set Options
          • Change Trust
          • Allow Trust
          • Account Merge
          • Manage Data
          • Bump Sequence
Powered by GitBook
On this page
  • What's a Custom Asset?
  • Add Trustlines Button
  • Add Trustlines

Was this helpful?

  1. Building Apps

Handle Custom Assets

PreviousMake XBN PaymentsNextRun a Core Node

Last updated 4 years ago

Was this helpful?

In this section of the tutorial, we'll add the ability to hold and transfer custom assets to the basic wallet we built in previous sections. It assumes that you've already completed Create a Basic Wallet and Make XBN Payments

What's a Custom Asset?

Bantu allows anyone to easily issue an asset, and all assets can be held, transferred, and traded just like XBN, the network token. Every asset other than XBN exists on the network in the form of trustlines: an asset holder explicitly agrees to allow a balance of a specific token issued by a specific issuing account by creating a persistent ledger entry tied to the holding account. You can find out more in the guide to .

Each trustline increases the user's by 0.5 XBN, and in this tutorial, we'll go over how to set up your wallet to create trustlines and manage the base reserve on behalf of a user.

Add Trustlines Button

To enable custom asset handling, we need to modify three files and create one new one. Let’s start with our modifications. First up the ./events/render.tsx file. We need to add a button for creating these new trustlines!

import { h } from "@stencil/core";
import { has as loHas } from "lodash-es";

export default function render() {
  return [
    <stellar-prompt prompter={this.prompter} />,

    this.account ? (
      [
        <div class="account-key">
          <p>{this.account.publicKey}</p>
          <button
            class="small"
            type="button"
            onClick={(e) => this.copyAddress(e)}
          >
            Copy Address
          </button>
          <button
            class="small"
            type="button"
            onClick={(e) => this.copySecret(e)}
          >
            Copy Secret
          </button>
        </div>,

        <button
          class={this.loading.trust ? "loading" : null}
          type="button"
          onClick={(e) => this.trustAsset(e)}
        >
          {this.loading.trust ? <bantu-loader /> : null} Trust Asset
        </button>,
        <button
          class={this.loading.pay ? "loading" : null}
          type="button"
          onClick={(e) => this.makePayment(e)}
        >
          {this.loading.pay ? <bantu-loader /> : null} Make Payment
        </button>,
      ]
    ) : (
      <button
        class={this.loading.fund ? "loading" : null}
        type="button"
        onClick={(e) => this.createAccount(e)}
      >
        {this.loading.fund ? <bantu-loader /> : null} Create Account
      </button>
    ),

    this.error ? (
      <pre class="error">{JSON.stringify(this.error, null, 2)}</pre>
    ) : null,

    loHas(this.account, "state") ? (
      <pre class="account-state">
        {JSON.stringify(this.account.state, null, 2)}
      </pre>
    ) : null,

    this.account
      ? [
          <button
            class={this.loading.update ? "loading" : null}
            type="button"
            onClick={(e) => this.updateAccount(e)}
          >
            {this.loading.update ? <bantu-loader /> : null} Update Account
          </button>,
          <button type="button" onClick={(e) => this.signOut(e)}>
            Sign Out
          </button>,
        ]
      : null,
  ];
}

If you look closely you’ll spot the Trust Asset button right below our account-key div. Nothing funky here, just a button that triggers this.trustAsset method which we’ll add in a moment.

Next up, let’s update the ./methods/makePayment.ts file.

import sjcl from "@tinyanvil/sjcl";
import {
  Keypair,
  Account,
  TransactionBuilder,
  BASE_FEE,
  Networks,
  Operation,
  Asset,
} from "stellar-sdk";
import { has as loHas } from "lodash-es";

import { handleError } from "@services/error";

export default async function makePayment(e: Event) {
  try {
    e.preventDefault();

    let instructions = await this.setPrompt("{Amount} {Asset} {Destination}");
    instructions = instructions.split(" ");

    if (!/xlm/gi.test(instructions[1]))
      instructions[3] = await this.setPrompt(
        `Who issues the ${instructions[1]} asset?`,
        "Enter ME to refer to yourself",
      );

    const pincode = await this.setPrompt("Enter your keystore pincode");

    if (!instructions || !pincode) return;

    const keypair = Keypair.fromSecret(
      sjcl.decrypt(pincode, this.account.keystore),
    );

    if (/me/gi.test(instructions[3])) instructions[3] = keypair.publicKey();

    this.error = null;
    this.loading = { ...this.loading, pay: true };

    await this.server
      .accounts()
      .accountId(keypair.publicKey())
      .call()
      .then(({ sequence }) => {
        const account = new Account(keypair.publicKey(), sequence);
        const transaction = new TransactionBuilder(account, {
          fee: BASE_FEE,
          networkPassphrase: Networks.TESTNET,
        })
          .addOperation(
            Operation.payment({
              destination: instructions[2],
              asset: instructions[3]
                ? new Asset(instructions[1], instructions[3])
                : Asset.native(),
              amount: instructions[0],
            }),
          )
          .setTimeout(0)
          .build();

        transaction.sign(keypair);
        return this.server.submitTransaction(transaction).catch((err) => {
          if (
            // Paying an account which doesn't exist, create it instead
            loHas(err, "response.data.extras.result_codes.operations") &&
            err.response.data.status === 400 &&
            err.response.data.extras.result_codes.operations.indexOf(
              "op_no_destination",
            ) !== -1 &&
            !instructions[3]
          ) {
            const transaction = new TransactionBuilder(account, {
              fee: BASE_FEE,
              networkPassphrase: Networks.TESTNET,
            })
              .addOperation(
                Operation.createAccount({
                  destination: instructions[2],
                  startingBalance: instructions[0],
                }),
              )
              .setTimeout(0)
              .build();

            transaction.sign(keypair);
            return this.server.submitTransaction(transaction);
          } else throw err;
        });
      })
      .then((res) => console.log(res))
      .finally(() => {
        this.loading = { ...this.loading, pay: false };
        this.updateAccount();
      });
  } catch (err) {
    this.error = handleError(err);
  }
}
let instructions = await this.setPrompt("{Amount} {Asset} {Destination}");
instructions = instructions.split(" ");

if (!/xlm/gi.test(instructions[1]))
  instructions[3] = await this.setPrompt(
    `Who issues the ${instructions[1]} asset?`,
    "Enter ME to refer to yourself",
  );

This change allows us to indicate a specific asset code we’d like use to make a payment and triggers an additional prompt to set the issuer for that asset if it’s not the native XBN.

if (/me/gi.test(instructions[3])) instructions[3] = keypair.publicKey();

This is just a nifty little helper shortcut to allow us to use the ME “issuer” to swap with our actual account publicKey. Niceties make the world go ‘round.

asset: instructions[3] ? new Asset(instructions[1], instructions[3]) : Asset.native(),

The final noteworthy change is a ternary operation that switches our payment asset between the native XBN and a custom asset based off of responses to our prompt. Essentially, if instructions[3] exists — meaning there is an issuer — use that issuer and custom token as the asset for the payment. Otherwise, just use the native Asset.

The final changes are in the wallet.ts itself and tie together all the other updates as well as pull in the new trustAsset method.

import { Component, State, Prop } from "@stencil/core";
import { Server, ServerApi } from "stellar-sdk";

import componentWillLoad from "./events/componentWillLoad";
import render from "./events/render";

import createAccount from "./methods/createAccount";
import updateAccount from "./methods/updateAccount";
import trustAsset from "./methods/trustAsset"; // NEW
import makePayment from "./methods/makePayment"; // UPDATE
import copyAddress from "./methods/copyAddress";
import copySecret from "./methods/copySecret";
import signOut from "./methods/signOut";
import setPrompt from "./methods/setPrompt";

import { Prompter } from "@prompt/prompt";

interface StellarAccount {
  publicKey: string;
  keystore: string;
  state?: ServerApi.AccountRecord;
}

interface Loading {
  // UPDATE
  fund?: boolean;
  pay?: boolean;
  trust?: boolean; // NEW
  update?: boolean;
}

@Component({
  tag: "stellar-wallet",
  styleUrl: "wallet.scss",
  shadow: true,
})
export class Wallet {
  @State() account: StellarAccount;
  @State() prompter: Prompter = { show: false };
  @State() loading: Loading = {};
  @State() error: any = null;

  @Prop() server: Server;

  // Component events
  componentWillLoad() {}
  render() {}

  // Batun methods
  createAccount = createAccount;
  updateAccount = updateAccount;
  trustAsset = trustAsset; // NEW
  makePayment = makePayment; // UPDATE
  copyAddress = copyAddress;
  copySecret = copySecret;
  signOut = signOut;

  // Misc methods
  setPrompt = setPrompt;
}

Wallet.prototype.componentWillLoad = componentWillLoad;
Wallet.prototype.render = render;

Only thing worth seeing here besides the inclusion of the new trustAsset method is the addition of the trust?: boolean, in the Loading class.

Add Trustlines

Alright so, finally we get to the ./methods/trustAsset.ts file!

import sjcl from "@tinyanvil/sjcl";
import {
  Keypair,
  Account,
  TransactionBuilder,
  BASE_FEE,
  Networks,
  Operation,
  Asset,
} from "stellar-sdk";

import { handleError } from "@services/error";

export default async function trustAsset(
  e?: Event,
  asset?: string,
  issuer?: string,
  pincode?: string,
) {
  try {
    if (e) e.preventDefault();

    let instructions;

    if (asset && issuer) instructions = [asset, issuer];
    else {
      instructions = await this.setPrompt("{Asset} {Issuer}");
      instructions = instructions.split(" ");
    }

    pincode = pincode || (await this.setPrompt("Enter your keystore pincode"));

    if (!instructions || !pincode) return;

    const keypair = Keypair.fromSecret(
      sjcl.decrypt(pincode, this.account.keystore),
    );

    this.error = null;
    this.loading = { ...this.loading, trust: true };

    await this.server
      .accounts()
      .accountId(keypair.publicKey())
      .call()
      .then(({ sequence }) => {
        const account = new Account(keypair.publicKey(), sequence);
        const transaction = new TransactionBuilder(account, {
          fee: BASE_FEE,
          networkPassphrase: Networks.TESTNET,
        })
          .addOperation(
            Operation.changeTrust({
              asset: new Asset(instructions[0], instructions[1]),
            }),
          )
          .setTimeout(0)
          .build();

        transaction.sign(keypair);
        return this.server.submitTransaction(transaction);
      })
      .then((res) => console.log(res))
      .finally(() => {
        this.loading = { ...this.loading, trust: false };
        this.updateAccount();
      });
  } catch (err) {
    this.error = handleError(err);
  }
}

This is similar to the makePayment method but there are a couple tiny tweaks worth noting:

export default async function trustAsset(
  e?: Event,
  asset?: string,
  issuer?: string,
  pincode?: string
) {
  try {
    if (e)
      e.preventDefault()

    let instructions

    if (
      asset
      && issuer
    ) instructions = [asset, issuer]

    else {
      instructions = await this.setPrompt('{Asset} {Issuer}')
      instructions = instructions.split(' ')
    }

    pincode = pincode || await this.setPrompt('Enter your keystore pincode')

We’re allowing the inclusion of several arguments in this function, namely asset, issuer, and pincode. We won’t be making use of them here, but transparently creating trustlines from within other functions will prove useful later.

If we have any of those variables set, we can “preload” our interface a bit, and even bypass user input altogether if a pincode is provided. Again, not something we’ll make use of quite yet, but once we look into depositing and withdrawing assets from an Anchor or accepting incoming payments for which we don’t yet have a trustline this functionality will prove useful.

So there we have it! The ability to accept and pay with custom assets on Bantu!

This is a big file that was covered in great detail in the tutorial, so we’ll just focus on the changes we need to make to support custom asset payments.

creating custom assets
base reserve
Make XBN Payments