15 releases

0.4.4 Jan 19, 2024
0.4.3 Jan 18, 2024
0.4.0 Nov 21, 2023
0.3.13 Oct 13, 2023
0.1.0 Feb 11, 2023

#7 in #bonfida

23 downloads per month
Used in bonfida-cli

MIT and LGPL-3.0

44KB
1K SLoC

Bonfida utils




Table of contents


  1. Introduction
  2. Installation
  3. Used by
  4. Check functions
  5. FP32 and FP64 math functions
  6. InstructionsAccount trait
  7. BorshSize trait
  8. Project structure
  9. Example

Introduction


This repo is a collection of different utilities in use across various Bonfida projects. The repository has the following structure:

  • utils : Main bonfida-utils utilities library
  • autobindings : CLI command to autogenerate Typescript or Python bindings for smart contracts written in the specific Bonfida style
  • autoproject : CLI command to autogenerate an extensive template smart contract
  • autodoc: CLI command to generate a documented instruction.rs file
  • macros : Auxiliary crate containing macros in use by the main bonfida-utils library
  • cli : CLI entrypoint for all tools

Used by



Installation


This repository is published on crates.io, in order to use it in your Solana programs add this to your Cargo.toml file

bonfida-utils = "0.1"

Install the main bonfida cli

git clone https://github.com/Bonfida/bonfida-utils.git
cd bonfida-utils
cargo install --path cli

To automatically generate Javascript or Python bindings

cd /path/to/your/project
bonfida autobindings --help

To automatically generate a template smart contract

cd /path/to/project/parent
bonfida autoproject project-name

To automatically generate a documented instruction.rs:

cd /path/to/your/project
bonfida autodoc

In order to generate instruction bindings automatically your project needs to follow a certain structure and derive certain traits that are detailed in the following sections.


Check functions


bonfida-utils contains safety verification functions:

  • check_account_key
  • check_account_owner
  • check_signer

FP32 and FP64 math functions


bonfida-utils contains some useful math functions for FP32 and FP64:

  • fp32_div
  • fp32_mul
  • ifp32_div
  • ifp32_mul
  • fp64_div
  • fp64_mul

InstructionsAccount


The Accounts struct needs to derive the InstructionsAccount trait in order to automatically generate bindings in Rust and JS. In order to know which accounts are writable and/or signer you will have to specify constraints (cons) for each account of the struct:

  1. For writable accounts: #[cons(writable)]
  2. For signer accounts: #[cons(signer)]
  3. For signer and writable accounts: #[cons(signer, writable)]

For example

use bonfida_utils::{InstructionsAccount};

#[derive(InstructionsAccount)]
pub struct Accounts<'a, T> {

    pub read_only_account: &'a T, // Read only account

    #[cons(writable)] // This specifies that the account is writable
    pub writable_account: &'a T,

    #[cons(signer)] // This specifies that the account is sginer
    pub signer_account: &'a T,

    // Write the d
    #[cons(signer, writable)]  // This specifies that the account is sginer and writable
    pub signer_and_writable_account: &'a T,
}

To specify accounts that are optional

pub struct Accounts<'a, T> {
    // Writable account that is optional
    #[cons(writable)]
    pub referrer_account_opt: Option<&'a T>,
}

BorshSize


The struct used for the data of the instruction needs to derive the BorshSize trait, for example let's take the following struct

#[derive(BorshSerialize, BorshDeserialize, BorshSize)]
pub struct Params {
    pub position_type: PositionType,
    pub market_index: u16,
    pub max_base_qty: u64,
    pub max_quote_qty: u64,
    pub limit_price: u64,
    pub match_limit: u64,
    pub self_trade_behavior: u8,
    pub order_type: OrderType,
    pub number_of_markets: u8,
}

In the above example, BorshSize should be derived for PositionType and OrderType as well. The derive macro can take care of this for field-less enums :

#[derive(BorshSerialize, BorshDeserialize, BorshSize)]
pub enum OrderType {
  Limit,
  ImmediateOrCancel,
  FillOrKill,
  PostOnly
}

You might need to implement BorshSize yourself for certain types (e.g an enum with variants containing fields) :

#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq, FromPrimitive)]
pub enum ExampleEnum {
    FirstVariant,
    SecondVariant(u128),
}

impl BorshSize for ExampleEnum {
    fn borsh_len(&self) -> usize {
        match self {
          Self::FirstVariant => 1,
          Self::SecondVariant(n) => 1 + n.borsh_len()
        }
    }
}

