Building a cryptocurrency exchange

Sequence provides cryptocurrency exchanges an easy-to-use, yet powerful, ledger infrastructure that can be used to securely track and custody client funds. Cryptocurrency exchanges can use Sequence to easily record deposits, withdrawals, and transfers of both fiat currencies and crypto assets on their platform.

In this guide, we explore how to build a cryptocurrency exchange application on top of Sequence.

Overview

In our example cryptocurrency exchange, users will be represented as accounts in the ledger. Additionally, we will create an account for the company, where fees will be collected.

There are two currencies - USD and EUR, as well as two crytpo assets - BTC (Bitcoin) and ETH (Ethereum ether). These will each be represented as assets in the ledger. (This can be extended to any number of different currencies and assets.)

Currencies can be deposited and withdrawn by users, and crypto assets can be transferred to other users in exchange for currencies or other crypto assets. The company charges a 1% fee on all withdrawals of currency. All of these interactions will be represented as transactions in the ledger.

Setup

To set up our ledger, we will create several keys, assets, and accounts.

Keys

Authority to create transactions in the ledger is assigned to two distinct systems:

  1. Treasury - responsible for deposits
  2. Exchange - responsible for exchange transactions that transfer assets between users and withdrawals

Each system will have a key that will be used to perform their actions in the ledger. Let's create a key for each system:

new Key.Builder()
  .setAlias("treasury")
  .create(ledger);

new Key.Builder()
  .setAlias("exchange")
  .create(ledger);
ledger.keys.create({alias: 'treasury'})
ledger.keys.create({alias: 'exchange'})
ledger.keys.create(alias: 'treasury')
ledger.keys.create(alias: 'exchange')

Assets

Assets represent the different types of balances in user accounts. We will create assets for usd, eur, btc, and eth, all using the treasury key:

new Asset.Builder()
  .setAlias("usd")
  .addKeyByAlias("treasury")
  .addTag("type", "currency")
  .create(ledger);

new Asset.Builder()
  .setAlias("eur")
  .addKeyByAlias("treasury")
  .addTag("type", "currency")
  .create(ledger);

new Asset.Builder()
  .setAlias("btc")
  .addKeyByAlias("treasury")
  .addTag("type", "crypto_asset")
  .create(ledger);

new Asset.Builder()
  .setAlias("eth")
  .addKeyByAlias("treasury")
  .addTag("type", "crypto_asset")
  .create(ledger);
ledger.assets.create({
  alias: 'usd',
  keys: [{alias: 'treasury'}],
  tags: {type: 'currency'}
})

ledger.assets.create({
  alias: 'eur',
  keys: [{alias: 'treasury'}],
  tags: {type: 'currency'}
})

ledger.assets.create({
  alias: 'btc',
  keys: [{alias: 'treasury'}],
  tags: {type: 'crypto_asset'}
})

ledger.assets.create({
  alias: 'eth',
  keys: [{alias: 'treasury'}],
  tags: {type: 'crypto_asset'}
})
ledger.assets.create(
  alias: 'usd',
  keys: [{alias: 'treasury'}],
  tags: {type: 'currency'}
)

ledger.assets.create(
  alias: 'eur',
  keys: [{alias: 'treasury'}],
  tags: {type: 'currency'}
)

ledger.assets.create(
  alias: 'btc',
  keys: [{alias: 'treasury'}],
  tags: {type: 'crypto_asset'}
)

ledger.assets.create(
  alias: 'eth',
  keys: [{alias: 'treasury'}],
  tags: {type: 'crypto_asset'}
)

Accounts

For each user, we will need an account in the ledger. 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 also need the company account where we will collect fees.

We will use tags to differentiate between the types of accounts.

We use the exchange key to create all accounts.

new Account.Builder()
  .setAlias("alice")
  .addKeyByAlias("exchange")
  .addTag("type", "user")
  .create(ledger);

new Account.Builder()
  .setAlias("bob")
  .addKeyByAlias("exchange")
  .addTag("type", "user")
  .create(ledger);

new Account.Builder()
  .setAlias("company")
  .addKeyByAlias("exchange")
  .addTag("type", "company")
  .create(ledger);
