Skip to main content

Withdraw Anchored Assets

info

At this point, you should have a basic wallet that can handle custom assets, and you should have enabled deposits and used the SDF-maintained testnet Anchor Reference Implementation to get some test tokens into your wallet. After you’ve had some fun with your new asset, it's time to learn to perform this process in reverse, and that's what we'll do here: set your wallet up to handle withdrawals.

In a live situation, this is the step when a user takes a Stellar-based token and redeems it with an Anchor for the underlying asset it represents. It's how they'd move money off the network and back into their bank account, for instance.

Create Withdraw Method

To start, create ./methods/withdrawAsset.ts and pop this in.

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

import axios from "axios";
import {
get as loGet,
each as loEach,
findIndex as loFindIndex,
} from "lodash-es";

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

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

let currency = await this.setPrompt(
"Select the currency you'd like to withdraw",
null,
this.toml.CURRENCIES,
);
currency = currency.split(":");

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

if (!pincode) return;

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

const balances = loGet(this.account, "state.balances");
const hasCurrency = loFindIndex(balances, {
asset_code: currency[0],
asset_issuer: currency[1],
});

if (hasCurrency === -1)
await this.trustAsset(null, currency[0], currency[1], pincode);

const info = await axios
.get(`${this.toml.TRANSFER_SERVER}/info`)
.then(({ data }) => data);

console.log(info);

const auth = await axios
.get(`${this.toml.WEB_AUTH_ENDPOINT}`, {
params: {
account: this.account.publicKey,
},
})
.then(async ({ data: { transaction, network_passphrase } }) => {
const txn: any = new Transaction(transaction, network_passphrase);

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

txn.sign(keypair);
return txn.toXDR();
})
.then((transaction) =>
axios.post(
`${this.toml.WEB_AUTH_ENDPOINT}`,
{ transaction },
{ headers: { "Content-Type": "application/json" } },
),
)
.then(({ data: { token } }) => token);

console.log(auth);

const formData = new FormData();

loEach(
{
asset_code: currency[0],
account: this.account.publicKey,
lang: "en",
},
(value, key) => formData.append(key, value),
);

const interactive = await axios
.post(
`${this.toml.TRANSFER_SERVER}/transactions/withdraw/interactive`,
formData,
{
headers: {
Authorization: `Bearer ${auth}`,
"Content-Type": "multipart/form-data",
},
},
)
.then(({ data }) => data);

console.log(interactive);

const transactions = await axios
.get(`${this.toml.TRANSFER_SERVER}/transactions`, {
params: {
asset_code: currency[0],
limit: 1,
kind: "withdrawal",
},
headers: {
Authorization: `Bearer ${auth}`,
},
})
.then(({ data: { transactions } }) => transactions);

console.log(transactions);

const urlBuilder = new URL(interactive.url);
urlBuilder.searchParams.set("callback", "postMessage");
const popup = open(urlBuilder.toString(), "popup", "width=500,height=800");

if (!popup) {
this.loading = { ...this.loading, withdraw: false };
throw "Popups are blocked. You'll need to enable popups for this demo to work";
}

await new Promise((resolve, reject) => {
let submittedTxn;

window.onmessage = ({ data: { transaction } }) => {
console.log(transaction.status, transaction);

if (transaction.status === "completed") {
this.updateAccount();
this.loading = { ...this.loading, withdraw: false };
resolve();
} else if (
!submittedTxn &&
transaction.status === "pending_user_transfer_start"
) {
this.server
.accounts()
.accountId(keypair.publicKey())
.call()
.then(({ sequence }) => {
const account = new Account(keypair.publicKey(), sequence);
const txn = new TransactionBuilder(account, {
fee: BASE_FEE,
networkPassphrase: Networks.TESTNET,
})
.addOperation(
Operation.payment({
destination: transaction.withdraw_anchor_account,
asset: new Asset(currency[0], currency[1]),
amount: transaction.amount_in,
}),
)
.addMemo(new Memo(MemoHash, transaction.withdraw_memo))
.setTimeout(0)
.build();

txn.sign(keypair);
return this.server.submitTransaction(txn);
})
.then((res) => {
console.log(res);
submittedTxn = res;

const urlBuilder = new URL(transaction.more_info_url);
urlBuilder.searchParams.set("callback", "postMessage");

popup.location.replace(urlBuilder.toString());
})
.catch((err) => reject(err));
} else {
setTimeout(() => {
const urlBuilder = new URL(transaction.more_info_url);
urlBuilder.searchParams.set("callback", "postMessage");

popup.location.replace(urlBuilder.toString());
}, 1000);
}
};
});
} catch (err) {
this.loading = { ...this.loading, withdraw: false };
this.error = handleError(err);
}
}

