Dexterity Typescript SDK QuickStart

The QuickStart tutorial document below provides an overview of how to connect to and use Hxro’s Dexterity Protocol

Index:

What is the Hxro Dexterity Protocol?

Dexterity is a decentralized protocol that allows for the creation and exchange of financial instruments such as perpetual swaps and futures. The protocol provides an on-chain solution to trade and settle these instruments in a liquid, capital-efficient, and decentralized manner. It has a customizable fee model and a customizable risk engine to handle margining and liquidation of trades. It also supports combo products for trading contracts with arbitrary leg ratios. Dexterity relies on a few main components including the Decentralized Exchange (DEX) contract, the Asset Agnostic Order Book (AAOB), and the Instruments, in order to manage the accounting of orders, trades, and product metadata, to track the order book, and to implement the payoff functions of various financial instruments.

  • MPG ⇒ Market Product Group

    • An MPG is a group of Products that derive from the same Market (ie: BTC-USD)

    • Examples:

      • BTC/USD

        • BTCUSD-PERP (Perpetual Future)

        • BTCUSD-MAR2023 (Dated Futures)

      • ETH/USD

        • ETHUSD-PERP (Perpetual Future)

        • ETHUSD-MAR2023 (Dated Futures)

  • TRG ⇒ Trader Risk Group

    • Think of these as user/trader ‘accounts’ per MPG, where you can deposit capital and trade with those products on their respective MPG

The following walkthrough provides a step-by-step tutorial on how to create a TRG in the BTC-USD Market-Product-Group for your Wallet

Requirements:

  1. Previous experience with TS

  2. Install node.js and npm

  3. Create a Typescript project (Don’t know how? Read here)

  4. Install Solana Web3.js in your project by running npm i @solana/web3.js

  5. Install the Dexterity SDK in your project by running npm i @hxronetwork/dexterity-ts

  6. Install the Anchor library in your project by running npm i @project-serum/anchor

  7. Install the base58 encoding library in your project by running npm i bs58

Getting Started:

Importing dependencies:

import { clusterApiUrl, Keypair, PublicKey } from "@solana/web3.js";
import { Wallet } from "@project-serum/anchor";
import dexterityTs from "@hxronetwork/dexterity-ts";
const dexterity = dexterityTs;
import bs58 from 'bs58'

Setting up an RPC

Set up an RPC to Solana Testnet using the clusterApiUrl() method from @solana/web3.js and set the cluster name to "testnet". Add the following code to your file:

const CLUSTER_NAME = "testnet";
const rpc = clusterApiUrl(CLUSTER_NAME);

Tip: Instead of using the clusterApiUrl() you can pass in your own RPC URL (as a string) for a better connection to Solana since the public ones tend to be more highly saturated

const rpc = "https://your-rpc-url.com";

Setting up a test wallet

To sign transactions and create the TRG account, set up your wallet with your private key. Replace the priv_key variable with your own private key.

const priv_key = "YOUR-PRIVATE-KEY"
const keypair = Keypair.fromSecretKey(
  bs58.decode(priv_key)
);
const wallet = new Wallet(keypair);

It is highly recommended that you create a new wallet to be used exclusively for testing purposes. This can be done via Phantom or any other Solana wallet, subsequently allowing you to retrieve the private key, and use it in your project. Once you have set up your wallet, you can then airdrop Testnet SOL to it from here.

Connecting to Dexterity

Getting the Manifest

Create an asynchronous function called CreateTRG() in order to connect to Dexterity and obtain the manifest. This is achieved by using the *getManifest(rpc, useCache, wallet)* method, passing in your rpc for *rpc*, a boolean for *useCache* (for this tutorial, false is used), and your wallet for *wallet*

const CreateTRG = async() => {
  
  // get the latest manifest
  const manifest = await dexterity.getManifest(rpc, false, wallet);

}

CreateTRG()

