172 releases (75 stable)

new 2.0.60 Oct 28, 2025
2.0.52 Sep 30, 2025
2.0.46 Jul 24, 2025
2.0.25 Mar 27, 2025
0.2.19 Mar 31, 2024

#612 in Magic Beans

Download history 810/week @ 2025-07-07 79/week @ 2025-07-14 216/week @ 2025-07-21 128/week @ 2025-07-28 440/week @ 2025-08-04 173/week @ 2025-08-11 59/week @ 2025-08-18 296/week @ 2025-08-25 172/week @ 2025-09-01 68/week @ 2025-09-08 74/week @ 2025-09-15 54/week @ 2025-09-22 285/week @ 2025-09-29 414/week @ 2025-10-06 421/week @ 2025-10-13 214/week @ 2025-10-20

1,334 downloads per month
Used in etherfuse-arb

Apache-2.0

1.5MB
31K SLoC

Contains (ELF lib, 1.5MB) tests/fixtures/spl_token_2022.so, (ELF lib, 795KB) tests/fixtures/mpl_token_metadata.so, (ELF lib, 135KB) tests/fixtures/spl_token.so, (ELF lib, 105KB) spl_associated_token_account.so

Rust client

A generated Rust library for the Stablebond program.

The stablebond-sdk provides a set of methods to interact with the Stablebond program on the Solana blockchain.

We're currently using this ourselves at app.etherfuse.com.

We also have a devnet version of the program running at devnet.etherfuse.com. This is useful for testing and development, reach out to us for play-money and we'll transfer you some fake tokens to use.


lib.rs:

To get started quickly with the stablebond SDK, you can use the following methods to quickly build transactions for the fundamentals of etherfuse stablebonds.

Including

  • Purchasing a bond
  • Requesting a redemption
  • Redeeming a bond
  • Create a purchase order
  • Redeeming a purchase order

Examples

use crate::{
    accounts::{Bond, BondMultisigMeta, PaymentFeed},
    find_bond_multisig_meta_pda, find_bond_pda, find_config_pda, find_issuance_pda, find_kyc_pda,
    find_nft_issuance_vault_pda, find_payment_feed_pda, find_payment_pda, find_payout_pda,
    find_purchase_order_pda, CreatePurchaseOrderV2, CreatePurchaseOrderV2InstructionArgs,
    PurchaseBondV2, PurchaseBondV2InstructionArgs, RedeemBond, RedeemPurchaseOrder,
    RequestRedemptionV2, RequestRedemptionV2InstructionArgs,
};
use anyhow::Result;
use mpl_token_metadata::accounts::{MasterEdition, Metadata};
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_sdk::{message::Message, signature::Keypair};
use solana_sdk::{pubkey::Pubkey, signer::Signer};
use solana_sdk::{system_program, transaction::Transaction};
use spl_associated_token_account::{
    get_associated_token_address, get_associated_token_address_with_program_id,
};

// Creates a purchase bond transaction that can be sent to solana

// The user_wallet will have already needed to complete the KYC process for the transaction to be successful.
// See [API Reference](https://docs.etherfuse.com/api-reference/onboarding/generate-onboarding-url) for more information.


let client = RpcClient::new(rpc_url);
// Some user wallet keypair
let user_wallet = Keypair::from_str("user_wallet").unwrap();
// Some mint account
let mint = Pubkey::from_str("mint").unwrap();
// Get an un-signed transaction
let tx = make_purchase_bond_instruction(1000000, user_wallet, client, mint).await?;
// Sign the transaction with the users keypair and then...
// Submit the transaction
let sig = client.send_and_confirm_transaction(&tx).await?;