We’ll actually skip everything except the stuff that is significantly different than the deposit flow. The main difference between the two is who submits the Stellar transaction: in the deposit flow the anchor is sends the wallet a Stellar asset; in the withdraw flow the wallet sends the asset to the anchor. So for the withdraw flow, we’ll need to build and submit a payment transaction.

Create and Submit Payment Transaction

  await new Promise((resolve, reject) => {
let submittedTxn

window.onmessage = ({data: {transaction}}) => {
console.log(transaction.status, transaction)

if (transaction.status === 'completed') {
this.updateAccount()
this.loading = {...this.loading, withdraw: false}
resolve()
}

First thing we’ll see is the use of a Promise. This allows us to respond to any errors in the transaction we’re about to build and submit. Inside the promise we have three if statement blocks. The first if statement is a response to a status of success, which is where we'll end up once the withdraw has registered and everything is hunky dory.

  else if (
!submittedTxn
&& transaction.status === 'pending_user_transfer_start'
) {
this.server
.accounts()
.accountId(keypair.publicKey())
.call()
.then(({sequence}) => {
const account = new Account(keypair.publicKey(), sequence)
const txn = new TransactionBuilder(account, {
fee: BASE_FEE,
networkPassphrase: Networks.TESTNET
})
.addOperation(Operation.payment({
destination: transaction.withdraw_anchor_account,
asset: new Asset(currency[0], currency[1]),
amount: transaction.amount_in
}))
.addMemo(new Memo(MemoHash, transaction.withdraw_memo))
.setTimeout(0)
.build()

txn.sign(keypair)
return this.server.submitTransaction(txn)
})
.then((res) => {
console.log(res)
submittedTxn = res

const urlBuilder = new URL(transaction.more_info_url)
urlBuilder.searchParams.set('callback', 'postMessage')

popup.location.replace(urlBuilder.toString())
})
.catch((err) => reject(err))
}

Otherwise if the transaction status is pending_user_transfer_start and we haven’t yet submitted a transaction to the Anchor, we attempt that. We load up the user's account to get the next valid sequence number, and build a Stellar transaction consisting of a payment operation with all the details from the transaction object that the anchor is expecting. Once we have a valid transaction built, we’ll sign it with the keypair, submit the transaction to the network, and wait for a response. If the transaction is successful, we save that value to submittedTxn and reload the anchor popup to observe the pending status. Make sure to set the submittedTxn to a truthy value or else you run the risk of submitting the transaction multiple times, as the anchor may take a moment to realize you’ve successfully submitted a transaction to them.

        else {
setTimeout(() => {
const urlBuilder = new URL(transaction.more_info_url)
urlBuilder.searchParams.set('callback', 'postMessage')

popup.location.replace(urlBuilder.toString())
}, 1000)
}
}
})
}

catch (err) {
this.loading = {...this.loading, withdraw: false}
this.error = handleError(err)
}
}

The last if block is that if all else fails just keep reloading the anchor popup every second until we get a 'completed' status.

Finally catch and respond to any errors.

With this method saved and the server reloaded we have a fully functional SEP-0024 compliant wallet! Noice!

View this code on GitHub