If the file is ran, you should now see your Manifest object printed out in the console.

  • Manifest Object Example Output

    getting manifest 9nWzcvtMAzNauXgeYDqZgNZZH24T8d8c99BKofMUsNCJ:<https://api.testnet.solana.com>
    cached manifest <https://api.testnet.solana.com>
    got manifest $882b6d93070905b3$var$Manifest {
      fields: {
        rpc: '<https://api.testnet.solana.com>',
        wallet: NodeWallet { payer: [Keypair] },
        connection: Connection {
          _commitment: 'processed',
          _confirmTransactionInitialTimeout: undefined,
          _rpcEndpoint: '<https://api.testnet.solana.com>',
          _rpcWsEndpoint: 'wss://api.testnet.solana.com',
          _rpcClient: [ClientBrowser],
          _rpcRequest: [Function (anonymous)],
          _rpcBatchRequest: [Function (anonymous)],
          _rpcWebSocket: [RpcWebSocketClient],
          _rpcWebSocketConnected: false,
          _rpcWebSocketHeartbeat: null,
          _rpcWebSocketIdleTimeout: null,
          _rpcWebSocketGeneration: 0,
          _disableBlockhashCaching: false,
          _pollingBlockhash: false,
          _blockhashInfo: [Object],
          _nextClientSubscriptionId: 0,
          _subscriptionDisposeFunctionsByClientSubscriptionId: {},
          _subscriptionHashByClientSubscriptionId: {},
          _subscriptionStateChangeCallbacksByHash: {},
          _subscriptionCallbacksByServerSubscriptionId: {},
          _subscriptionsByHash: {},
          _subscriptionsAutoDisposedByRpc: Set(0) {}
        },
        dexProgram: Program {
          _idl: [Object],
          _provider: [AnchorProvider],
          _programId: [PublicKey [PublicKey(FUfpR31LmcP1VSbz5zDaM7nxnH55iBHkpwusgrnhaFjL)]],
          _coder: [BorshCoder],
          _events: [EventManager],
          rpc: [Object],
          instruction: [Object],
          transaction: [Object],
          account: [Object],
          simulate: [Object],
          methods: [Object],
          state: undefined,
          views: {}
        },
        instrumentsProgram: Program {
          _idl: [Object],
          _provider: [AnchorProvider],
          _programId: [PublicKey [PublicKey(8981bZYszfz1FrFVx7gcUm61RfawMoAHnURuERRJKdkq)]],
          _coder: [BorshCoder],
          _events: [EventManager],
          rpc: [Object],
          instruction: [Object],
          transaction: [Object],
          account: [Object],
          simulate: [Object],
          methods: [Object],
          state: undefined,
          views: {}
        },
        riskProgram: Program {
          _idl: [Object],
          _provider: [AnchorProvider],
          _programId: [PublicKey [PublicKey(92wdgEqyiDKrcbFHoBTg8HxMj932xweRCKaciGSW3uMr)]],
          _coder: [BorshCoder],
          _events: [EventManager],
          rpc: [Object],
          instruction: [Object],
          transaction: [Object],
          account: [Object],
          simulate: [Object],
          methods: [Object],
          state: undefined,
          views: {}
        },
        aaob_id: PublicKey [PublicKey(DchhQ6g8LyRCM5mnao1MAg3g9twfqBbDmUWgpQpFfn1b)] {
          _bn: <BN: bb71964ea07cfe4e351ddb01c484fe05886a6912fcda06cf6ec9aa20bb6282e6>
        },
        dex_id: PublicKey [PublicKey(FUfpR31LmcP1VSbz5zDaM7nxnH55iBHkpwusgrnhaFjL)] {
          _bn: <BN: d71a309c94ab674428d4bd8bf5f07817796e0d5cd09687d8f647f0f0a8799e17>
        },
        fees_id: PublicKey [PublicKey(5AZioCPiC7uZ4zRmkKSg5nsb2A98RhmW89a1pMwiDoeT)] {
          _bn: <BN: 3de1f8e9fd4d9b79d4247fb825b59e32d976a0519ac1907d692eebc81fc1b2a4>
        },
        risk_id: PublicKey [PublicKey(92wdgEqyiDKrcbFHoBTg8HxMj932xweRCKaciGSW3uMr)] {
          _bn: <BN: 775cd966720b5a5e49a9a100312a886e50099cdd62eb251b05665e6bcf05a509>
        },
        mpgs: Map(1) {
          'DDxNzq3A4qKJxnK2PFYeXE1SgGXCR5baaDBhpfLn3LRS' => [Object]
        },
        creationTime: 1676932727763
      }
    }

