Solana API Usage Example

View an example showing how to send a transaction using Blockdaemon Solana Staking API.

In this section, you will find a TypeScript example showing how to send a Solana transaction using the Solana Staking API.

It signs and broadcasts the transaction using the standardized Solana SDK and RPC endpoints. These are supported by Blockdaemon through the Native RPC API for Solana.

import {
  Keypair,
  Transaction,
  LAMPORTS_PER_SOL,
  sendAndConfirmRawTransaction,
  Connection,
  PublicKey,
  StakeProgram,
} from '@solana/web3.js';
import * as bs58 from 'bs58';

function SOLToLamports(sols: number) {
  return Math.floor(LAMPORTS_PER_SOL * sols);
}

function txDecode(
  str: string,
): Transaction {
  // The transaction is a hex-encoded string
  return Transaction.from(Buffer.from(str, 'hex'));
}
// This function will commit a signed transaction to the Solana network
// and will wait for confirmation
async function commitTx(connection: Connection, tx: Transaction) {
  tx.verifySignatures()
  const latestBlock = await connection.getLatestBlockhash();
  if (!tx.signature) return;

  const confirmed = await sendAndConfirmRawTransaction(connection, tx.serialize(), {
    signature: bs58.encode(tx.signature),
    blockhash: latestBlock .blockhash,
    lastValidBlockHeight: latestBlock.lastValidBlockHeight,
  })
  console.log(`Confirmed: ${confirmed}`);
}

async function verifyValidatorAddress(connection: Connection, validatorAddress: string): Promise {
  const voteAccounts = await connection.getVoteAccounts('finalized');
  // We check if the validator address is part of the `active` validators
  // alternatively we can also check if it's part of the `delinquent` too
  const found = voteAccounts.current.find(acc => acc.votePubkey.toString() == validatorAddress);
  return found != undefined;
}

async function createStakeExample() {
    // get environment variables
    const { validatorAddress, delegatorKey, rpcUrl, bossApiKey, clientId } =
      getSolanaVariables();

    const connection = new Connection(rpcUrl, "confirmed");
    // Check if validator exists
    const exits = await verifyValidatorAddress(connection, validatorAddress)
    if(!exits) {
      throw "Validator address is not part of the active validators in the network";
    }

    // create a stake intent with the Staking Integration API
    const response = await createStakeIntent(bossApiKey, clientId, {
      amount: SOLToLamports(1).toString(),
      validator_address: validatorAddress,
      // By default `staking_authority` and `withdrawal_authority` will be
      // the same as delegator address
      delegator_address: delegatorKey.publicKey.toString(),
    });
    if (!response.solana) {
      throw "Missing property `solana` in BOSS responce";
    }

    // get the unsigned transaction data returned by the Staking Integration API
    const unsigned_transaction = response.solana.unsigned_transaction;

    const decodedTx = txDecode(unsigned_transaction);
    decodedTx.partialSign(delegatorKey);


    const delegatorBalance = await connection.getBalance(new PublicKey(delegatorKey.publicKey));
    const fee = await decodedTx.getEstimatedFee(connection);
    const delegatedAmount = Number(response.solana.amount);
    if (delegatorBalance < delegatedAmount + fee) { throw `insufficient funds: ${delegatorKey.publicKey.toString()}` } if (!decodedTx.verifySignatures()) { const missingSignatureFromPubkeys = decodedTx.signatures .filter(x => !x.signature)
      .map(x => x.publicKey.toString())
      throw `Missing signatures; [${missingSignatureFromPubkeys.join(", ")}]`;
    }

    await commitTx(connection, decodedTx);
}

