Skip to main content

Deposit Anchored Assets


This section of the tutorial takes the basic wallet with trustlines and anchor connections enabled and adds the ability to initiate in-app deposits with an Anchor. If you have all that ready, and the stellar.toml file is loaded into a @Prop on your component, you’re ready to begin the initial deposit of their asset into our wallet.

Create Deposit Method

Let’s create ./methods/depositAsset.ts.

import sjcl from "@tinyanvil/sjcl";import { Transaction, Keypair } 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 depositAsset(e: Event) {  try {    e.preventDefault();    let currency = await this.setPrompt(      "Select the currency you'd like to deposit",      null,      this.toml.CURRENCIES,    );    currency = currency.split(":");    const pincode = await this.setPrompt("Enter your keystore pincode");    if (!pincode) return;    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 }) => {        const transaction: any = new Transaction(          data.transaction,          data.network_passphrase,        );        this.error = null;        this.loading = { ...this.loading, deposit: true };        const keypair = Keypair.fromSecret(          sjcl.decrypt(pincode, this.account.keystore),        );        transaction.sign(keypair);        return transaction.toXDR();      })      .then((transaction) =>          `${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/deposit/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: "deposit",        },        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, deposit: false };      throw "Popups are blocked. You'll need to enable popups for this demo to work";    }    window.onmessage = ({ data: { transaction } }) => {      console.log(transaction.status, transaction);      if (transaction.status === "completed") {        this.updateAccount();        this.loading = { ...this.loading, deposit: false };      } 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, deposit: false };    this.error = handleError(err);  }}

It’s a lot of code, but keep in mind this is everything we need to completely interoperate with financial infrastructure foreign to the Stellar ecosystem, which is a big ask and in ~130 lines of code. It’s a modern day miracle!

Currency Code and Issuer Prompt

First things first install the missing axios package.

npm i -D axios

We’ll skip the rest of the top import stuff as you’re well aware of what that is, and we’ve installed and walked through anything noteworthy here already.

export default async function depositAsset(e: Event) {  try {    e.preventDefault()    let currency = await this.setPrompt('Select the currency you\'d like to deposit', 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)    )

Only thing here we haven’t seen before is the prompt selection of the currency code and issuer we’d like to deposit. To get this, we send the this.setPrompt a final options array argument, in this case the values from this.toml.CURRENCIES. This will open the prompt with a select<>options input field rather than a text input field.

That selection will come back as a string in the form of {code}:{issuer} so we split that by the : so we’ll have a nice tidy array of [{code}, {issuer}] to use later.

Check Trustline

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);

Once we’ve got the currency we’re dealing with we need to check if we have a trustline for that asset setup for our account. The anchor won’t be able to deposit their token into our account if we don’t have a trustline set for it first. So we get the balance from the this.account.state and then inspect the balances to see if that currency exists. If it doesn’t we trigger the this.trustAsset method with the arguments set to automatically add that trustline to our account.

Get Info

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

Once our account is setup, it’s time to start asking the Anchor some questions about itself and the features it supports. We get at that info by calling a GET on the TRANSFER_SERVER/info url from the anchor’s TOML file. We won’t make use of any of this data right now, but this is where you'd find fee and feature information which to display in you UI for your wallet user to review.

Create Authenticated User Session

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) =>      `${this.toml.WEB_AUTH_ENDPOINT}`,      { transaction },      { headers: { "Content-Type": "application/json" } },    ),  )  .then(({ data: { token } }) => token);console.log(auth);

This call is actually a part of SEP-0010, which allows us to verify ownership of a user's public key so the Anchor knows the deposit request is coming from a valid entity. You don’t want someone else issuing deposit requests on your account, so to prove you control the Stellar account request the deposit, you sign a dummy authentication transaction and send it to the Anchor's web auth server. The server responds with a JWT token, which will be used to verify all further API calls to the anchor. For more info on how this works, check out the user authetication from an Anchor's point of view.

The endpoint for creating an authenticated user session is specified the WEB_AUTH_ENDPOINT on an Anchor's stellar.toml file. The first thing our wallet does is make a GET request with a public account param which will send back an unsigned Stellar transaction for that account that has in invalide sequence number, so it doesn't actually do anything when submitted to the network. In response you’ll sign that transaction on behalf of the user and POST it back. If the signature checks out, the success response will contain an Authorization Bearer header JWT which you’ll want to store and use for all future interactions with the Anchor.

Initiate Deposit

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);

This call is our first little bit of magic. Once we setup our multipart/form-data entry values we can POST them to the ${this.toml.TRANSFER_SERVER}/transactions/withdraw/interactive endpoint along with our auth Authorization Bearer auth token we acquired earlier. The response to this call will contain a special url that allows a user to interact directly with the Anchor to provide the data required for a deposit.

{  "type": "interactive_customer_info_needed",  "url": "",  "id": "3dcf204e-e3e8-4831-a840-8e36c0695a07"}

In a moment we’ll open that url in an iframe, popup, or new tab. Once we’ve sent that request, there will be a pending transaction request we can inspect at the ${this.toml.TRANSFER_SERVER}/transactions route.

Check Transaction Status

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

This request should return the deposit status for the request we just made.

{  "transactions": [    {      "id": "3dcf204e-e3e8-4831-a840-8e36c0695a07",      "kind": "deposit",      "status": "incomplete",      "status_eta": 3600,      "amount_in": null,      "amount_out": null,      "amount_fee": null,      "started_at": "2020-02-11T20:17:04.984590Z",      "completed_at": null,      "stellar_transaction_id": null,      "external_transaction_id": null,      "external_extra": null,      "external_extra_text": null,      "deposit_memo": null,      "deposit_memo_type": "text",      "more_info_url": "",      "to": "GB34AU66TSGT5FS3EULJOTR32MWKAOMFZK6MPWM7V4SIBNLN5P5TFRJG",      "from": null    }  ]}

Once we’re certain we’ve got a "status": "incomplete", which is the correct response since we haven't served the user the URL that allows them to initiate a deposit, we know everything's working, so we can go back to the interactive.url route and open that to begin filling out the deposit requirements.

Serve the Anchor Webapp

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, deposit: false };  throw "Popups are blocked. You'll need to enable popups for this demo to work";}

We’ll opt for a popup. When you first open that url, it’s likely the anchor will ask for some level of KYC data from the user: name, email, address, verification docs like bank statements, passport, or license, etc. The SDF demo server we're using — which is hooked up to the testnet — will ask for name and email.

After passing that screen your user enters the amount of an asset they'd like to deposit on the next screen. If, for instance, a user enters $500, once that deposit clears — and on this demo it clears automatically — the Anchor will credit the user's wallet with 500 of the token less any fees. In our case, the user ends up with 498.95: 1.05 is taken out in fees for a $500 deposit.

If popups are disabled, we should kill the loader and show that error in the UI.

    window.onmessage = ({data: {transaction}}) => {      console.log(transaction.status, transaction)      if (transaction.status === 'completed') {        this.updateAccount()        this.loading = {...this.loading, deposit: false}      }      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, deposit: false}    this.error = handleError(err)  }}

Check Deposit Status

SEP-0024 allows specifies two methods for further communication between the anchor window and the wallet: a POST JSON for when you have a server, message and postMessage for when you have a client-side service. We don’t have a server, so we opt for the urlBuilder.searchParams.set('callback', 'postMessage'). With that set, we can setup a window 'message' event listener which will fire whenever the anchor popup window sends a postMessage to our popup window. From there we just listen to the popup message until transaction.status equals 'completed'. At that point our deposit has succeeded, and we can this.updateAccount() and kill the loader. If the transaction is still in a pending status we’ll wait 1 second before reloading the anchor popup window and checking again.

If at any point during this flow there’s an error we should catch that, kill the loader, and display the error to the user.

Whew! *wipes sweat from brow then hands on sleeves Nice work! “Just like that” we’ve deposited an anchored asset into our wallet! We can now make payments with that asset to anyone else who has a trustline enabled for this asset.