pub async fn make_purchase_bond_transaction(
    client: &RpcClient,
    amount: u64,
    user_wallet: Pubkey,
    mint: Pubkey,
) -> Result<Transaction> {
    let ix_args = PurchaseBondV2InstructionArgs { amount };
    let addresses = generate_buy_addresses(client, mint, user_wallet).await?;
    let mint_auth_multisig_account = get_bond_mint_auth_multisig_if_needed(&client, mint).await?;
    let ix = PurchaseBondV2 {
        kyc_account: addresses.kyc_account,
        user_wallet: user_wallet,
        user_token_account: addresses.user_token_account,
        user_payment_token_account: addresses.user_payment_token_account,
        payment_account: addresses.payment_account,
        payment_token_account: addresses.payment_token_account,
        payment_mint_account: addresses.payment_mint_account,
        payment_feed_account: addresses.payment_feed_account,
        payment_base_price_feed_account: addresses.payment_price_feed_account,
        payment_quote_price_feed_account: addresses.payment_quote_price_feed_account,
        mint_auth_multisig_account,
        bond_account: addresses.bond_account,
        issuance_account: addresses.issuance_account,
        mint_account: mint,
        token2022_program: addresses.token_2022_program,
        associated_token_program: addresses.associated_token_program,
        token_program: addresses.token_program,
        system_program: addresses.system_program,
    }
    .instruction(ix_args);

    let tx = Transaction::new_unsigned(Message::new(&[ix], Some(&user_wallet)));

    Ok(tx)
}

// Creates a transaction to begin the bond redemption process


let client = RpcClient::new(rpc_url);
// Some user wallet keypair
let user_wallet = Keypair::from_str("user_wallet").unwrap();
// Some mint account
let mint = Pubkey::from_str("mint").unwrap();
// Some NFT mint account
let nft_mint = Pubkey::from_str("nft_mint").unwrap();
// Get an un-signed transaction
let tx = make_request_redemption_instruction(1000000, user_wallet, mint, nft_mint)?;
// Sign the transaction with the users keypair and then...
// Submit the transaction
let sig = client.send_and_confirm_transaction(&tx).await?;

pub async fn make_request_redemption_transaction(
    client: &RpcClient,
    amount_in_tokens: u64,
    user_wallet: Pubkey,
    mint: Pubkey,
) -> Result<Transaction> {
    let ix_args = RequestRedemptionV2InstructionArgs {
        amount: amount_in_tokens,
    };
    let addresses = generate_buy_addresses(client, mint, user_wallet).await?;
    let nft_mint_pair = Keypair::new();
    let nft_mint = nft_mint_pair.pubkey();
    let nft_issuance_vault_account = find_nft_issuance_vault_pda(nft_mint).0;
    let nft_issuance_vault_token_account = get_associated_token_address_with_program_id(
        &nft_issuance_vault_account,
        &mint,
        &spl_token_2022::id(),
    );
    let user_nft_token_account = get_associated_token_address(&user_wallet, &nft_mint);
    let nft_metadata_account = Metadata::find_pda(&nft_mint).0;
    let nft_master_edition_account = MasterEdition::find_pda(&nft_mint).0;
    let nft_collection_mint = find_config_pda().0;

    let nft_collection_metadata_account = Metadata::find_pda(&nft_collection_mint).0;
    let nft_collection_master_edition_account = MasterEdition::find_pda(&nft_collection_mint).0;

    let ix = RequestRedemptionV2 {
        kyc_account: find_kyc_pda(user_wallet).0,
        user_wallet,
        config_account: find_config_pda().0,
        token2022_program: addresses.token_2022_program,
        associated_token_program: addresses.associated_token_program,
        token_program: addresses.token_program,
        system_program: addresses.system_program,
        metadata_program: mpl_token_metadata::ID,
        sysvar_instructions: solana_sdk::sysvar::instructions::id(),
        bond_account: addresses.bond_account,
        issuance_account: addresses.issuance_account,
        mint_account: mint,
        user_token_account: addresses.user_token_account,
        nft_mint_account: nft_mint,
        nft_issuance_vault_account,
        nft_issuance_vault_token_account,
        user_nft_token_account,
        nft_metadata_account,
        nft_master_edition_account,
        nft_collection_mint,
        nft_collection_metadata_account,
        nft_collection_master_edition_account,
    }
    .instruction(ix_args);

    Ok(Transaction::new_unsigned(Message::new(
        &[ix],
        Some(&user_wallet),
    )))
}

// Creates a transaction to redeem a bond once payout is available