Selecting our MPG

This tutorial will focus solely on utilizing the BTC-USD MPG.

In the DexExample() function, it is recommended to establish a constant for the BTC-USD MPG publickey.

// BTC-USD Market-Product-Group PubKey
  const MPG = "DDxNzq3A4qKJxnK2PFYeXE1SgGXCR5baaDBhpfLn3LRS"
  const mpgPubkey = new PublicKey(MPG);

Creating your TRG

To create a TRG for the BTC-USD MPG, use the createTrg(marketProductGroup) from *manifest* and pass in mpgPubkey for ****marketProductGroup****, then you can console.log your result.

//Create our TRG for the BTC-USD MPG 
  const trgPubkey = await manifest.createTrg(mpgPubkey);
  console.log("success! trg pubkey:", trgPubkey.toBase58());

When the file is ran, you should see the following:

success! trg pubkey: CUNNMSSRVFu82wXSjkrdhZFPKwKmAf7uLoxLHDRgVCnN

Nicely done! You just created your first TRG for the BTC-USD MPG. The following section provides an overview of how to use the TRG trader account to deposit capital into the account, trade products inside of the MPG, cancel positions, and more.

The following walkthrough provides a step-by-step guides on how to use TRGs to interact with Dexterity

Using TRG

Depositing into TRGs

Create a new file called **fundingTrg.ts** where you will build your own function to deposit and withdraw from a TRG.

Copy and paste the code below into your new file:

import { clusterApiUrl, Keypair, PublicKey } from "@solana/web3.js";
import { Wallet } from "@project-serum/anchor";
import dexterityTs from "@hxronetwork/dexterity-ts";
const dexterity = dexterityTs;
import bs58 from 'bs58'

// Solana Testnet RPC for connection, which can be used later to get your manifest
const CLUSTER_NAME = "testnet";
const rpc = clusterApiUrl(CLUSTER_NAME);
// or your own RPC URL // const rpc = "https://your-own-rpc.com"

// Setting up our wallet with our Private Key so that we can sign transactions and create our trg account from it
const priv_key = bs58.decode("YOUR-PRIVATE_KEY")
const keypair = Keypair.fromSecretKey(
    priv_key
);

// From the keypair we can pass it to the Wallet() method to then be able to pass it in getManifest
const wallet = new Wallet(keypair);

const fundingTRG = async () => {

    // Get the latest manifest
    const manifest = await dexterity.getManifest(rpc, false, wallet);

    // BTC-USD Market-Product-Group PubKey
    const MPG = new PublicKey("DDxNzq3A4qKJxnK2PFYeXE1SgGXCR5baaDBhpfLn3LRS")
    // Our TRG for the BTC-USD MPG 
    const trgPubkey = new PublicKey("YOUR-TRG");

    console.log(
        `Wallet: ${wallet.publicKey.toBase58()} TRG: ${trgPubkey.toBase58()}`
    );
}

fundingTRG()

To interact with dexterity and the products inside your MPG using TRG, a trader instance is needed. To create one, use the trader method from dexterity and pass in your trgPubkey and manifest.

 const trader = new dexterity.Trader(manifest, trgPubkey);

Next, create a viewAccount() function that allows you to view the cash balance of your TRG account. This will help you to know how much is in the account and enable you to withdraw and deposit funds as needed. This is achieved by using the getNetCash() method from your trader instance:

// View Cash amount in TRG account
    const viewAccount = async() => {
        console.log(
          "Net Cash:",
          trader.getNetCash().toString(),
        );
    };

Next, to obtain updated account information, connect to your trader account. To do this, use the connect method from your trader instance. The connect method allows you to pass in a function as the first argument to get a perpetual update stream, meaning that it will call your function perpetually. Alternatively, you can pass in a function as the second argument to get an account update only once. In this case, you are going to pass in your viewAccount() function to only output your cash balance once when you call the account() function.

