Options
All
  • Public
  • Public/Protected
  • All
Menu

@unstake-it/sol-ag

unstake.it

npm-version npm-license Twitter

unstake.it Staked SOL Instant Unstake Aggregator Typescript SDK

unstake.it allows users to instantly unstake their Solana stake accounts to liquid SOL.

This SDK provides the core UnstakeAg class that aggregates the various unstake routes to compute the best route for a given stake account and unstake amount.

The SDK is heavily inspired by, and uses, @jup-ag/core. The usage patterns are very similar.

Contents:

API

For easy dapp integration without having to install this SDK, we provide a ready-to-use API at https://api.unstake.it

API documentation is available at https://api.unstake.it

Installation

npm

$ npm install @unstake-it/sol-ag

yarn

$ yarn add @unstake-it/sol-ag

Example

Initialize

import { Connection } from "@solana/web3.js";
import { UnstakeAg, legacyTxAmmsToExclude } from "@unstake-it/sol-ag";

const connection = new Connection("https://api.mainnet-beta.solana.com");

// This loads the required accounts for all stake pools
// and jup-ag from on-chain.
// The arg type is `JupiterLoadParams` from jup-ag
const unstake = await UnstakeAg.load({
cluster: "mainnet-beta",
connection,
// if you're using only legacy transactions (no lookup tables),
// you should set ammsToExclude to legacyTxAmmsToExclude() to
// avoid running into transaction size limits
ammsToExclude: legacyTxAmmsToExclude(),
});

Initialize with Reference to Shared Jupiter object

If you're already using the @jup-ag/core SDK elsewhere in your code, you can construct an UnstakeAg object that uses the same existing Jupiter object to avoid fetching and caching duplicate accounts.

import { Jupiter, JupiterLoadParams } from "@jup-ag/core";
import { UnstakeAg } from "@unstake-it/sol-ag";

const myJupParams: JupiterLoadParams = { ... };

const jupiter = await Jupiter.load(myJupParams);

const stakePools = UnstakeAg.createStakePools(myJupParams.cluster);
const withdrawStakePools = UnstakeAg.createWithdrawStakePools(myJupParams.cluster);
const hybridPools = UnstakeAg.createHybridPools(myJupParams.cluster);

const unstake = new UnstakeAg(myJupParams, stakePools, withdrawStakePools, hybridPools, jupiter);

// call unstake.updatePools()
// to perform an initial fetch of all stake pools' accounts
await unstake.updatePools();

Compute Routes

import { PublicKey } from "@solana/web3.js";
import { getStakeAccount } from "@soceanfi/solana-stake-sdk";
import { outLamports, minOutLamports, totalRentLamports } from "@unstake-it/sol-ag";

const stakeAccountPubkey = new PublicKey(...);
const stakeAccount = await getStakeAccount(connection, stakeAccountPubkey);
const routes = await unstake.computeRoutes({
stakeAccount,
amountLamports: BigInt(stakeAccount.lamports),
slippageBps: 10,
// you can optionally collect a fee on top
// of any jup swaps, just as you can in jup sdk
jupFeeBps: 3,
});
const bestRoute = routes[0];
const {
stakeAccInput: {
stakePool,
inAmount,
outAmount,
},
// optional jup-ag `RouteInfo` for any additional swaps
// via jup required to convert stake pool tokens into SOL
jup,
} = bestRoute;

console.log(
"Route will give me",
outLamports(bestRoute),
"lamports, and at least",
minOutLamports(bestRoute),
"lamports at max slippage.",
"I need to spend an additional",
totalRentLamports(bestRoute),
"lamports to pay for rent",
);

Create Transaction(s) From Route

import { prepareSetupTx, prepareUnstakeTx, prepareCleanupTx } from "@unstake-it/sol-ag";

// returned transactions do not have `recentBlockhash` or `feePayer` set
// and are not signed
const exchangeReturn =
await unstake.exchange({
route: bestRoute,
stakeAccount,
stakeAccountPubkey,
user: MY_WALLET_KEYPAIR.publicKey,
// You can optionally provide a mapping of StakePool output tokens / wrapped SOL
// to your token account of the same type to collect stake pool referral fees / jup swap fees
feeAccounts: {
"So11111111111111111111111111111111111111112": MY_WRAPPED_SOL_ACCOUNT,
"5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm": MY_SCNSOL_ACCOUNT,
},
});

const {
setupTransaction,
unstakeTransaction: { tx, signers },
cleanupTransaction,
} = exchangeReturn;

const { blockhash, lastValidBlockHeight } = await unstake.connection.getLatestBlockhash();
const feePayer = MY_WALLET_KEYPAIR.publicKey;

const setupTx = prepareSetupTx(exchangeReturn, blockhash, feePayer);
if (setupTx) {
setupTx.partialSign(MY_WALLET_KEYPAIR);
const signature = await unstake.connection.sendRawTransaction(
setupTx.serialize(),
);
await unstake.connection.confirmTransaction(
{
signature,
blockhash,
lastValidBlockHeight,
}
);
}