let client = RpcClient::new(rpc_url);
// Some user wallet keypair
let user_wallet = Keypair::from_str("user_wallet").unwrap();
// The mint for the bond underlying the NFT
let mint = Pubkey::from_str("mint").unwrap();
// The addres of the NFT
let nft_mint = Pubkey::from_str("nft_mint").unwrap();
// Get an un-signed transaction
let tx = make_redeem_bond_transaction(client, user_wallet, mint, nft_mint).await?;
// Sign the transaction with the users keypair and then...
// Submit the transaction
let sig = client.send_and_confirm_transaction(&tx).await?;

pub async fn make_redeem_bond_transaction(
    client: &RpcClient,
    user_wallet: Pubkey,
    mint: Pubkey,
    nft_mint: Pubkey,
) -> Result<Transaction> {
    let bond_account = find_bond_pda(mint).0;
    let data = client.get_account_data(&bond_account).await?;
    let bond = Bond::from_bytes(&data).unwrap();
    let payment_feed_account = find_payment_feed_pda(bond.payment_feed_type).0;
    let issuance_account = find_issuance_pda(bond_account, bond.issuance_number).0;
    let payment_mint_account = find_payment_pda(issuance_account).0;
    let nft_issuance_vault_account = find_nft_issuance_vault_pda(nft_mint).0;
    let nft_issuance_vault_token_account = get_associated_token_address_with_program_id(
        &nft_issuance_vault_account,
        &mint,
        &spl_token_2022::id(),
    );
    let user_nft_token_account = get_associated_token_address(&user_wallet, &nft_mint);
    let user_payment_token_account =
        get_associated_token_address(&user_wallet, &payment_mint_account);
    let nft_metadata_account = Metadata::find_pda(&nft_mint).0;
    let nft_master_edition_account = MasterEdition::find_pda(&nft_mint).0;
    let nft_collection_metadata_account = Metadata::find_pda(&nft_mint).0;
    let payout_account = find_payout_pda(issuance_account).0;
    let payout_token_account = get_associated_token_address(&payout_account, &mint);
    let token2022_program = spl_token_2022::id();
    let associated_token_program = spl_associated_token_account::id();
    let token_program = spl_token::id();
    let metadata_program = mpl_token_metadata::ID;
    let system_program = system_program::id();
    let ix = RedeemBond {
        bond_account,
        mint_account: mint,
        issuance_account,
        user_wallet,
        user_nft_token_account,
        user_payment_token_account,
        payment_mint_account,
        payment_feed_account,
        nft_mint_account: nft_mint,
        nft_metadata_account,
        nft_master_edition_account,
        nft_collection_metadata_account,
        nft_issuance_vault_account,
        nft_issuance_vault_token_account,
        payout_account,
        payout_token_account,
        token2022_program,
        associated_token_program,
        token_program,
        metadata_program,
        system_program,
    }
    .instruction();

    Ok(Transaction::new_unsigned(Message::new(
        &[ix],
        Some(&user_wallet),
    )))
}

// Creates a transaction to redeem a purchase order


let client = RpcClient::new(rpc_url);
// Some user wallet keypair
let user_wallet = Keypair::from_str("user_wallet").unwrap();
// The mint for the bond underlying the NFT
let mint = Pubkey::from_str("mint").unwrap();
// The address of the NFT
let nft_mint = Pubkey::from_str("nft_mint").unwrap();
// Get an un-signed transaction
let tx = make_redeem_purchase_order_transaction(client, user_wallet, mint, nft_mint).await?;
// Sign the transaction with the users keypair and then...
// Submit the transaction
let sig = client.send_and_confirm_transaction(&tx).await?;

