Crypto Exchange
Sequence provides crypto asset exchanges with an easy-to-use, powerful ledger infrastructure for securely tracking client funds. Exchanges can use Sequence to easily record deposits, withdrawals, and transfers of fiat currencies and crypto assets on their platform.
In this guide, we explore how to build a crypto asset exchange application on top of Sequence.
Overview
In our example crypto asset exchange, users will be represented as accounts in the ledger.
There are two currencies, USD and EUR, as well as two crypto assets, BTC (Bitcoin) and ETH (Ethereum ether). These will each be represented as flavors in the ledger. (This can be extended to any number of different currencies and assets.)
Currencies and crypto assets can be deposited, withdrawn, and transferred to other users in exchange for currencies or other crypto assets. The company charges a 1% fee on all withdrawals of fiat currency. All of these interactions will be represented as transactions in the ledger.
Setup
To set up our ledger, we will create several keys, flavors, and accounts.
Keys
Authority to create transactions in the ledger is assigned to two distinct systems:
- Treasury - responsible for deposits
- Exchange - responsible for exchange transactions that transfer tokens between users and withdrawals
Each system will have a key that will be used to perform their actions in the ledger. To create these keys, we run the following:
- Java
- Node.js
- Ruby
new Key.Builder()
.setId("treasury")
.create(ledger);
new Key.Builder()
.setId("exchange")
.create(ledger);
ledger.keys.create({id: 'treasury'})
ledger.keys.create({id: 'exchange'})
ledger.keys.create(id: 'treasury')
ledger.keys.create(id: 'exchange')
Flavors
Flavors represent the different types of balances in user accounts. We will create flavors for USD, EUR, BTC, and ETH, all using the treasury
key:
- Java
- Node.js
- Ruby
new Flavor.Builder()
.setId("usd")
.addKeyId("treasury")
.addTag("type", "currency")
.create(ledger);
new Flavor.Builder()
.setId("eur")
.addKeyId("treasury")
.addTag("type", "currency")
.create(ledger);
new Flavor.Builder()
.setId("btc")
.addKeyId("treasury")
.addTag("type", "crypto_Flavor")
.create(ledger);
new Flavor.Builder()
.setId("eth")
.addKeyId("treasury")
.addTag("type", "crypto_Flavor")
.create(ledger);
ledger.flavors.create({
id: 'usd',
keyIds: ['treasury'],
tags: {type: 'currency'}
})
ledger.flavors.create({
id: 'eur',
keyIds: ['treasury'],
tags: {type: 'currency'}
})
ledger.flavors.create({
id: 'btc',
keyIds: ['treasury'],
tags: {type: 'crypto_asset'}
})
ledger.flavors.create({
id: 'eth',
keyIds: ['treasury'],
tags: {type: 'crypto_asset'}
})
ledger.flavors.create(
id: 'usd',
key_ids: ['treasury'],
tags: {type: 'currency'}
)
ledger.flavors.create(
id: 'eur',
key_ids: ['treasury'],
tags: {type: 'currency'}
)
ledger.flavors.create(
id: 'btc',
key_ids: ['treasury'],
tags: {type: 'crypto_asset'}
)
ledger.flavors.create(
id: 'eth',
key_ids: ['treasury'],
tags: {type: 'crypto_asset'}
)
Accounts
We will need an account in the ledger for each user. Although these accounts would actually be created by the exchange application in real-time, for this example we'll assume we have two users (Alice and Bob) and create them as part of the setup.
We will use tags to differentiate between the types of accounts.
We use the exchange
key to create all accounts.
- Java
- Node.js
- Ruby
new Account.Builder()
.setId("alice")
.addKeyId("exchange")
.addTag("type", "user")
.create(ledger);
new Account.Builder()
.setId("bob")
.addKeyId("exchange")
.addTag("type", "user")
.create(ledger);
ledger.accounts.create({
id: 'alice',
keyIds: ['exchange'],
tags: {type: 'user'}
})
ledger.accounts.create({
id: 'bob',
keyIds: ['exchange'],
tags: {type: 'user'}
})
ledger.accounts.create(
id: 'alice',
key_ids: ['exchange'],
tags: {type: 'user'}
)
ledger.accounts.create(
id: 'bob',
key_ids: ['exchange'],
tags: {type: 'user'}
)
Transaction Types
Now that we have created our flavors and accounts, we can track events with transactions. A single transaction can include multiple actions, involving any number of flavors and accounts. The actions in a transaction occur simultaneously, as a single, atomic operation. A transaction can never be partially applied.
Deposit
When a user deposits fiat currency or a crypto asset, we create a transaction containing an issue action to issue the amount of the deposited currency or crypto asset into their account. This will create a number of tokens of the corresponding flavor and put them in the account.
We can use action tags to record details about the deposit, such as the deposit method and associated transaction ID in an external system.
For this example, we assume that Alice deposits $1,000.00 via ACH and 5 BTC, and that Bob deposits 50 ETH. Note that the amount of issuance of USD is 10000, because the fundamental unit of the USD asset is a cent. (In a real application, our BTC asset would be denominated in Satoshi. We don't do that here so the amounts are easier to read.) We can do all three of these actions in a single transaction:
- Java
- Node.js
- Ruby
new Transaction.Builder()
.addAction(new Transaction.Builder.Action.Issue()
.setFlavorId("usd")
.setAmount(10000)
.setDestinationAccountId("alice")
.addActionTagsField("type", "deposit")
.addActionTagsField("system", "ach")
.addActionTagsField("ach_transaction_id", "11111")
).addAction(new Transaction.Builder.Action.Issue()
.setFlavorId("btc")
.setAmount(5)
.setDestinationAccountId("alice")
.addActionTagsField("type", "deposit")
).addAction(new Transaction.Builder.Action.Issue()
.setFlavorId("eth")
.setAmount(50)
.setDestinationAccountId("bob")
.addActionTagsField("type", "deposit")
).transact(ledger);
ledger.transactions.transact(builder => {
builder.issue({
flavorId: 'usd',
amount: 100000,
destinationAccountId: 'alice',
actionTags: {
type: 'deposit',
system: 'ach',
ach_transaction_id: '11111'
}
})
builder.issue({
flavorId: 'btc',
amount: 5,
destinationAccountId: 'alice',
actionTags: {
type: 'deposit'
}
})
builder.issue({
flavorId: 'eth',
amount: 50,
destinationAccountId: 'bob',
actionTags: {
type: 'deposit'
}
})
})
ledger.transactions.transact do |builder|
builder.issue(
flavor_id: 'usd',
amount: 100000,
destination_account_id: 'alice',
action_tags: {
type: 'deposit',
system: 'ach',
ach_transaction_id: '11111'
}
)
builder.issue(
flavor_id: 'btc',
amount: 5,
destination_account_id: 'alice',
action_tags: {
type: 'deposit'
}
)
builder.issue(
flavor_id: 'eth',
amount: 50,
destination_account_id: 'bob',
action_tags: {
type: 'deposit'
}
)
end
Because this transaction issues tokens of the USD flavor, it must be signed by the treasury
key. This is handled automatically by the transact
SDK method.
Buy Crypto Assets with Fiat Currency
When a user purchases a crypto asset using a fiat currency, we model the purchase as an atomic transaction with two actions:
- Transfer - purchase price from the buyer to the seller
- Transfer - purchased crypto asset from the seller to the buyer
In this example, we assume that Alice buys 1 ETH from Bob for $500.00.
- Java
- Node.js
- Ruby
new Transaction.Builder()
.addAction(new Transaction.Builder.Action.Transfer()
.setFlavorId("usd")
.setAmount(50000)
.setSourceAccountId("alice")
.setDestinationAccountId("bob")
.addActionTagsField("type", "crypto_for_fiat")
.addActionTagsField("tx_id", "1234")
).addAction(new Transaction.Builder.Action.Transfer()
.setFlavorId("eth")
.setAmount(1)
.setSourceAccountId("bob")
.setDestinationAccountId("alice")
.addActionTagsField("type", "crypto_for_fiat")
.addActionTagsField("tx_id", "1234")
).transact(ledger);
ledger.transactions.transact(builder => {
builder.transfer({
flavorId: 'usd',
amount: 50000,
sourceAccountId: 'alice',
destinationAccountId: 'bob',
actionTags: {
type: 'crypto_for_fiat',
tx_id: '1234'
}
})
builder.transfer({
flavorId: 'eth',
amount: 1,
sourceAccountId: 'bob',
destinationAccountId: 'alice',
actionTags: {
type: 'crypto_for_fiat',
tx_id: '1234'
}
})
})
ledger.transactions.transact do |builder|
builder.transfer(
flavor_id: 'usd',
amount: 50000,
source_account_id: 'alice',
destination_account_id: 'bob',
action_tags: {
type: 'crypto_for_fiat',
tx_id: '1234'
}
)
builder.transfer(
flavor_id: 'eth',
amount: 1,
source_account_id: 'bob',
destination_account_id: 'alice',
action_tags: {
type: 'crypto_for_fiat',
tx_id: '1234'
}
)
end
Settle Trade Match
When two users match on a trade of crypto assets, the transaction is similar to the previous one, except in this case both actions will involve crypto assets.
- Transfer - trade amount of first crypto asset from the first user to the second user
- Transfer - trade amount of second crypto asset from the second user to the first user
The exchange rate would be determined by the company before the transaction is submitted to the ledger.
In this example, we assume that Alice transfers 1 BTC to Bob in exchange for 15 ETH.
- Java
- Node.js
- Ruby
new Transaction.Builder()
.addAction(new Transaction.Builder.Action.Transfer()
.setFlavorId("btc")
.setAmount(1)
.setSourceAccountId("alice")
.setDestinationAccountId("bob")
.addActionTagsField("type", "exchange")
.addActionTagsField("subtype", "crypto_exchange")
).addAction(new Transaction.Builder.Action.Transfer()
.setFlavorId("eth")
.setAmount(16)
.setSourceAccountId("bob")
.setDestinationAccountId("alice")
.addActionTagsField("type", "exchange")
.addActionTagsField("subtype", "crypto_exchange")
).transact(ledger);
ledger.transactions.transact(builder => {
builder.transfer({
flavorId: 'btc',
amount: 1,
sourceAccountId: 'alice',
destinationAccountId: 'bob',
actionTags: {
type: 'crypto_exchange',
tx_id: '5678'
}
})
builder.transfer({
flavorId: 'eth',
amount: 15,
sourceAccountId: 'bob',
destinationAccountId: 'alice',
actionTags: {
type: 'crypto_exchange',
tx_id: '5678'
}
})
})
ledger.transactions.transact do |builder|
builder.transfer(
flavor_id: 'btc',
amount: 1,
source_account_id: 'alice',
destination_account_id: 'bob',
action_tags: {
type: 'crypto_exchange',
tx_id: '5678'
}
)
builder.transfer(
flavor_id: 'eth',
amount: 15,
source_account_id: 'bob',
destination_account_id: 'alice',
action_tags: {
type: 'crypto_exchange',
tx_id: '5678'
}
)
end
External Sale
If a user sells a crypto asset to a party that is not a user of the exchange, instead of transferring tokens in the ledger we will retire the crypto asset being sold (because it will no longer be under the control of the exchange) and issue the amounts that the user is receiving in return (because those amounts are coming in from outside of the exchange).
- Retire - the amount of the crypto asset being sold from the user's account
- Issue - the amount of crypto asset or fiat currency received by the user to that user's account
We can use action tags to record details about the transaction, such as a transaction id or information about any incoming wire.
For this example, assume Bob sells 1 BTC to an external purchaser for $9,000.00.
- Java
- Node.js
- Ruby
new Transaction.Builder()
.addAction(new Transaction.Builder.Action.Retire()
.setFlavorId("btc")
.setAmount(1)
.setSourceAccountId("bob")
.addActionTagsField("type", "external_sale")
).addAction(new Transaction.Builder.Action.Issue()
.setFlavorId("usd")
.setAmount(900000)
.setDestinationAccountId("bob")
.addActionTagsField("type", "external_sale")
).transact(ledger);
ledger.transactions.transact(builder => {
builder.retire({
flavorId: 'btc',
amount: 1,
sourceAccountId: 'bob',
actionTags: {
type: 'external_sale'
}
})
builder.issue({
flavorId: 'usd',
amount: 900000,
destinationAccountId: 'bob',
actionTags: {
type: 'external_sale'
}
})
})
ledger.transactions.transact do |builder|
builder.retire(
flavor_id: 'btc',
amount: 1,
source_account_id: 'bob',
action_tags: {
type: 'external_sale'
}
)
builder.issue(
flavor_id: 'usd',
amount: 900000,
destination_account_id: 'bob',
action_tags: {
type: 'external_sale'
}
)
end
Withdraw
When a user withdraws fiat currency, the company takes a 1% fee and remits the remainder to the user. We model this as a single atomic transaction with two actions:
- Retire - the fee amount of the currency asset from the user's account to the company account
- Retire - the remaining amount of the currency asset from the user's account
We can use action tags to record details about the withdrawal, such as the withdrawal method and associated transaction ID in that external system. Note that we do two retire actions instead of a single one for the full amount so that we can query for fee amounts in the future. See the Queries section for an example of this query.
For this example, we'll assume that Alice withdraws $200.00 via ACH.
- Java
- Node.js
- Ruby
new Transaction.Builder()
.addAction(new Transaction.Builder.Action.Retire()
.setFlavorId("usd")
.setAmount(200)
.setSourceAccountId("alice")
.addActionTagsField("type", "withdrawal_fee")
).addAction(new Transaction.Builder.Action.Retire()
.setFlavorId("usd")
.setAmount(19800)
.setSourceAccountId("alice")
.addActionTagsField("type", "withdrawal")
.addActionTagsField("system", "ACH")
.addActionTagsField("ach_transaction_id", "22222")
).transact(ledger);
ledger.transactions.transact(builder => {
builder.retire({
flavorId: 'usd',
amount: 200,
sourceAccountId: 'alice',
actionTags: {
type: 'withdrawal_fee'
}
})
builder.retire({
flavorId: 'usd',
amount: 19800,
sourceAccountId: 'alice',
actionTags: {
type: 'withdrawal',
system: 'ach',
ach_transaction_id: '22222'
}
})
})
ledger.transactions.transact do |builder|
builder.retire(
flavor_id: 'usd',
amount: 200,
source_account_id: 'alice',
action_tags: {
type: 'withdrawal_fee'
}
)
builder.retire(
flavor_id: 'usd',
amount: 19800,
source_account_id: 'alice',
action_tags: {
type: 'withdrawal',
system: 'ach',
ach_transaction_id: '22222'
}
)
end
Since this transaction retires from a user account, it must be signed by the exchange
key. This is handled automatically by the transact
SDK method.
Note that instead of retiring the amount corresponding to the company fee, we could have transferred it to a company account. That design, however, would result in those tokens for the company fee sitting in the company account indefinitely. In general, you should only use tokens for amounts that will be needed later. In other words, you should use balances of tokens for current state, and query actions for historical state. With our design, we can still determine the total amount of fees collected by querying actions with the type of withdrawal_fee
.
Queries
Now that we have created several transactions, we can query the ledger in various ways.
User Balances
If we want to know the the balances in every account, we perform a sum tokens query with no filter (so we count every token) and group the results by flavor id.
For example, let's list the balances in Alice's account.
- Java
- Node.js
- Ruby
TokenSum.ItemIterable balances = new Token.SumBuilder()
.addGroupByField("accountId")
.addGroupByField("flavorId")
.getIterable(ledger);
for (Balance balance : balances) {
System.out.println("account: " + balance.accountId);
System.out.println("amount: " + balance.amount );
System.out.println("flavor: " + balance.flavorId);
System.out.println("");
}
var page1 = ledger.tokens.sum({
groupBy: ['accountId', 'flavorId']
}).page()
page1.items.forEach(balance => {
console.log('account: ' + balance.accountId)
console.log('amount: ' + balance.amount)
console.log('flavor: ' + balance.flavorId)
console.log('')
})
ledger.tokens.sum(
# filter: 'account_id=$1',
# filter_params: ['alice'],
group_by: ['account_id', 'flavor_id']
).each do |balance|
puts 'account: ' + balance.account_id
puts 'amount: ' + balance.amount.to_s
puts 'flavor: ' + balance.flavor_id
puts ''
end
which will output:
account: alice
amount: 16
flavor: eth
(etc.)
Crypto Asset Totals
If we want to know the total amount of each crypto asset on the exchange across all accounts, we perform a sum tokens query, filtering to the crypto_asset
type in flavor tags and grouping the results by flavor id.
- Java
- Node.js
- Ruby
TokenSum.ItemIterable balances = new Token.SumBuilder()
.setFilter("FlavorTags.type=$1")
.addFilterParameter("crypto_asset")
.addGroupByField("flavorId")
.getIterable(ledger);
for (Balance balance : balances) {
System.out.println("amount: " + balance.amount);
System.out.println("flavor: " + balance.flavorId);
System.out.println("");
}
page1 = ledger.tokens.sum({
filter: 'flavorTags.type=$1',
filterParams: ['crypto_asset'],
groupBy: ['flavorId']
}).page()
page1.items.forEach(balance => {
console.log('amount: ' + balance.amount)
console.log('flavor: ' + balance.flavorId)
console.log('')
})
ledger.tokens.sum(
filter: 'flavor_tags.type=$1',
filter_params: ['crypto_asset'],
group_by: ['flavor_id']
).each do |balance|
puts 'amount: ' + balance.amount.to_s
puts 'flavor: ' + balance.flavor_id
puts ''
end
which will output:
amount: 100
flavor: usd
Total Fees
If we want to know the total fees that have been collected, we perform a sum actions query, filtering to actions with the withdrawal_fee
type in action tags.
- Java
- Node.js
- Ruby
ActionSum.ItemIterable sums = new Action.SumBuilder()
.setFilter("Tags.type")
.addFilterParameter("withdrawal_fee")
.getIterable(ledger);
for (ActionSum sum : sums) {
System.out.println("total fees: " + sum.amount);
}
page1 = ledger.actions.sum({
filter: 'tags.type=$1',
filterParams: ['withdrawal_fee']
}).page()
page1.items.forEach(sum => {
console.log('total fees: ' + sum.amount)
})
ledger.actions.sum(
filter: 'tags.type=$1',
filter_params: ['withdrawal_fee']
).each do |sum|
puts 'total fees: ' + sum.amount.to_s
end
User Activity
If we want to retrieve historical transactions, we use the list transactions query. We can specify the number of transactions we want to retrieve at a time by setting a page size. If we only care about transactions on a specific account, we can filter to those that have an action with that account as the source or destination by using actions()
in a filter.
The below will return the 10 most recent transactions that involved Alice's account.
- Java
- Node.js
- Ruby
Transaction.Page txs = new Transaction.ListBuilder()
.setFilter("actions(SourceAccountId=$1 OR DestinationAccountId=$1)")
.addFilterParameter("alice")
.setPageSize(10)
.getPage(ledger);
page1 = ledger.transactions.list({
filter: 'actions(sourceAccountId=$1 OR destinationAccountId=$1)',
filterParams: ['alice']
}).page({size: 10})
page1 = ledger.transactions.list(
filter: 'actions(source_account_id=$1 OR destination_account_id=$1)',
filter_params: ['alice']
).page(size: 10)
If we only cared about the specific actions involving the account (as opposed to the entire transaction), we could use the list actions query instead. This will return Action objects, whereas the above query will return Transaction objects.
- Java
- Node.js
- Ruby
Action.Page txs = new Action.ListBuilder()
.setFilter("SourceAccountId=$1 OR DestinationAccountId=$1")
.addFilterParameter("alice")
.setPageSize(10)
.getPage(ledger);
page1 = ledger.actions.list({
filter: 'sourceAccountId=$1 OR destinationAccountId=$1',
filterParams: ['alice']
}).page({size: 10})
page1 = ledger.actions.list(
filter: 'source_account_id=$1 OR destination_account_id=$1',
filter_params: ['alice']
).page(size: 10)