// Connect to the trader & get updated account cash balance
    const account = async() => await trader.connect(NaN, viewAccount)

    await account()

To proceed, you will need Testnet Hxro, UXDC

💡 UXDC is an SPL token issued by Hxro Network that is used to test Dexterity Contracts on the Solana Testnet & Devnet

UXDC Faucet: https://uxdc-faucet-api-1srh.vercel.app/

After obtaining UXDC (remember to also have Testnet SOL for transaction fees), create a function to deposit and withdraw UXDC from your TRG

To use the deposit and withdraw methods from the trader instance, pass in a Fractional type from your dexterity instance. To create a Fractional, use **dexterity.Fractional.New(amount: number, exponent: number)**

❓ The Fractional method from the dexterity instance creates a new fractional value with a specified numerator and denominator. It is used to represent numbers with fractional values, such as decimals or percentages, and allows for precise calculations with those values.

    const deposit_or_withdraw = async (type: string, amount: number) => {

        if (type === 'd') {
            try {
                console.log(`Depositing ${amount} UXDC...`);
                await trader.deposit(dexterity.Fractional.New(amount, 0));
                console.log(`Successfully Deposited ${amount} UXDC`)
                await account()
            } catch (error) {
                console.log(error)
            }
        } else if (type === 'w') {
            try {
                console.log(`Withdrawing ${amount} UXDC...`);
                await trader.withdraw(dexterity.Fractional.New(amount, 0));
                console.log(`Successfully Withdrawn ${amount} UXDC`) 
                await account()
            } catch (error) {
                console.log(error)
            }
        } else {
            console.log('Error: Funding Type not valid; Valid Types: \\n1. \\'d\\' => Deposit\\n2. \\'w\\' => Withdraw')
            return
        }
    }

    deposit_or_withdraw('d', 5000) // Deposit 5,000 UXDC into our TRG
    // deposit_or_withdraw('w', 5000) // Withdraw 5,000 UXDC from our TRG
}

fundingTRG()

You can now run the code and observe that it deposited 5,000 UXDC from your wallet account to your TRG account. After that, you can comment out the deposit function and uncomment the withdraw function. Running the code again will withdraw the UXDC from your TRG account and transfer it back to your test wallet account.

Placing Limit Orders

Create a new file called **limitOrder.ts** which will be used to build your own function to create and place limit orders

You can copy and paste the code below into your new file:

import { clusterApiUrl, Keypair, PublicKey } from "@solana/web3.js";
import { Wallet } from "@project-serum/anchor";
import dexterityTs from "@hxronetwork/dexterity-ts";
const dexterity = dexterityTs;
import bs58 from 'bs58'

// Solana Testnet RPC for connection, we can use this later to get our manifest
const CLUSTER_NAME = "testnet";
const rpc = clusterApiUrl(CLUSTER_NAME);
// or your own testnet RPC URL // const rpc = "https://your-own-rpc.com"

// Setting up our wallet with our Private Key so that we can sign transactions and create our trg account from it
const priv_key = bs58.decode("YOUR-PRIVATE_KEY")
const keypair = Keypair.fromSecretKey(
    priv_key
);

// From the keypair we can pass it to the Wallet() method to then be able to pass it in getManifest
const wallet = new Wallet(keypair);

const accountTRG = async () => {

    // Get the latest manifest
    const manifest = await dexterity.getManifest(rpc, false, wallet);

    // BTC-USD Market-Product-Group PubKey
    const MPG = new PublicKey("DDxNzq3A4qKJxnK2PFYeXE1SgGXCR5baaDBhpfLn3LRS")
    // Our TRG for the BTC-USD MPG 
    const trgPubkey = new PublicKey("YOUR-TRG");

    console.log(
        `Wallet: ${wallet.publicKey.toBase58()} TRG: ${trgPubkey.toBase58()}`
    );
}

accountTRG()

