Dexterity Typscript Scripts

A recolection of scripts that expose different ways to interact & fetch information from the Dexterity Protocol through the Typescript SDK

Index:

  1. Create TRG, Get TRGs, Deposit, Withdraw, Place Order

  2. Get all MPGs and their Products for each

  3. Orderbook

  4. Mark & Index Price

  5. Account info

  6. Instructions Bundle

Requirements:

Create a typescript project:

mkdir dexterity-project
cd dexterity-project
npm i typescript --save-dev
npx tsc --init

Install Dependencies:

npm install @solana/web3.js @hxronetwork/dexterity-ts @project-serum/anchor

Create a Solana Wallet and export your private/secret-key:

Send yourself some devnet SOL:

Send yourself some devnet UXDC to trade on Devnet:


Create TRG, Get TRGs, Deposit, Withdraw, Place Order:

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

// Configure Solana Devnet connection and relevant constants
const rpc = "your-devnet-rpc-url";
const keypair = Keypair.fromSecretKey(new Uint8Array([])); // Replace with your private key
const wallet = new Wallet(keypair);
const pubkey = 'wallet-pubkey'; // Replace with your wallet public key
const MPG = "BRWNCEzQTm8kvEXHsVVY9jpb1VLbpv9B8mkF43nMLCtu";
const TRG = new PublicKey("your-created-trg"); // Replace with the TRG public key
const PRODUCT_NAME = 'SOLUSD-PERP'; // Replace with the desired product name

/**
 * Creates a Trader Account (TRG) for a specified MPG.
 * Consumes 0.54 SOL in rent, which is refundable upon account closure.
 */
const createTrg = async () => {
    const manifest = await dexterity.getManifest(rpc, true, wallet);
    const trg = await manifest.createTrg(new PublicKey(MPG));
    console.log('\nTRG: ', trg.toBase58());
};

/**
 * Retrieves all Trader Accounts (TRGs) associated with a specified MPG for a given wallet.
 */
const getTrgs = async () => {
    const manifest = await dexterity.getManifest(rpc, true, wallet);
    const trgArr = await manifest.getTRGsOfWallet(new PublicKey(MPG));
    const TRGs = trgArr.map(trg => trg.pubkey);
    console.log(`\nTRGs:\n`, TRGs);
};

getTrgs();

/**
 * Deposits or withdraws a specified amount to/from a Trader Account (TRG).
 */