ledger.accounts.create({
  alias: 'alice',
  keys: [{alias: 'exchange'}],
  tags: {type: 'user'}
})

ledger.accounts.create({
  alias: 'bob',
  keys: [{alias: 'exchange'}],
  tags: {type: 'user'}
})

ledger.accounts.create({
  alias: 'company',
  keys: [{alias: 'exchange'}],
  tags: {type: 'company'}
})
ledger.accounts.create(
  alias: 'alice',
  keys: [{alias: 'exchange'}],
  tags: {type: 'user'}
)

ledger.accounts.create(
  alias: 'bob',
  keys: [{alias: 'exchange'}],
  tags: {type: 'user'}
)

ledger.accounts.create(
  alias: 'company',
  keys: [{alias: 'exchange'}],
  tags: {type: 'company'}
)

Transaction Types

Now that we have created our assets and accounts, we can model the different types of transactions. To ensure atomicity, a single transaction can include multiple actions involving any number of assets and accounts. These actions occur simultaneously, as a single, atomic operation. There is never any point where a transaction is only partially applied.

Deposit

When a user deposits 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.

We can use action reference data to record details about the deposit, such as the deposit method and associated transaction ID in that external system.

For this example, we assume that Alice deposits $100.00 via ACH and 5 BTC, and that Bob deposits 50 ETH. Note that the amount of issuance of USD is 10000, since the fundamental unit of the USD asset is a cent. We can do all three of these actions in a single transaction:

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Issue()
    .setAssetAlias("usd")
    .setAmount(10000)
    .setDestinationAccountAlias("alice")
    .addReferenceDataField("type", "deposit")
    .addReferenceDataField("system", "ach")
    .addReferenceDataField("ach_transaction_id", "11111")
  ).addAction(new Transaction.Builder.Action.Issue()
    .setAssetAlias("btc")
    .setAmount(5)
    .setDestinationAccountAlias("alice")
    .addReferenceDataField("type", "deposit")
  ).addAction(new Transaction.Builder.Action.Issue()
    .setAssetAlias("eth")
    .setAmount(50)
    .setDestinationAccountAlias("bob")
    .addReferenceDataField("type", "deposit")
  ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.issue({
    assetAlias: 'usd',
    amount: 10000,
    destinationAccountAlias: 'alice',
    referenceData: {
      type: 'deposit',
      system: 'ach',
      ach_transaction_id: '11111'
    }
  })
  builder.issue({
    assetAlias: 'btc',
    amount: 5,
    destinationAccountAlias: 'alice',
    referenceData: {
      type: 'deposit'
    }
  })
  builder.issue({
    assetAlias: 'eth',
    amount: 50,
    destinationAccountAlias: 'bob',
    referenceData: {
      type: 'deposit'
    }
  })
})
ledger.transactions.transact do |builder|
  builder.issue(
    asset_alias: 'usd',
    amount: 10000,
    destination_account_alias: 'alice',
    reference_data: {
      type: 'deposit',
      system: 'ach',
      ach_transaction_id: '11111'
    }
  )
  builder.issue(
    asset_alias: 'btc',
    amount: 5,
    destination_account_alias: 'alice',
    reference_data: {
      type: 'deposit'
    }
  )
  builder.issue(
    asset_alias: 'eth',
    amount: 50,
    destination_account_alias: 'bob',
    reference_data: {
      type: 'deposit'
    }
  )
end

Since this transaction issues the USD asset, it must be signed by the treasury key. This is handled automatically by the transact SDK method.

Settle Trade Match