const unstakeTx = prepareUnstakeTx(exchangeReturn, blockhash, feePayer);
unstakeTx.partialSign(MY_WALLET_KEYPAIR);
const signature = await unstake.connection.sendRawTransaction(
unstakeTx.serialize(),
);
await unstake.connection.confirmTransaction(
{
signature,
blockhash,
lastValidBlockHeight,
}
);

const cleanupTx = prepareCleanupTx(exchangeReturn, blockhash, feePayer);
if (cleanupTx) {
cleanupTx.partialSign(MY_WALLET_KEYPAIR);
const signature = await unstake.connection.sendRawTransaction(
cleanupTx.serialize(),
);
await unstake.connection.confirmTransaction(
{
signature,
blockhash,
lastValidBlockHeight,
}
);
}

Compute Routes for xSOL

The aggregator also handles the unstaking of xSOL (supported liquid staking derivatives).

import { PublicKey } from "@solana/web3.js";
import { getStakeAccount } from "@soceanfi/solana-stake-sdk";
import JSBI from "jsbi";
import { isXSolRouteJupDirect, outLamportsXSol, minOutLamportsXSol, totalRentLamportsXSol } from "@unstake-it/sol-ag"

const scnSOL = new PublicKey("5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm");
const routesScnSol = await unstake.computeRoutesXSol({
inputMint: scnSOL,
amount: JSBI.BigInt(1_000_000_000)
slippageBps: 10,
// args are the same as jups' computeRoutes(), except
// - feeBps -> jupFeeBps
// - +shouldIgnoreRouteErrors: boolean
// - +stakePoolsToExclude: StakePoolsToExclude
});
const bestRouteScnSol = routesScnSol[0];
if (isXSolRouteJupDirect(bestRouteScnSol)) {
const {
jup, // jup RouteInfo type
} = bestRouteScnSol;
} else {
const {
withdrawStake: {
withdrawStakePool,
inAmount,
outAmount,
stakeSplitFrom,
},
intermediateDummyStakeAccountInfo,
unstake, // UnstakeRoute type
} = bestRouteScnSol;
}

console.log(
"Route will give me",
outLamportsXSol(bestRouteScnSol),
"lamports, and at least",
minOutLamportsXSol(bestRouteScnSol),
"lamports at max slippage",
"I need to spend an additional",
totalRentLamportsXSol(bestRouteScnSol),
"lamports to pay for rent",
);

Create Transaction(s) From Route for xSOL

If required, stake pool stake withdraw instructions are placed in setupTransaction. This means that if the main unstakeTransaction fails, the user will be left with a stake account.

import { prepareSetupTx, prepareUnstakeTx, prepareCleanupTx } from "@unstake-it/sol-ag";

// returned transactions do not have `recentBlockhash` or `feePayer` set
// and are not signed
const exchangeReturn =
await unstake.exchangeXSol({
route: bestRouteScnSol,
user: MY_WALLET_KEYPAIR.publicKey,
srcTokenAccount: MY_SCNSOL_ACCOUNT,
// You can optionally provide a mapping of StakePool output tokens / wrapped SOL
// to your token account of the same type to collect stake pool referral fees / jup swap fees
feeAccounts: {
"So11111111111111111111111111111111111111112": MY_WRAPPED_SOL_ACCOUNT,
},
});

const {
setupTransaction,
unstakeTransaction: { tx, signers },
cleanupTransaction,
} = exchangeReturn;

const { blockhash, lastValidBlockHeight } = await unstake.connection.getLatestBlockhash();
const feePayer = MY_WALLET_KEYPAIR.publicKey;

const setupTx = prepareSetupTx(exchangeReturn, blockhash, feePayer);
if (setupTx) {
setupTx.partialSign(MY_WALLET_KEYPAIR);
const signature = await unstake.connection.sendRawTransaction(
setupTx.serialize(),
);
await unstake.connection.confirmTransaction(
{
signature,
blockhash,
lastValidBlockHeight,
}
);
}

const unstakeTx = prepareUnstakeTx(exchangeReturn, blockhash, feePayer);
unstakeTx.partialSign(MY_WALLET_KEYPAIR);
const signature = await unstake.connection.sendRawTransaction(
unstakeTx.serialize(),
);
await unstake.connection.confirmTransaction(
{
signature,
blockhash,
lastValidBlockHeight,
}
);

const cleanupTx = prepareCleanupTx(exchangeReturn, blockhash, feePayer);
if (cleanupTx) {
cleanupTx.partialSign(MY_WALLET_KEYPAIR);
const signature = await unstake.connection.sendRawTransaction(
cleanupTx.serialize(),
);
await unstake.connection.confirmTransaction(
{
signature,
blockhash,
lastValidBlockHeight,
}
);
}

Lookup Table

We provide a utility script for creating a lookup table that contains most of the included stake pools' relevant addresses and some commonly used programs and sysvars.

# verify that your solana cli config is correct
solana config get

yarn lut

A lookup table maintained by the team is available on mainnet-beta at EhWxBHdmQ3yDmPzhJbKtGMM9oaZD42emt71kSieghy5

Learn More

Generated using TypeDoc