Handle Custom Assets
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
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 creating custom assets.
Each trustline increases the user's base reserve 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.
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);
}
}
This is a big file that was covered in great detail in the Make XBN Payments tutorial, so we’ll just focus on the changes we need to make to support custom asset payments.
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.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!
Last modified 2yr ago