When two users match on a trade, we model their exchange as one atomic transaction with two actions:

  • transfer - trade amount of first currency or crypto asset from one user ("User1") to the other ("User2")
  • transfer - trade amount of second currency or crypto asset from User2 to User1

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 16 ETH.

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Transfer()
    .setAssetAlias("btc")
    .setAmount(1)
    .setSourceAccountAlias("alice")
    .setDestinationAccountAlias("bob")
    .addReferenceDataField("type", "exchange")
    .addReferenceDataField("subtype", "crypto_exchange")
  ).addAction(new Transaction.Builder.Action.Transfer()
    .setAssetAlias("eth")
    .setAmount(16)
    .setSourceAccountAlias("bob")
    .setDestinationAccountAlias("alice")
    .addReferenceDataField("type", "exchange")
    .addReferenceDataField("subtype", "crypto_exchange")
  ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.transfer({
    assetAlias: 'btc',
    amount: 1,
    sourceAccountAlias: 'alice',
    destinationAccountAlias: 'bob',
    referenceData: {
      type: 'exchange',
      subtype: 'crypto_exchange'
    }
  })
  builder.transfer({
    assetAlias: 'eth',
    amount: 16,
    sourceAccountAlias: 'bob',
    destinationAccountAlias: 'alice',
    referenceData: {
      type: 'exchange',
      subtype: 'crypto_exchange'
    }
  })
})
ledger.transactions.transact do |builder|
  builder.transfer(
    asset_alias: 'btc',
    amount: 1,
    source_account_alias: 'alice',
    destination_account_alias: 'bob',
    reference_data: {
      type: 'exchange',
      sub_type: 'crypto_exchange'
    }
  )
  builder.transfer(
    asset_alias: 'eth',
    amount: 16,
    source_account_alias: 'bob',
    destination_account_alias: 'alice',
    reference_data: {
      type: 'exchange',
      subtype: 'crypto_exchange'
    }
  )
end

Withdraw

When a user withdraws 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:

  1. Transfer - the fee amount of the currency asset from the user's account to the company account
  2. Retire - the remaining amount of the currency asset from the user's account

We can use action reference data to record details about the withdrawal, such as the withdrawal method and associated transaction ID in that external system.

For this example, we'll assume that Alice withdraws $200.00 via ACH. Note that the amount being retired is 20000 because the fundamental unit of the USD asset is a cent.

Transaction tx = new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Transfer()
    .setAssetAlias("usd")
    .setAmount(200)
    .setSourceAccountAlias("alice")
    .setDestinationAccountAlias("company")
    .addReferenceDataField("type", "withdrawal_fee")
  ).addAction(new Transaction.Builder.Action.Retire()
    .setAssetAlias("usd")
    .setAmount(19800)
    .setSourceAccountAlias("alice")
    .addReferenceDataField("type", "withdrawal")
    .addReferenceDataField("system", "ACH")
    .addReferenceDataField("ach_transaction_id", "22222")
    ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.transfer({
    assetAlias: 'usd',
    amount: 200,
    sourceAccountAlias: 'alice',
    destinationAccountAlias: 'company',
    referenceData: {
      type: 'withdrawal_fee'
    }
  })
  builder.retire({
    assetAlias: 'usd',
    amount: 19800,
    sourceAccountAlias: 'alice',
    referenceData: {
      type: 'withdrawal',
      system: 'ACH',
      ach_transaction_id: '22222'
    }
  })
})
tx = ledger.transactions.transact do |builder|
  builder.transfer(
    asset_alias: 'usd',
    amount: 200,
    source_account_alias: 'alice',
    destination_account_alias: 'company',
    reference_data: {
      type: 'withdrawal_fee'
    }
  )
  builder.retire(
    asset_alias: 'usd',
    amount: 9800,
    source_account_alias: 'alice',
    reference_data: {
      type: 'withdrawal',
      system: 'ach',
      ach_transaction_id: '22222'
    }
  )
end

Since this transaction transfers and retires from a user account, it must be signed by the exchange key. This is handled automatically by the transact SDK method.

Queries

Now that we have created several transactions, we can query the ledger in various ways.

User Balances

If we want to know the amount of each asset in an account, we perform a balance query, filtering to the account alias and summing the results by asset alias.

For example, let's list the balances in Alice's account.

Balance.ItemIterable balances = new Balance.QueryBuilder()
  .setFilter("account_alias=$1")
  .addFilterParameter("alice")
  .addSumByField("asset_alias")
  .getIterable(ledger);