Moving forward, for simplification purposes, you will only use the 'BTCUSD-PERP' perpetual product within your BTCUSD Market Product Group.

const PRODUCT_NAME = 'BTCUSD-PERP';

Create a trader instance that can be used to interact and trade on Dexterity.

const trader = new dexterity.Trader(manifest, trgPubkey);

Create a function that handles updates on the TRG account and logs out the TRGs account cash balance (in USDC) to the console.

const streamAccount = () => {
      console.log(
        'Portfolio Value:',
        trader.getPortfolioValue().toString(),
        'Position Value:',
        trader.getPositionValue().toString(),
        'Net Cash:',
        trader.getNetCash().toString(),
        'PnL:',
        trader.getPnL().toString()
      );
    };

Call the connect() method from trader so updates are streamed.

const account = async () => {
	await trader.connect(NaN, streamAccount);
};

await account()

You are currently iterating over a list of products obtained using the getProducts() method from the trader instance. The objective is to find the index of a particular product you are interested in. In this case, that is the BTCUSD-PERP product.

let perpIndex: any;
    for (const [name, {index, product}] of trader.getProducts()) {
      console.log('saw', name, ' ', index);
      if (name !== PRODUCT_NAME) {
        continue;
      }
      perpIndex = index;
      break;
    }

Now, set up a QUOTE_SIZE constant that is needed to establish the order amount:

		// 1.0000 contracts
    const QUOTE_SIZE = dexterity.Fractional.New(1, 0);
  • QUOTE_SIZE: This constant represents the size of a contract. In this case, it is set to a value of 1.0000 contracts. Therefore, when a user enters a size of 1, they will receive one contract.

Then, set a constant for the price of BTCUSD that will be used to set limit orders:

		const price = 22_000

Note: As of the creation of this walkthrough, to place an order using Dexterity, the limit order price must be within 15% of the mark price, up or down.

Now, use the dexterity.Fractional.New method to convert the price into a fractional value with zero decimal places.

		const dollars = dexterity.Fractional.New(price, 0);

Now, you can finally place an order! Using the newOrder() method from the trader instance, pass in the following arguments:

  • productIndex: number the index of the product for which we want to place an order

  • isBid: boolean determines whether the order is a bid or an offer

    • Bid = Buy

    • Ask = Sell

  • limitPrice: Fractional the value at which the order will be filled

  • maxBaseQty: Fractional the size of the order

Here is an example of a Long:

trader.newOrder(perpIndex, true, dollars, QUOTE_SIZE).then(async () => {
      console.log(`Placed Buy Limit Order at $${dollars}`);
      await account(NaN, streamAccount);
})

Here is an example of a Short:

trader.newOrder(perpIndex, false, dollars, QUOTE_SIZE).then(async () => {
      console.log(`Placed Sell Limit Order at $${dollars}`);
      await account(NaN, streamAccount); 
});

Congratulations! You have just placed your first orders on Dexterity. The following section will show you how to view your account and TRGs open orders more extensively.

View Account Information and Open Orders

Create a new file called **accountTrg.ts** where you will build your own function to view TRG account information and open orders.

You can copy and paste the code below into your new file:

import { clusterApiUrl, Keypair, PublicKey } from "@solana/web3.js";
import { Wallet } from "@project-serum/anchor";
import dexterityTs from "@hxronetwork/dexterity-ts";
const dexterity = dexterityTs;
import bs58 from 'bs58'

// Solana Testnet RPC for connection, this can be used later to get your manifest
const CLUSTER_NAME = "testnet";
const rpc = clusterApiUrl(CLUSTER_NAME);
// or your own RPC URL // const rpc = "https://your-rpc-url.com";

// Setting up our wallet with our Private Key so that we can sign transactions and create our trg account from it
const priv_key = bs58.decode("YOUR-PRIVATE_KEY")
const keypair = Keypair.fromSecretKey(
    priv_key
);

// From the keypair we can pass it to the Wallet() method to then be able to pass it in getManifest
const wallet = new Wallet(keypair);