Project structure


🚨 The project structure is important:

  1. The Solana program must be in a folder called program
  2. The JS bindings must be in a folder called js
  3. The processor folder needs to contain instructions' logic in separate files. The name of each file needs to be snake case and match the name of the associated function in instructions.rs
  4. The instruction enum of instructions.rs needs to have Pascal case names that match the snake case names of the files in processor. This is detailed in the example below.
  5. The instruction enum of instructions.rs needs to be the first enum to be defined in that file.

Examples


Let's have a look at a real life example.

The project structure is as follow

├── program
│   ├── instructions.rs
│   ├── processor
│   │   ├── create_market.rs
├── js
    ├── src
├── python
    ├── src
│...... The rest is omitted

To simplify we will consider only one instruction create_market.rs and only focus on processor and instructions.rs.

We can see from the project structure that create_market.rs is located in the processor directory, let's have a look at the contents of the file:

//! Creates a perp market
// The sentence above will be used to describe the instruction in the auto generated doc ⚠️
use bonfida_utils::{checks::check_signer, BorshSize, InstructionsAccount};
use borsh::{BorshDeserialize, BorshSerialize};
// Other imports are omitted

#[derive(InstructionsAccount)]
pub struct Accounts<'a, T> {
    /// The market address
    #[cons(writable)] // This specifies that the account is writable
    pub market: &'a T,

    /// The ecosystem address
    #[cons(writable)]
    pub ecosystem: &'a T,

    /// The address of the Serum Core market
    pub aob_orderbook: &'a T,

    /// The address of the Serum Core event queue
    pub aob_event_queue: &'a T,

    /// The address of the Serum Core asks slab
    pub aob_asks: &'a T,

    /// The address of the Serum Core bids slab
    pub aob_bids: &'a T,

    /// The program ID of Serum Core
    pub aob_program: &'a T,

    /// The Pyth oracle address for this market
    pub oracle: &'a T,

    /// The market admin address
    #[cons(signer)] // This specifies that the account is signer
    pub admin: &'a T,

    /// The market vault address
    pub vault: &'a T,
}

// Constraints (cons) can be combined e.g
// #[cons(signer, writable)]
// pub some_account: &'a T
//
// Optional accounts are supported as well
// pub discount_account_opt: Option<&'a T>

// BorshSize might require custom impl e.g for enum
#[derive(BorshSerialize, BorshDeserialize, BorshSize)]
pub struct Params {
    pub market_symbol: String,
    pub signer_nonce: u8,
    pub coin_decimals: u8,
    pub quote_decimals: u8,
}

impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
    pub fn parse(accounts: &'a [AccountInfo<'b>]) -> Result<Self, ProgramError> {
        let accounts_iter = &mut accounts.iter();

        let a = Accounts {
            market: next_account_info(accounts_iter)?,
            ecosystem: next_account_info(accounts_iter)?,
            aob_orderbook: next_account_info(accounts_iter)?,
            aob_event_queue: next_account_info(accounts_iter)?,
            aob_asks: next_account_info(accounts_iter)?,
            aob_bids: next_account_info(accounts_iter)?,
            aob_program: next_account_info(accounts_iter)?,
            oracle: next_account_info(accounts_iter)?,
            admin: next_account_info(accounts_iter)?,
            vault: next_account_info(accounts_iter)?,
        };

        // Account checks are omitted

        Ok(a)
    }
}

pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], params: Params) -> ProgramResult {
    let accounts = Accounts::parse(accounts)?;

    let Params {
        market_symbol,
        signer_nonce,
        coin_decimals,
        quote_decimals,
    } = params;

    // Instruction logic is omitted

    Ok(())
}

We can see that create_market.rs contains two important definitions:

  1. Accounts struct: its InstructionsAccount trait implementation should be derived, and the constraints for each account of the struct should be specified
  2. Params struct: its BorshSize trait implementation should be derived

The instruction.rs file can be autogenerated using cargo autodoc

use bonfida_utils::InstructionsAccount;
// Other imports are omitted