async function deactivateExample() {
    // get environment variables
    const { delegatorKey, rpcUrl, bossApiKey, clientId } =
    getSolanaVariables();

    const connection = new Connection(rpcUrl);
    // create a deactivate intent with the Staking Integration API
    const response = await createDeactivateIntent(bossApiKey, clientId, SOLToLamports(1));
    if (!response.solana) {
      throw "Missing property `solana` in BOSS responce";
    }

    const splits = response.solana.splits;
    // We should execute all split transactions first
    const splitRequests = splits?.map(async split => {
      const decodedTx = txDecode(split.unsigned_transaction);
      // Here we assume that the delegatorKey is a staking autorithy
      if (split.stake_account_authority != delegatorKey.publicKey.toString()) {
        throw "The delegator key is not stake authority";
      }
      decodedTx.partialSign(delegatorKey);
      await commitTx(connection, decodedTx);
    })
    await Promise.all(splitRequests);

    const deactivates = response.solana.deactivates;
    const deactivateRequests = deactivates.map(async deactivate => {
      const decodedTx = txDecode(deactivate.unsigned_transaction);

      if (deactivate.stake_account_authority != delegatorKey.publicKey.toString()) {
        throw "The delegator key is not stake authority";
      }
      decodedTx.partialSign(delegatorKey);
      await commitTx(connection, decodedTx);
    })

    await Promise.all(deactivateRequests);
}


async function withdrawExample() {
    // get environment variables
    const { delegatorKey, rpcUrl, bossApiKey, clientId } =
    getSolanaVariables();

    const connection = new Connection(rpcUrl);
    // create a withdraw intent with the Staking Integration API
    const response = await createWithdrawIntent(bossApiKey, clientId, SOLToLamports(1), delegatorKey.publicKey.toString());
    if (!response.solana) {
      throw "Missing property `solana` in BOSS response";
    }

    const withdraws = response.solana.withdraws;
    const withdrawRequests = withdraws.map(async withdraw => {
      const decodedTx = txDecode(withdraw.unsigned_transaction);
      if (withdraw.withdrawal_authority_public_key != delegatorKey.publicKey.toString()) {
        throw "The delegator key is not stake authority";
      }
      decodedTx.partialSign(delegatorKey);
      await commitTx(connection, decodedTx);
    })
    // After this all funds must be tranfered to the withdraw address
    await Promise.all(withdrawRequests);
}


// a function for getting environment variables
function getSolanaVariables() {

  // check for the solana delegator address(the address which will fund the stake)
  if (!process.env.SOLANA_VALIDATOR_ADDRESS) {
    throw new Error('Please set the SOLANA_VALIDATOR_ADDRESS env variable.');
  }

// check for the solana delegator secret key
  if (!process.env.SOLANA_DELEGATOR_SECRET_KEY_PATH) {
    throw new Error('Please set the SOLANA_DELEGATOR_ADDRESS env variable.');
  }

  // check for the Solana RPC URL or the Ubiquity API key
  if (!process.env.SOLAN_RPC_URL && !process.env.UBIQUITY_API_KEY) {
    throw new Error('Please set either ETH_RPC_URL or UBIQUITY_API_KEY env variables.');
  }

  // check for the BOSS API key
  if (!process.env.BOSS_API_KEY) {
    throw new Error('Please set the BOSS_API_KEY env variable.');
  }

  // check for the delegator private key
  if (!process.env.DELEGATOR_PRIVATE_KEY) {
    throw new Error('Please set the DELEGATOR_PRIVATE_KEY env variable. It should be base58 encoded');
  }

  // check for the Staking Reporting client id
  if (!process.env.CLIENT_ID) {
    throw new Error('Please set the CLIENT_ID env variable');
  }

  return {
    validatorAddress: process.env.SOLANA_VALIDATOR_ADDRESS,
    delegatorKey:  Keypair.fromSecretKey(bs58.decode(process.env.DELEGATOR_PRIVATE_KEY)),
    rpcUrl:
      process.env.SOLAN_RPC_URL ??
      getUbiquityNativeUrl(process.env.UBIQUITY_API_KEY!),
    bossApiKey: process.env.BOSS_API_KEY,
    clientId: process.env.CLIENT_ID,
  };
}

/* a function for getting the Ubiquity native RPC access URL
(please note that Ubiquity API key is required) */
function getUbiquityNativeUrl(
  apiKey: string,
  network: 'mainnet' | 'devnet' = 'devnet',
) {
  return `https://svc.blockdaemon.com/solana/${network}/native?apiKey=${apiKey}`;
}

export type StakeIntentSolanaRequest = {
  amount: string;
  validator_address: string;
  delegator_address: string;
  staking_authority?: string;
  withdrawal_authority?: string;
};