pub async fn make_redeem_purchase_order_transaction(
    client: &RpcClient,
    user_wallet: Pubkey,
    mint: Pubkey,
    nft_mint: Pubkey,
) -> Result<Transaction> {
    let mint_auth_multisig_account = get_bond_mint_auth_multisig_if_needed(&client, mint).await?;
    let bond_account = find_bond_pda(mint).0;
    let data = client.get_account_data(&bond_account).await?;
    let bond = Bond::from_bytes(&data).unwrap();
    let issuance_account = find_issuance_pda(bond_account, bond.issuance_number).0;
    let purchase_order_vault_account = find_purchase_order_pda(nft_mint).0;
    let user_token_account =
        get_associated_token_address_with_program_id(&user_wallet, &mint, &spl_token_2022::id());
    let user_nft_token_account = get_associated_token_address(&user_wallet, &nft_mint);
    let nft_metadata_account = Metadata::find_pda(&nft_mint).0;
    let nft_master_edition_account = MasterEdition::find_pda(&nft_mint).0;
    let nft_collection_metadata_account = Metadata::find_pda(&nft_mint).0;
    let token2022_program = spl_token_2022::id();
    let associated_token_program = spl_associated_token_account::id();
    let token_program = spl_token::id();
    let metadata_program = mpl_token_metadata::ID;
    let system_program = system_program::id();

    let ix = RedeemPurchaseOrder {
        user_wallet,
        bond_account,
        mint_account: mint,
        issuance_account,
        purchase_order_vault_account,
        user_token_account,
        user_nft_token_account,
        nft_mint_account: nft_mint,
        nft_metadata_account,
        nft_master_edition_account,
        nft_collection_metadata_account,
        token2022_program,
        token_program,
        associated_token_program,
        metadata_program,
        system_program,
        mint_auth_multisig_account,
    }
    .instruction();

    Ok(Transaction::new_unsigned(Message::new(
        &[ix],
        Some(&user_wallet),
    )))
}

// Create a purchase order.

// The user will need to have already completed the KYC process for the transaction to be successful.
// See [API Reference](https://docs.etherfuse.com/api-reference/onboarding/generate-onboarding-url) for more information.


let client = RpcClient::new(rpc_url);
// Some user wallet keypair
let user_wallet = Keypair::from_str("user_wallet").unwrap();
// The mint for the bond underlying the NFT
let mint = Pubkey::from_str("mint").unwrap();
// The amount of tokens to purchase
let token_amount = 1000000;
// Get an un-signed transaction
let tx = make_create_purchase_order_transaction(client, token_amount, user_wallet, mint).await?;
// Sign the transaction with the users keypair and then...
// Submit the transaction
let sig = client.send_and_confirm_transaction(&tx).await?;

pub async fn make_create_purchase_order_transaction(
    client: &RpcClient,
    token_amount: u64,
    user_wallet: Pubkey,
    mint: Pubkey,
) -> Result<Transaction> {
    let ix_args = CreatePurchaseOrderV2InstructionArgs {
        amount: token_amount,
    };
    let addresses = generate_buy_addresses(client, mint, user_wallet).await?;
    let nft_mint = Keypair::new();
    let nft_mint_account = nft_mint.pubkey();
    let user_nft_token_account = get_associated_token_address(&user_wallet, &nft_mint_account);
    let nft_master_edition_account = MasterEdition::find_pda(&nft_mint_account).0;
    let purchase_order_vault_account = find_purchase_order_pda(nft_mint_account).0;
    let nft_collection_mint = find_config_pda().0;
    let nft_collection_metadata_account = Metadata::find_pda(&nft_collection_mint).0;
    let nft_collection_master_edition_account = MasterEdition::find_pda(&nft_collection_mint).0;
    let nft_metadata_account = Metadata::find_pda(&nft_mint_account).0;

    let ix = CreatePurchaseOrderV2 {
        kyc_account: find_kyc_pda(user_wallet).0,
        user_wallet,
        bond_account: addresses.bond_account,
        issuance_account: addresses.issuance_account,
        payment_account: addresses.payment_account,
        payment_token_account: addresses.payment_token_account,
        user_payment_token_account: addresses.user_payment_token_account,
        user_nft_token_account,
        nft_mint_account,
        nft_metadata_account,
        nft_master_edition_account,
        purchase_order_vault_account,
        nft_collection_mint,
        nft_collection_metadata_account,
        nft_collection_master_edition_account,
        payment_mint_account: addresses.payment_mint_account,
        payment_feed_account: addresses.payment_feed_account,
        payment_base_price_feed_account: addresses.payment_price_feed_account,
        config_account: find_config_pda().0,
        associated_token_program: spl_associated_token_account::id(),
        token2022_program: spl_token_2022::id(),
        token_program: spl_token::id(),
        system_program: system_program::id(),
        metadata_program: mpl_token_metadata::ID,
        sysvar_instructions: solana_sdk::sysvar::instructions::id(),
        payment_quote_price_feed_account: addresses.payment_quote_price_feed_account,
    }
    .instruction(ix_args);

    Ok(Transaction::new_unsigned(Message::new(
        &[ix],
        Some(&user_wallet),
    )))
}