for (Balance balance : balances) {
  System.out.println("amount: " + balance.amount );
  System.out.println("asset: " + balance.sumBy.get("asset_alias"));
  System.out.println("");
}
ledger.balances.queryAll({
  filter: 'account_alias=$1',
  filterParams: ['alice'],
  sumBy: ['asset_alias']
}).then(balances => {
  for (let i in balances) {
    const balance = balances[i];
    console.log('amount: ' + balance.amount )
    console.log('asset: ' + balance.sumBy.assetAlias )
    console.log('')
  }
})
ledger.balances.query(
  filter: 'account_alias=$1',
  filter_params: ['alice'],
  sum_by: ['asset_alias']
).each do |balance|
  puts 'amount: ' + balance.amount.to_s
  puts 'asset: ' + balance.sum_by['asset_alias']
  puts ''
end

Which will output:

amount: x
asset: usd

amount: y
asset: btc

Crypto Asset Totals

If we want to know the total amount of each crypto asset on the exchange across all accounts, we perform a balance query, filtering to the crypto_asset type (in tags) and summing the results by asset alias.

Balance.ItemIterable balances = new Balance.QueryBuilder()
  .setFilter("asset_tags.type=$1")
  .addFilterParameter("crypto_asset")
  .addSumByField("asset_alias")
  .getIterable(ledger);
for (Balance balance : balances) {
  System.out.println("amount: " + balance.amount);
  System.out.println("asset: " + balance.sumBy.get("asset_alias"));
  System.out.println("");
}
ledger.balances.queryAll({
  filter: 'asset_tags.type=$1',
  filterParams: ['crypto_asset'],
  sumBy: ['asset_alias']
}).then(balances => {
  for (let i in balances) {
    const balance = balances[i];
    console.log('amount: ' + balance.amount )
    console.log('asset: ' + balance.sumBy.assetAlias )
    console.log('')
  }
})
ledger.balances.query(
  filter: 'asset_tags.type=$1',
  filter_params: ['crypto_asset'],
  sum_by: ['asset_alias']
).each do |balance|
  puts 'amount: ' + balance.amount.to_s
  puts 'asset: ' + balance.sum_by['asset_alias']
  puts ''
end

Which will output:

amount: 100
asset: usd

User Activity

If we want to display the latest transactions for a specific account, we perform a transaction query, filtering to actions that contain the account as the source_account or destination_account.

Let's query for all the transactions that involve Alice's account and display the type of transaction by accessing the type field in the action reference data.

Transaction.ItemIterable txs = new Transaction.QueryBuilder()
  .setFilter("actions(source_account_alias=$1 OR destination_account_alias=$1)")
  .addFilterParameter("alice")
  .getIterable(ledger);
for (Transaction tx : txs) {
  for (Transaction.Action action : tx.actions) {
    if ("alice".equals(action.sourceAccountAlias) || "alice".equals(action.destinationAccountAlias)) {
      System.out.println("type: " + action.referenceData.get("type"));
      System.out.println("asset: " + action.assetAlias);
      System.out.println("amount: " + action.amount);
      System.out.println("to: " + action.destinationAccountAlias);
      System.out.println("");
    }
  }
}
ledger.transactions.queryAll({
  filter: 'actions(source_account_alias=$1 OR destination_account_alias=$1)',
  filterParams: ['alice']
}).then(txs => {
  for (let i in txs) {
    const tx = txs[i];
    for (let j in tx.actions) {
      const action = tx.actions[j]
      if (action.sourceAccountAlias == 'alice' || action.destinationAccountAlias == 'alice') {
        console.log('type: ' + action.referenceData.type)
        console.log('asset: ' + action.assetAlias)
        console.log('amount: ' + action.amount)
        console.log('to: ' + action.destinationAccountAlias)
        console.log('')
      }
    }
  }
})
ledger.transactions.query(
  filter: 'actions(source_account_alias=$1 OR destination_account_alias=$1)',
  filter_params: ['alice']
).each do |tx|
  tx['actions'].each do |action|
    if action['source_account_alias'] == 'alice' || action['destination_account_alias'] == 'alice'
      puts 'type: ' + action['reference_data']['type']
      puts 'asset: ' + action['asset_alias']
      puts 'amount: ' + action['amount'].to_s
      puts 'to: ' + action['destination_account_alias'].to_s
      puts ''
    end
  end
end

Which will output:

type: deposit
asset: usd
amount: 500
to: alice

type: exchange
asset: btc
amount: 1000
to: bob