export type StakeIntentSolana = {
  stake_id: string;
  amount: string;
  validator_public_key: string;
  staking_authority: string;
  withdrawal_authority: string;
  stake_account_public_key: string;
  unsigned_transaction: string;
};

export type StakeIntentResponce = {
  stake_intent_id: string;
  protocol: string;
  network: string;
  solana?: StakeIntentSolana;
  customer_id?: string;
};


/* a function for creating a stake intent with the Staking Integration API */
function createStakeIntent(
  bossApiKey: string,
  clientId: string,
  request: StakeIntentSolanaRequest,
): Promise {
  const requestOptions = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Accept: 'application/json',
      'X-API-Key': bossApiKey,
      'X-Client-ID': clientId,
    },
    body: JSON.stringify(request),
  };

  const network = 'devnet';
  // return the response from POST Create a New Stake Intent
    return fetch(
      `${process.env.BOSS_API_ADDRESS}/v1/solana/${network}/stake-intents`,
    requestOptions,
  ).then(async response => {
    if (response.status != 200) {
      throw await response.json();
    }
    return response.json() as Promise
  })
}

interface DeactivateStakeSolana {
  unsigned_transaction: string,
  stake_account_authority: string,
  stake_account_address: string,
  stake_id: string,
  amount: string,
};

interface DeactivateIntentResponce {
  solana?: DeactivateIntentSolana
}

interface DeactivateIntentSolana {
  deactivates: Array;
  splits: Array;
  total_deactivated_amount: string,
};

interface SplitStakeSolana {
  unsigned_transaction: string,
  stake_account_authority: string,
  new_stake_account_address: string,
  new_stake_id: string,
  splitted_stake_account_address: string,
  splitted_stake_id: string,
  amount: string,
};


/* a function for creating a deactivate intent with the Staking Integration API */
function createDeactivateIntent(
  bossApiKey: string,
  clientId: string,
  amount: number,
): Promise {
  const requestOptions = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Accept: 'application/json',
      'X-API-Key': bossApiKey,
      'X-Client-ID': clientId,
    },
    body: JSON.stringify({
      amount: amount.toString()
    }),
  };

  const network = 'devnet';
  // return the response from POST Create a New Deactivate Intent
    return fetch(
      `${process.env.BOSS_API_ADDRESS}/v1/solana/${network}/deactivation-intents`,
    requestOptions,
  ).then(async response => {
    if (response.status != 200) {
      throw await response.json();
    }
    return response.json() as Promise
  })
}

interface WithdrawSolana {
  unsigned_transaction: string,
  withdrawal_authority_public_key: string,
  stake_account_address: string,
  stake_id: string,
  amount: string,
};

interface WithdrawIntent {
  solana?: {
    withdraws: Array,
    total_withdraw_amount: string,
  };
};

/* a function for creating a withdraw intent with the Staking Integration API */
function createWithdrawIntent(
  bossApiKey: string,
  clientId: string,
  amount: number,
  withdrawAddress: string
): Promise {
  const requestOptions = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Accept: 'application/json',
      'X-API-Key': bossApiKey,
      'X-Client-ID': clientId,
    },
    body: JSON.stringify({
      amount: amount.toString(),
      withdrawal_address: withdrawAddress,
    }),
  };

  const network = 'devnet';
  // return the response from POST Create a New Deactivate Intent
    return fetch(
      `${process.env.BOSS_API_ADDRESS}/v1/solana/${network}/withdrawal-intents`,
    requestOptions,
  ).then(async response => {
    if (response.status != 200) {
      throw await response.json();
    }
    return response.json() as Promise
  })
}

// run the example
// createStakeExample()
//   .then(() => process.exit(0))
//   .catch(err => {
//     console.error(err);
//     process.exit(1);
//   });

//run the example
// deactivateExample()
// .then(() => process.exit(0))
// .catch(err => {
//   console.error(err);
//   process.exit(1);
// });


//run the example
withdrawExample()
.then(() => process.exit(0))
.catch(err => {
  console.error(err);
  process.exit(1);
});

👋 Need Help?

Contact us through email or our support page for any issues, bugs, or assistance you may need.