#[derive(BorshSerialize, BorshDeserialize)]
pub enum PerpInstruction {
    /// Creates a new perps market
    ///
    /// | Index | Writable | Signer | Description                               |
    /// | --------------------------------------------------------------------- |
    /// | 0     | ✅        | ❌      | The market address                        |
    /// | 1     | ✅        | ❌      | The ecosystem address                     |
    /// | 2     | ❌        | ❌      | The address of the Serum Core market      |
    /// | 3     | ❌        | ❌      | The address of the Serum Core event queue |
    /// | 4     | ❌        | ❌      | The address of the Serum Core asks slab   |
    /// | 5     | ❌        | ❌      | The address of the Serum Core bids slab   |
    /// | 6     | ❌        | ❌      | The program ID of Serum Core              |
    /// | 7     | ❌        | ❌      | The Pyth oracle address for this market   |
    /// | 8     | ❌        | ✅      | The market admin address                  |
    /// | 9     | ❌        | ❌      | The market vault address                  |
    CreateMarket,
    ///
    /// ...
}
pub fn create_market(
    accounts: create_market::Accounts<Pubkey>,
    params: create_market::Params,
) -> Instruction {
    accounts.get_instruction(crate::ID, PerpInstruction::CreateMarket as u8, params)
}

In order to generate Javascript instruction bindings run

cargo autobindings

This will generate a file named raw_instructions.ts that contains all the instructions of your program

// This file is auto-generated. DO NOT EDIT
import BN from "bn.js";
import { Schema, serialize } from "borsh";
import { PublicKey, TransactionInstruction } from "@solana/web3.js";

export interface AccountKey {
  pubkey: PublicKey;
  isSigner: boolean;
  isWritable: boolean;
}

export class createMarketInstruction {
  tag: number;
  marketSymbol: string;
  signerNonce: number;
  coinDecimals: number;
  quoteDecimals: number;
  static schema: Schema = new Map([
    [
      createMarketInstruction,
      {
        kind: "struct",
        fields: [
          ["tag", "u8"],
          ["marketSymbol", "string"],
          ["signerNonce", "u8"],
          ["coinDecimals", "u8"],
          ["quoteDecimals", "u8"],
        ],
      },
    ],
  ]);
  constructor(obj: {
    marketSymbol: string,
    signerNonce: number,
    coinDecimals: number,
    quoteDecimals: number,
  }) {
    this.tag = 0;
    this.marketSymbol = obj.marketSymbol;
    this.signerNonce = obj.signerNonce;
    this.coinDecimals = obj.coinDecimals;
    this.quoteDecimals = obj.quoteDecimals;
  }
  serialize(): Uint8Array {
    return serialize(createMarketInstruction.schema, this);
  }
  getInstruction(
    programId: PublicKey,
    market: PublicKey,
    ecosystem: PublicKey,
    aobOrderbook: PublicKey,
    aobEventQueue: PublicKey,
    aobAsks: PublicKey,
    aobBids: PublicKey,
    aobProgram: PublicKey,
    oracle: PublicKey,
    admin: PublicKey,
    vault: PublicKey
  ): TransactionInstruction {
    const data = Buffer.from(this.serialize());
    let keys: AccountKey[] = [];
    keys.push({
      pubkey: market,
      isSigner: false,
      isWritable: true,
    });
    keys.push({
      pubkey: ecosystem,
      isSigner: false,
      isWritable: true,
    });
    keys.push({
      pubkey: aobOrderbook,
      isSigner: false,
      isWritable: false,
    });
    keys.push({
      pubkey: aobEventQueue,
      isSigner: false,
      isWritable: false,
    });
    keys.push({
      pubkey: aobAsks,
      isSigner: false,
      isWritable: false,
    });
    keys.push({
      pubkey: aobBids,
      isSigner: false,
      isWritable: false,
    });
    keys.push({
      pubkey: aobProgram,
      isSigner: false,
      isWritable: false,
    });
    keys.push({
      pubkey: oracle,
      isSigner: false,
      isWritable: false,
    });
    keys.push({
      pubkey: admin,
      isSigner: true,
      isWritable: false,
    });
    keys.push({
      pubkey: vault,
      isSigner: false,
      isWritable: false,
    });
    return new TransactionInstruction({
      keys,
      programId,
      data,
    });
  }
}

To generate Python bindings run

bonfida autobindings --target-language py

To run the autobindings tests you have to:

  • Regenerate the js and python bindings to be sure they are up to date
  • Run yarnin the js folder
  • Install ts-node with: sudo npm install -g ts-node typescript '@types/node'
  • From the programfolder, run bonfida autobindings --test true

Dependencies

~19–30MB
~487K SLoC