async fn get_bond_mint_auth_multisig_if_needed(
    rpc_client: &RpcClient,
    mint_account: Pubkey,
) -> Result<Option<Pubkey>> {
    let bond_account = find_bond_pda(mint_account).0;
    let data = rpc_client.get_account_data(&bond_account).await?;
    let bond = Bond::from_bytes(&data).unwrap();

    // Get the mint_auth_multisig_account if is_authority_multisig is true
    let mint_auth_multisig_account = if bond.is_authority_multisig {
        let bond_multisig_meta_account = find_bond_multisig_meta_pda(mint_account).0;
        let bond_multisig_meta_data = rpc_client
            .get_account_data(&bond_multisig_meta_account)
            .await?;
        let bond_multisig_meta = BondMultisigMeta::from_bytes(&bond_multisig_meta_data).unwrap();
        Some(bond_multisig_meta.mint_auth_multisig)
    } else {
        None
    };

    Ok(mint_auth_multisig_account)
}

struct BuyAddresses {
    issuance_account: Pubkey,
    payment_account: Pubkey,
    payment_mint_account: Pubkey,
    payment_token_account: Pubkey,
    user_payment_token_account: Pubkey,
    bond_account: Pubkey,
    payment_price_feed_account: Pubkey,
    payment_quote_price_feed_account: Option<Pubkey>,
    kyc_account: Pubkey,
    payment_feed_account: Pubkey,
    user_token_account: Pubkey,
    token_2022_program: Pubkey,
    associated_token_program: Pubkey,
    token_program: Pubkey,
    system_program: Pubkey,
}

async fn generate_buy_addresses(
    client: &RpcClient,
    mint_account: Pubkey,
    user_wallet: Pubkey,
) -> Result<BuyAddresses> {
    let bond_account = find_bond_pda(mint_account).0;
    let issuance_account = find_issuance_pda(bond_account, 1).0;
    let payment_account = find_payment_pda(issuance_account).0;
    let data = client.get_account_data(&bond_account).await?;
    let bond = Bond::from_bytes(&data).unwrap();
    let payment_feed_account = find_payment_feed_pda(bond.payment_feed_type).0;
    let data = client.get_account_data(&payment_feed_account).await?;
    let payment_feed = PaymentFeed::from_bytes(&data).unwrap();
    let payment_mint_account = payment_feed.payment_mint;
    let mut payment_quote_price_feed_account = None;
    if payment_feed.quote_price_feed != Pubkey::default() {
        payment_quote_price_feed_account = Some(payment_feed.quote_price_feed);
    }
    let user_token_account = get_associated_token_address_with_program_id(
        &user_wallet,
        &mint_account,
        &spl_token_2022::id(),
    );
    let payment_price_feed_account = payment_feed.base_price_feed;
    let kyc_account = find_kyc_pda(user_wallet).0;
    let payment_token_account =
        get_associated_token_address(&payment_account, &payment_mint_account);
    let user_payment_token_account =
        get_associated_token_address(&user_wallet, &payment_mint_account);

    Ok(BuyAddresses {
        issuance_account,
        payment_account,
        payment_mint_account,
        payment_token_account,
        user_payment_token_account,
        bond_account,
        payment_price_feed_account,
        payment_quote_price_feed_account,
        kyc_account,
        payment_feed_account,
        user_token_account,
        token_2022_program: spl_token_2022::id(),
        associated_token_program: spl_associated_token_account::id(),
        token_program: spl_token::id(),
        system_program: system_program::id(),
    })
}

Dependencies

~26–48MB
~834K SLoC