const depositOrWithdraw = async (amount: number, type: 'deposit' | 'withdraw') => {
    const manifest = await dexterity.getManifest(rpc, true, wallet);
    const trader = new dexterity.Trader(manifest, TRG);
    const n = dexterity.Fractional.New(amount, 0);

    await trader.connect(NaN, async () => {
        console.log(`\nBALANCE: ${Number(trader.getCashBalance()).toLocaleString()} UXDC`);
    });

    const callbacks = { 
        onGettingBlockHashFn: () => {}, 
        onGotBlockHashFn: () => {}, 
        onTxSentFn: (sig: string) => console.log(`\nSUCCESSFUL ${type.toUpperCase()} OF ${amount.toLocaleString()} UXDC\n${sig ? `SIGNATURE: https://solscan.io/tx/${sig}?cluster=devnet` : ''}`),
    };

    if (type === 'deposit') await trader.deposit(n, callbacks);
    if (type === 'withdraw') await trader.withdraw(n);

    await trader.connect(NaN, async () => {
        console.log(`\nBALANCE: ${Number(trader.getCashBalance()).toLocaleString()} USDC`);
    });
};

depositOrWithdraw(1000, 'deposit');
depositOrWithdraw(500, 'withdraw');

/**
 * Places a buy or sell order for a specified product in the MPG.
 */
const placeOrder = async (type: 'buy' | 'sell', price: number, size: number) => {
    const manifest = await dexterity.getManifest(rpc, true, wallet);
    const trader = new dexterity.Trader(manifest, TRG);

    await trader.connect(NaN, async () => {
        console.log(`\nBALANCE: ${trader.getCashBalance()} | OPEN ORDERS: ${(await Promise.all(trader.getOpenOrders([PRODUCT_NAME]))).length} | POSITIONS VALUE: ${trader.getPositionValue()} | PNL: ${trader.getPnL()}`);
    });

    // Find the index of the desired product within the MPG
    let ProductIndex;
    for (const [name, { index }] of trader.getProducts()) {
        if (name.trim() === PRODUCT_NAME.trim()) {
            ProductIndex = index;
            break;
        }
    }

    // Prepare order details
    const QUOTE_SIZE = dexterity.Fractional.New(size, 0);
    const priceFractional = dexterity.Fractional.New(price, 0);

    // Define callback functions for order handling
    const callbacks = {
        onGettingBlockHashFn: () => {},
        onGotBlockHashFn: () => {},
        onTxSentFn: (sig: string) => console.log(`\nSUCCESSFULLY PLACED LIMIT ${type.toUpperCase()} ORDER\nSIGNATURE: https://solscan.io/tx/${sig}?cluster=devnet`)
    };

    // Make sure Mark Prices are always up-to-date for orders to go through
    await trader.updateMarkPrices()

    // Place a limit order based on the specified type (buy or sell)
    if (type === 'buy') {
        await trader.newOrder(ProductIndex, true, priceFractional, QUOTE_SIZE, false {/*true = fillOrKill*/}, null, null, null, null, callbacks); // For a buy order
    } else if (type === 'sell') {
        await trader.newOrder(ProductIndex, false, priceFractional, QUOTE_SIZE, false {/*true = fillOrKill*/}, null, null, null, null, callbacks); // For a sell order
    }
};

placeOrder('buy', 30000, 100);
placeOrder('sell', 30000, 50);

Get all MPGs and Products for each:

Get all the MPGs for dexterity on the selected Solana network (mainnet, devenet or testnet) and each of their products

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

// Replace with your private key
const keypair = Keypair.fromSecretKey(new Uint8Array([]));

// Initialize a wallet instance for Dexterity using the keypair
const wallet = new Wallet(keypair);

// Specify your RPC URL here
const rpc = 'rpc-url';

/**
 * Retrieves and logs information about all Market Product Groups (MPGs) and their associated products.
 * Filters out a specific MPG if necessary.
 */
const getMpgs = async () => {
    // Fetch the manifest from Dexterity which contains market information
    const manifest = await dexterity.getManifest(rpc, true, wallet);

    // Convert the MPG Map to an array for easier processing
    const mpgs = Array.from(manifest.fields.mpgs.values());

    // Iterate through each MPG to access its products
    for (const { pubkey, mpg, orderbooks } of mpgs) {
        // Skip a specific MPG if needed (replace "MPG-PUBKEY-HERE" with the actual public key to skip)
        if (pubkey.toBase58() === "MPG-PUBKEY-HERE") continue;

        // Iterate through each product in the MPG
        for (const [_, { index, product }] of dexterity.Manifest.GetProductsOfMPG(mpg)) {
            // Convert the product data to a more readable format
            const meta = dexterity.productToMeta(product);

            // Log the index and name of each product for debugging and information purposes
            console.log('productIndex: ', index);
            console.log('Name: ', dexterity.bytesToString(meta.name).trim());
        }
    }
};

getMpgs();

Orderbook:

Get Live Orderbook ASK & BID data for a given product in a given MPG

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

// Initialize your keypair from your private/secret key
const keypair = Keypair.fromSecretKey(new Uint8Array([]));
// When working with a UI you need to use the DexterityWallet type
const wallet = new Wallet(keypair);
const rpc =
    'https://devnet.helius-rpc.com/?api-key=';
// Testnet MPG
const mpgPubkey = 'BRWNCEzQTm8kvEXHsVVY9jpb1VLbpv9B8mkF43nMLCtu';
// Desired Product
const productName = 'SOLUSD-PERP';

/**
 * Asynchronously retrieves the order book for a specific product.
 * 
 * - Fetches the manifest containing market information.
 * - Filters to find the desired Market Product Group (MPG).
 * - Iterates through products to find the specific product and its associated market state.
 * - Initializes streaming of order book data for the selected product.
 */
const getOB = async () => {
    // Fetch the manifest containing market information
    const manifest = await dexterity.getManifest(rpc, true, wallet);

    // Convert MPG map to an array for easier processing
    const mpgs = Array.from(manifest.fields.mpgs.values());

    // Find the MPG with the matching public key
    const selectedMPG = mpgs.filter(
        (value) => value.pubkey.toBase58() === mpgPubkey,
    );

    // Initialize variables to store market state and product details
    let MarketState;
    let PRODUCT;
    let productIndex;

    // Loop through each MPG to find the desired product
    for (const { pubkey, mpg, orderbooks } of selectedMPG) {
        for (const [_, { index, product }] of dexterity.Manifest.GetProductsOfMPG(mpg)) {
            // Extract metadata of the product
            const meta = dexterity.productToMeta(product);

            // Check for the specified product name
            if (dexterity.bytesToString(meta.name).trim() === productName.trim()) {
                // Fetch the current state of the order book for the product
                MarketState = await manifest.fetchOrderbook(meta.orderbook);
                productIndex = index;

                // Determine the type of product and assign it to PRODUCT
                if (product.hasOwnProperty('outright')) {
                    PRODUCT = product.outright.outright;
                } else {
                    PRODUCT = product.combo.combo;
                }

                // Logging for debugging purposes
                console.log('productIndex: ', productIndex);
                console.log('Name: ', dexterity.bytesToString(meta.name).trim());

                // Break out of the loop once the desired product is found
                break;
            }
        }
    }

    // Stream order book data for the selected product
    const { asksSocket, bidsSocket, markPricesSocket } = manifest.streamBooks(
        PRODUCT,
        MarketState,
        aggregateBookL2 // Callback function for handling streamed data
    );
};


getOB();

/**
 * Aggregates and processes Level 2 order book data.
 * @param data The incoming order book data, containing asks and bids.
 */
function aggregateBookL2(data: any) {
    
    // Define the structure for cumulative order data
    interface CumulativeOrder {
        price: number;
        ordersSize: number;
    }

    // Check for the presence of both asks and bids
    if (data.asks.length !== 0 && data.bids.length !== 0) {
        const cumulativeBids = new Map<number, CumulativeOrder>();
        const cumulativeAsks = new Map<number, CumulativeOrder>();

        // Process bids
        data.bids.forEach((order: any) => {
            const price = order.price.toNumber();
            const quantity = order.quantity.toDecimal();

            if (cumulativeBids.has(price)) {
                cumulativeBids.get(price)!.ordersSize += quantity;
            } else {
                cumulativeBids.set(price, { price, ordersSize: quantity });
            }
        });

        // Process asks
        data.asks.forEach((offer: any) => {
            const price = offer.price.toNumber();
            const quantity = offer.quantity.toDecimal();

            if (cumulativeAsks.has(price)) {
                cumulativeAsks.get(price)!.ordersSize += quantity;
            } else {
                cumulativeAsks.set(price, { price, ordersSize: quantity });
            }
        });

        // Convert Maps to sorted arrays and limit to top 10
        const bids = Array.from(cumulativeBids.values()).sort((a, b) => b.price - a.price).slice(0, 10);
        const asks = Array.from(cumulativeAsks.values()).sort((a, b) => a.price - b.price).slice(0, 10);

        // Prepare and output the result
        let output = `Order book update received:\n\nASKS:\n`;
        asks.forEach((ask, index) => {
            output += `#${index + 1} QTY: ${ask.ordersSize} | PRICE: $${ask.price}\n`;
        });

        output += '\nBIDS:\n';
        bids.forEach((bid, index) => {
            output += `#${index + 1} QTY: ${bid.ordersSize} | PRICE: $${bid.price}\n`;
        });

        process.stdout.write(output);
        process.stdout.write('\u001b[?25h'); // Show cursor
    } else {
        // Handle missing data cases
        console.log(`Missing ${data.bids.length === 0 && data.asks.length > 0 ? 'BIDS' : data.asks.length === 0 && data.bids.length > 0 ? 'ASKS' : 'BIDS & ASKS'}`);
    }
}

Mark & Index Price:

Fetch for Index & Mark prices for a desired product in a desired MPG

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

// Initialize your keypair from your private/secret key
const keypair = Keypair.fromSecretKey(new Uint8Array([]));
// Initialize a wallet instance for Dexterity using the keypair
const wallet = new Wallet(keypair);
const rpc = 'https://devnet.helius-rpc.com/?api-key=';
// Testnet Market Product Group (MPG) public key
const mpgPubkey = 'BRWNCEzQTm8kvEXHsVVY9jpb1VLbpv9B8mkF43nMLCtu';
// Trader Risk Group (TRG) for the selected MPG
const TRG = new PublicKey("your-created-trg")
// Name of the desired product
const productName = 'SOLUSD-PERP';
// Uninitialized account public key
const UNINITIALIZED = new PublicKey('11111111111111111111111111111111');

/**
 * Periodically updates the market prices for products in a specified MPG.
 * - Initializes the Dexterity manifest.
 * - Creates a trader instance and repeatedly updates market prices.
 */
const updatePrices = async () => {
    // Initialize the Dexterity manifest with the RPC endpoint and wallet
    const manifest = await dexterity.getManifest(rpc, true, wallet);

    // Create a trader instance for the specified MPG
    const trader = new dexterity.Trader(manifest, TRG);

    // Set an interval to periodically update market prices
    setInterval(async () => {
        // Update the mark prices for all products in the MPG
        await trader.updateMarkPrices();

        // Iterate through all products in the MPG
        for (const [productName, obj] of dexterity.Manifest.GetProductsOfMPG(trader.mpg)) {
            const { product } = obj;

            // Skip if the product name is empty
            if (!productName.trim()) {
                continue;
            }

            // Get metadata of the product
            const meta = dexterity.productToMeta(product);

            // Skip if the product key is uninitialized
            if (meta.productKey.equals(UNINITIALIZED)) {
                continue;
            }

            // Skip if the product is of type 'combo'
            if (product.combo?.combo) {
                continue;
            }

            // Update mark prices again (consider if this is necessary)
            await trader.updateMarkPrices();

            // Fetch and log the index and mark prices for the product
            const index = Number(dexterity.Manifest.GetIndexPrice(trader.markPrices, meta.productKey));
            const mark = Number(dexterity.Manifest.GetMarkPrice(trader.markPrices, meta.productKey));
            console.log({ index, mark });
        }
    }, 500); // The interval is set to 500 milliseconds
};

Account info:

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

// Solana RPC URL for connecting to the blockchain
const rpc = 'https://example-rpc.com';

// Setting up our wallet using a private key to sign transactions and interact with the blockchain
const keypair = Keypair.fromSecretKey(new Uint8Array([]));
const wallet = new Wallet(keypair);

/**
 * View account information and open orders for a specified trading group (TRG) in Dexterity.
 */
const viewAccount = async () => {
  // Fetch the latest manifest from Dexterity
  const manifest = await dexterity.getManifest(rpc, false, wallet);

  // Specify the Market-Product-Group (MPG) public key
  const MPG = new PublicKey('BRWNCEzQTm8kvEXHsVVY9jpb1VLbpv9B8mkF43nMLCtu');
  const trgPubkey = new PublicKey('your-trg'); // Replace with actual TRG public key

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

  // Define the product name to filter orders
  const PRODUCT_NAME = ['SOLUSD-PERP']; // Array of product names

  // Create a trader instance to interact with Dexterity
  const trader = new dexterity.Trader(manifest, trgPubkey);

  // Function to stream and display account and order information
  const streamAccount = async () => {
    // Retrieve all open orders for specified products
    const orders = await Promise.all(trader.getOpenOrders(PRODUCT_NAME));

    // Format and display open orders
    if (orders.length === 0) {
      console.log('No Open Orders');
    } else {
      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()}`);
      });
    }

    // Display account information
    console.log('\nOpen Orders:', orders.length, '\nPortfolio Value:', trader.getPortfolioValue().toString(), 'Position Value:', trader.getPositionValue().toString(), 'Net Cash:', trader.getNetCash().toString(), 'PnL:', trader.getPnL().toString());
  };

  // Connect to the trader account and stream account information
  await trader.connect(streamAccount, NaN);
};

viewAccount();

Instructions Bundles

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

// Solana RPC URL for connecting to the blockchain
const rpc = 'https://example-rpc.com';

// Set up the wallet using a private key to sign transactions
const keypair = Keypair.fromSecretKey(new Uint8Array([])); // Replace with actual private key
const wallet = new Wallet(keypair);

// Initialize constants like the Trader Account (TRG) and Product Index
const TRG = new PublicKey(''); // Replace with actual TRG public key
const PRODUCT_INDEX = 0; // Index for 'SOLUSD-PERP' or similar product

/**
 * Demonstrates how to efficiently submit multiple instructions as a bundle.
 */
const submitIxBundle = async () => {
    // Fetch the latest manifest from Dexterity
    const manifest = await dexterity.getManifest(rpc, true, wallet);

    // Create a trader instance to interact with Dexterity
    const trader = new dexterity.Trader(manifest, TRG);

    // Connect to the trader account and log account details
    await trader.connect(NaN, async () => {
        console.log(`\nBALANCE: ${trader.getCashBalance()} | OPEN ORDERS: ${(await Promise.all(trader.getOpenOrders([PRODUCT_NAME]))).length} | POSITIONS VALUE: ${trader.getPositionValue()} | PNL: ${trader.getPnL()}`);
    });

    // Prepare order details using fractional values
    const sizeFractional = dexterity.Fractional.New(1, 0); // Replace with desired size
    const priceFractional = dexterity.Fractional.New(65, 0); // Replace with desired price

    // Define callback functions for handling order transactions
    const callbacks = {
        onGettingBlockHashFn: () => {},
        onGotBlockHashFn: () => {},
        onTxSentFn: (sig: string) => console.log(`SIGNATURE: https://solscan.io/tx/${sig}?cluster=devnet`)
    };

    // Generate the instruction for updating market prices
    const products = Array.from(dexterity.Manifest.GetProductsOfMPG(trader.mpg));
    const updateMarkIx = trader.getUpdateMarkPricesIx(products);

    // Generate the instruction for a new limit order
    const orderIx = trader.getNewOrderIx(PRODUCT_INDEX, false, priceFractional, sizeFractional, false, null, null, null, null);

    // Submit the bundled instructions
    await trader.sendTx([updateMarkIx, orderIx], callbacks);
};

submitIxBundle();

Last updated