const accountTRG = async () => {

    // Get the latest manifest
    const manifest = await dexterity.getManifest(rpc, false, wallet);

    // BTC-USD Market-Product-Group PubKey
    const MPG = new PublicKey("DDxNzq3A4qKJxnK2PFYeXE1SgGXCR5baaDBhpfLn3LRS")
    // Our TRG for the BTC-USD MPG
    const trgPubkey = new PublicKey("YOUR-TRG");

    console.log(
        `Wallet: ${wallet.publicKey.toBase58()} TRG: ${trgPubkey.toBase58()}`
    );
		// Array of products
		const PRODUCT_NAME: [any] = ['BTCUSD-PERP     '];

		// Trader instance from TRG
		const trader = new dexterity.Trader(manifest, trgPubkey);
}

accountTRG()

Create a function that handles updates on the TRG account and logs them to the console.

const streamAccount = async () => {}

Within the streamAccount() function, call the getOpenOrders() method of your trader instance and pass in your PRODUCT_NAME array. This returns all the Open Limit Orders of your TRG account.

const streamAccount = async () => {
    // get all the open orders from our TRG trader instance, filtered by products.
    const orders = Promise.all(trader.getOpenOrders(PRODUCT_NAME));
}

Now that you have your orders array, create a function called prettierOrders(). This function formats the open orders inside your orders array to make them more readable and prints them to the console.

    const prettierOrders = async () => {

        // if there are no open orders, return
        if ((await orders).length === 0) {
        console.log('No Open Orders');
        return;
      }

      (await orders).forEach((order, index) => {
        console.log(
          `Index: ${index} | Product: ${order.productName} | Price: $${
            order.price.m
          } | Qty: ${order.qty.m.toNumber() / 10 ** 6} | Type: ${
            order.isBid ? 'Bid':'Ask'
          } | Id: ${order.id.toString()}`
        );
      });
    };
  • index: Index of the order in the orders array.

  • order.productName: Name of the product for which the order was set (e.g. BTCUSD-PERP).

  • order.price.m: Price at which the limit order was placed.

  • order.qty.m: Size quantity for the order.

  • order.isBid: Boolean value indicating if the order was a "bid" or not. If not, it was an "ask."

  • order.id: Number ID of the order. This can be used to obtain more information about the order from Dexterity or even to cancel the order itself.

Call pretierorders() to print out TRGs open orders. Then, print out TRG account information.


    (await prettierOrders()).then(() => console.log(
      '\\nOpen Orders:',
      (await orders).length,
      '\\nPortfolio Value:',
      trader.getPortfolioValue().toString(),
      'Position Value:',
      trader.getPositionValue().toString(),
      'Net Cash:',
      trader.getNetCash().toString(),
      'PnL:',
      trader.getPnL().toString()
    ));
} // Close streamAccount() function

Next, call the streamAccount() function within the trader.connect() method.

const account = async () => {
    await trader.connect(NaN, streamAccount);
  };

await account()

Here is an example of what the output should look like if there are open orders in the target account:

Index: 0 | Product: BTCUSD-PERP | Price: $21978 | Qty: 3 | Type: Bid | Id: 1741276555738520058374982645737685
Index: 1 | Product: BTCUSD-PERP | Price: $21978 | Qty: 3 | Type: Bid | Id: 1741276555738520058374982645737686
Index: 2 | Product: BTCUSD-PERP | Price: $21978 | Qty: 3 | Type: Bid | Id: 1741276555738520058374982645737693
undefined 
Open Orders: 3 
Portfolio Value: 77328.2304096 Position Value: -4842.411644 Net Cash: 82170.6420536 PnL: -171.7695904

BONUS: Cancel All Open Orders

Here you can see how to easily cancel all open limit orders when called


  const cancelOrder = async () => {

    const orders = Promise.all(trader.getOpenOrders(PRODUCT_NAME));

    if ((await orders).length === 0) {
        console.log('CancelAllOrders Failed: Sorry there are no open orders on this account')
        return
    }

    trader.cancelAllOrders(PRODUCT_NAME, true);

    console.log(`Canceled all orders`);
  };

  cancelOrder()

Last updated