This guide describes how to add tokens from the Stellar network to your exchange. First, we walk through adding Stellar’s native asset, lumens. Following that, we describe how to add other tokens. This example uses Node.js and the JS Stellar SDK, but it should be easy to adapt to other languages.
There are many ways to architect an exchange. This guide uses the following design:
issuing account
: One Stellar account that holds the majority of customer deposits offline.base account
: One Stellar account that holds a small amount of customer deposits online and is used to payout to withdrawal requests.customerID
: Each user has a customerID, used to correlate incoming deposits with a particular user’s account on the exchange.The two main integration points to Stellar for an exchange are:
It’s recommended, though not strictly necessary, to run your own instances of Stellar Core and Horizon - this doc lists more benefits. If you choose not to, it’s possible to use the Stellar.org public-facing Horizon servers. Our test and live networks are listed below:
test net: {hostname:'horizon-testnet.stellar.org', secure:true, port:443};
live: {hostname:'horizon.stellar.org', secure:true, port:443};
An issuing account is typically used to keep the bulk of customer funds secure. An issuing account is a Stellar account whose secret keys are not on any device that touches the Internet. Transactions are manually initiated by a human and signed locally on the offline machine—a local install of js-stellar-sdk
creates a tx_blob
containing the signed transaction. This tx_blob
can be transported to a machine connected to the Internet via offline methods (e.g., USB or by hand). This design makes the issuing account secret key much harder to compromise.
A base account contains a more limited amount of funds than an issuing account. A base account is a Stellar account used on a machine that is connected to the Internet. It handles the day-to-day sending and receiving of lumens. The limited amount of funds in a base account restricts loss in the event of a security breach.
StellarTransactions
.StellarCursor
.customerID
for each user.CREATE TABLE StellarTransactions (UserID INT, Destination varchar(56), XLMAmount INT, state varchar(8));
CREATE TABLE StellarCursor (id INT, cursor varchar(50)); // id - AUTO_INCREMENT field
Possible values for StellarTransactions.state
are “pending”, “done”, “error”.
Use this code framework to integrate Stellar into your exchange. For this guide, we use placeholder functions for reading/writing to the exchange database. Each database library connects differently, so we abstract away those details. The following sections describe each step:
// Config your server
var config = {};
config.baseAccount = "your base account address";
config.baseAccountSecret = "your base account secret key";
// You can use Stellar.org's instance of Horizon or your own
config.horizon = 'https://horizon-testnet.stellar.org';
// Include the JS Stellar SDK
// It provides a client-side interface to Horizon
var StellarSdk = require('stellar-sdk');
// uncomment for live network:
// StellarSdk.Network.usePublicNetwork();
// Initialize the Stellar SDK with the Horizon instance
// You want to connect to
var server = new StellarSdk.Server(config.horizon);
// Get the latest cursor position
var lastToken = latestFromDB("StellarCursor");
// Listen for payments from where you last stopped
// GET https://horizon-testnet.stellar.org/accounts/{config.baseAccount}/payments?cursor={last_token}
let callBuilder = server.payments().forAccount(config.baseAccount);
// If no cursor has been saved yet, don't add cursor parameter
if (lastToken) {
callBuilder.cursor(lastToken);
}
callBuilder.stream({onmessage: handlePaymentResponse});
// Load the account sequence number from Horizon and return the account
// GET https://horizon-testnet.stellar.org/accounts/{config.baseAccount}
server.loadAccount(config.baseAccount)
.then(function (account) {
submitPendingTransactions(account);
})
When a user wants to deposit lumens in your exchange, instruct them to send XLM to your base account address with the customerID in the memo field of the transaction.
You must listen for payments to the base account and credit any user that sends XLM there. Here’s code that listens for these payments:
// Start listening for payments from where you last stopped
var lastToken = latestFromDB("StellarCursor");
// GET https://horizon-testnet.stellar.org/accounts/{config.baseAccount}/payments?cursor={last_token}
let callBuilder = server.payments().forAccount(config.baseAccount);
// If no cursor has been saved yet, don't add cursor parameter
if (lastToken) {
callBuilder.cursor(lastToken);
}
callBuilder.stream({onmessage: handlePaymentResponse});
For every payment received by the base account, you must:
StellarCursor
table so you can resume payment processing where you left off.So, you pass this function as the onmessage
option when you stream payments:
function handlePaymentResponse(record) {
// GET https://horizon-testnet.stellar.org/transaction/{id of transaction this payment is part of}
record.transaction()
.then(function(txn) {
var customer = txn.memo;
// If this isn't a payment to the baseAccount, skip
if (record.to != config.baseAccount) {
return;
}
if (record.asset_type != 'native') {
// If you are a XLM exchange and the customer sends
// you a non-native asset, some options for handling it are
// 1. Trade the asset to native and credit that amount
// 2. Send it back to the customer
} else {
// Credit the customer in the memo field
if (checkExists(customer, "ExchangeUsers")) {
// Update in an atomic transaction
db.transaction(function() {
// Store the amount the customer has paid you in your database
store([record.amount, customer], "StellarDeposits");
// Store the cursor in your database
store(record.paging_token, "StellarCursor");
});
} else {
// If customer cannot be found, you can raise an error,
// add them to your customers list and credit them,
// or do anything else appropriate to your needs
console.log(customer);
}
}
})
.catch(function(err) {
// Process error
});
}
When a user requests a lumen withdrawal from your exchange, you must generate a Stellar transaction to send them XLM. See building transactions for more information.
The function handleRequestWithdrawal
will queue up a transaction in the exchange’s StellarTransactions
table whenever a withdrawal is requested.
function handleRequestWithdrawal(userID,amountLumens,destinationAddress) {
// Update in an atomic transaction
db.transaction(function() {
// Read the user's balance from the exchange's database
var userBalance = getBalance('userID');
// Check that user has the required lumens
if (amountLumens <= userBalance) {
// Debit the user's internal lumen balance by the amount of lumens they are withdrawing
store([userID, userBalance - amountLumens], "UserBalances");
// Save the transaction information in the StellarTransactions table
store([userID, destinationAddress, amountLumens, "pending"], "StellarTransactions");
} else {
// If the user doesn't have enough XLM, you can alert them
}
});
}
Then, you should run submitPendingTransactions
, which will check StellarTransactions
for pending transactions and submit them.
StellarSdk.Network.useTestNetwork();
// This is the function that handles submitting a single transaction
function submitTransaction(exchangeAccount, destinationAddress, amountLumens) {
// Update transaction state to sending so it won't be
// resubmitted in case of the failure.
updateRecord('sending', "StellarTransactions");
// Check to see if the destination address exists
// GET https://horizon-testnet.stellar.org/accounts/{destinationAddress}
server.loadAccount(destinationAddress)
// If so, continue by submitting a transaction to the destination
.then(function(account) {
var transaction = new StellarSdk.TransactionBuilder(exchangeAccount)
.addOperation(StellarSdk.Operation.payment({
destination: destinationAddress,
asset: StellarSdk.Asset.native(),
amount: amountLumens
}))
// Wait a maximum of three minutes for the transaction
.setTimeout(180)
// Sign the transaction
.build();
transaction.sign(StellarSdk.Keypair.fromSecret(config.baseAccountSecret));
// POST https://horizon-testnet.stellar.org/transactions
return server.submitTransaction(transaction);
})
//But if the destination doesn't exist...
.catch(StellarSdk.NotFoundError, function(err) {
// create the account and fund it
var transaction = new StellarSdk.TransactionBuilder(exchangeAccount)
.addOperation(StellarSdk.Operation.createAccount({
destination: destinationAddress,
// Creating an account requires funding it with XLM
startingBalance: amountLumens
}))
// Wait a maximum of three minutes for the transaction
.setTimeout(180)
.build();
transaction.sign(StellarSdk.Keypair.fromSecret(config.baseAccountSecret));
// POST https://horizon-testnet.stellar.org/transactions
return server.submitTransaction(transaction);
})
// Submit the transaction created in either case
.then(function(transactionResult) {
updateRecord('done', "StellarTransactions");
})
.catch(function(err) {
// Catch errors, most likely with the network or your transaction
updateRecord('error', "StellarTransactions");
});
}
// This function handles submitting all pending transactions, and calls the previous one
// This function should be run in the background continuously
function submitPendingTransactions(exchangeAccount) {
// See what transactions in the db are still pending
// Update in an atomic transaction
db.transaction(function() {
var pendingTransactions = querySQL("SELECT * FROM StellarTransactions WHERE state =`pending`");
while (pendingTransactions.length > 0) {
var txn = pendingTransactions.pop();
// This function is async so it won't block. For simplicity we're using
// ES7 `await` keyword but you should create a "promise waterfall" so
// `setTimeout` line below is executed after all transactions are submitted.
// If you won't do it will be possible to send a transaction twice or more.
await submitTransaction(exchangeAccount, tx.destinationAddress, tx.amountLumens);
}
// Wait 30 seconds and process next batch of transactions.
setTimeout(function() {
submitPendingTransactions(sourceAccount);
}, 30*1000);
});
}
The federation protocol allows you to give your users easy addresses—e.g., bob*yourexchange.com—rather than cumbersome raw addresses such as: GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ?19327
For more information, check out the federation guide.
If you’re an exchange, it’s easy to become a Stellar anchor as well. Anchors are entities people trust to hold their deposits and issue credits into the Stellar network. As such, they act a bridge between existing currencies and the Stellar network. Becoming a anchor could potentially expand your business.
To learn more about what it means to be an anchor, see the anchor guide.
If you’d like to accept other non-lumen tokens follow these instructions.
First, open a trustline with the issuing account of the token you’d like to list – without this you cannot begin to accept the token.
var someAsset = new StellarSdk.Asset('ASSET_CODE', issuingKeys.publicKey());
transaction.addOperation(StellarSdk.Operation.changeTrust({
asset: someAsset
}))
If the token issuer has authorization_required
set to true, you will need to wait for the trustline to be authorized before you can begin accepting this token. Read more about trustline authorization here.
Then, make a few changes to the example code above:
handlePaymentResponse
function, we dealt with the case of incoming non-lumen assets. Since we are now accepting other tokens, you will need to change this condition; if the user sends us XLM we will either:
Note: the user cannot send us tokens whose issuing account we have not explicitly opened a trustline with.
withdraw
function, when we add an operation to the transaction, we must specify the details of the token we are sending. For example:var someAsset = new StellarSdk.Asset('ASSET_CODE', issuingKeys.publicKey());
transaction.addOperation(StellarSdk.Operation.payment({
destination: receivingKeys.publicKey(),
asset: someAsset,
amount: '10'
}))
withdraw
function your customer must have opened a trustline with the issuing account of the token they are withdrawing. So you must take the following into consideration:
For more information about tokens check out the general asset guide and the issuing asset guide.