This repository contains our beta V3 SDK for Typescript developers. It simplifies interaction with Plenty's segmented CFMM by offering essential math utilities and operation object creation for tasks like swapping, liquidity management, and staking positions in farms.
The SDK is extensively utilized within our frontend application, hosted at app.plenty.nework. Furthermore, it plays a pivotal role in the testing processes within our smart contract repository.
👷 As this is beta software, both the code and documentation will undergo continuous improvement over time. If you have any questions about how to use the SDK or wish to report bugs, please feel free to reach out to us at our discord server.
You can install the SDK through npm:
npm install @plenty-labs/v3-sdk
The SDK uses the Taquito library for utilities that allow for interaction with smart contracts.
You may also find it beneficial to make extensive use of Plenty's unified API in conjunction with the SDK.
Here are reference scenarios to guide you in using the SDK to interact with the contracts. For an in-depth understanding of the math, you may check the README provided here.
With the SDK, you can effortlessly estimate a wide range of values by inputting the state from the smart contract storage. To access the smart contract storage you can either use Taquito or a tool like tzkt API. For the purpose of this demonstration, we'll use Taquito.
import { TezosToolkit } from "@taquito/taquito";
const tezos = new TezosToolkit("https://ghostnet.smartpy.io"); // Select rpc node of your choice
(async () => {
// Create Taquito contract instance of the CFMM
const cfmm = await tezos.contract.at(process.env.CFMM_ADDRESS as string);
// Pull the smart contract storage
const storage = await cfmm.storage();
// Rest of the code
// ......
})();
To estimate the output received in token-y when swapping in token-x or vice versa, you can use the Pool entity:
import BigNumber from "bignumber.js";
import { Pool } from "@plenty-labs/v3-sdk";
(async () => {
// .....
// Essential setup
// Create an instance of the Pool class
const pool = new Pool(
storage.cur_tick_index.toNumber(),
storage.cur_tick_witness.toNumber(),
storage.constants.tick_spacing.toNumber(),
storage.sqrt_price,
storage.fee_bps.toNumber(),
storage.liquidity
);
// Input amount token x
const tokenXIn = new BigNumber(10);
// Estimate the final output amount of token y
const estimatedOutputY = await pool.estimateSwapXToY(tokenXIn, async (tick: number) => {
const tickElement = await storage.ticks.get(tick);
return {
index: tick,
prevIndex: tickElement.prev.toNumber(),
nextIndex: tickElement.next.toNumber(),
liquidityNet: tickElement.liquidity_net,
sqrtPrice: tickElement.sqrt_price,
};
});
})();
The same can achieved for estimate token y to token x swaps through pool.estimateSwapYToX(...)
.
Both estimateSwapYToX
and estimateSwapXToY
accept a custom function of type (index: number) => Promise<TickElement>
as the second argument. It is upon you to device a way of supplying the tick states to the estimation function. You can either use Taquito (as shown above), tzkt API or Plenty's unified API
Similar to estimating swaps, you can use the Pool entity to estimate the amount of token y required when a certain amount of token x is being added as liquidity in a price range.
⚠️ It is crucial to have an understanding of how price ranges and ticks work in a segmented cfmm when using the liquidity estimation utility. The function call may throw an error if called incorrectly.
import BigNumber from "bignumber.js";
import { Pool, Price, Tick, Token } from "@plenty-labs/v3-sdk";
(async () => {
// .....
// Essential setup
// Create an instance of the Pool class
const pool = new Pool(
storage.cur_tick_index.toNumber(),
storage.cur_tick_witness.toNumber(),
storage.constants.tick_spacing.toNumber(),
storage.sqrt_price,
storage.fee_bps.toNumber(),
storage.liquidity
);
// Create objects for the tokens in the pair
const tokenX: Token = { address: "KT1Uw1oio434UoWFuZTNKFgt5wTM9tfuf7m7", tokenId: 2, decimals: 6 };
const tokenY: Token = { address: "KT1Uw1oio434UoWFuZTNKFgt5wTM9tfuf7m7", tokenId: 5, decimals: 18 };
// Price boundaries Y / X
const lowerPriceBoundary = new BigNumber(0.99);
const upperPriceBoundary = new BigNumber(1.01);
// Calculate lower and upper tick indices using the math utilities
const lowerTickIndex = Tick.computeTickFromSqrtPrice(
Price.computeSqrtPriceFromRealPrice(lowerPriceBoundary, tokenX, tokenY),
storage.constants.tick_spacing.toNumber()
);
const upperTickIndex = Tick.computeTickFromSqrtPrice(
Price.computeSqrtPriceFromRealPrice(upperPriceBoundary, tokenX, tokenY),
storage.constants.tick_spacing.toNumber()
);
// Amount of token x
const tokenXAmount = new BigNumber(10);
// Estimate the amount of token y required when the above specified amount of token x
// is added as liquidity in the price range 0.99 -> 1.01
const tokenYAmount = await pool.estimateAmountYFromX(tokenXAmount, lowerTickIndex, upperTickIndex);
})();
The same can be done to estimate amount of token x required for a certain amount of token y through pool.estimateAmountXFromY(...)
.
To estimate current unclaimed fees accrued by a position, you can use the Position entity.
import BigNumber from "bignumber.js";
import { Pool, Position } from "@plenty-labs/v3-sdk";
(async () => {
// .....
// Essential setup
// Retrieve the position from the storage
const pos = await storage.positions.get(<position-id>);
// Create an instance of the Pool class for the cfmm to which the position belongs
const pool = new Pool(
storage.cur_tick_index.toNumber(),
storage.cur_tick_witness.toNumber(),
storage.constants.tick_spacing.toNumber(),
storage.sqrt_price,
storage.fee_bps.toNumber(),
storage.liquidity
);
// Create an instance of the Position class
const position = new Position(
pool,
pos.lower_tick_index.toNumber(),
pos.upper_tick_index.toNumber(),
pos.liquidity,
pos.fee_growth_inside_last
);
// Pull the latest tick states of the positions lower and upper tick
const lowerTickState = await storage.ticks.get(pos.lower_tick_index.toNumber());
const upperTickState = await storage.ticks.get(pos.lower_tick_index.toNumber());
// Compute the fees
const feesCollected = position.computeFees(
storage.fee_growth,
lowerTickState.fee_growth_outside,
upperTickState.fee_growth_outside
);
})();
Similarly, you can use position.computeTokenAmounts()
to estimate the amount of each token present within the position. This utility does not require any other state to be passed into it.
You can use the Stake entity to estimate unclaimed rewards for a position that is staked in Plenty's v3 farm contract.
import { Stake, Incentive } from "@plenty-labs/v3-sdk";
(async () => {
// .....
// Essential setup without retrieving pool storage
// Create a Taquito instance of the farm contract
const farm = await tezos.contract.at(process.env.FARM_CONTRACT as string);
const storage = await farm.storage();
// Pull the incentive and stake state
const incentiveState = await storage.incentives.get(<incentive-id>);
const stakeState = await storage.stakes.get({ 0: <position-id>; 1: <incentive-id> });
const incentive: Incentive = {
startTime: Math.floor(new Date(incentiveState.start_time).getTime() / 1000),
endTime: Math.floor(new Date(incentiveState.end_time).getTime() / 1000),
totalRewardUnclaimed: incentiveState.total_reward_unclaimed,
totalSecondsClaimed: incentiveState.total_seconds_claimed,
}
// Create an instance of the Stake class
const stake = new Stake(incentive, stakeState.liquidity, stakeState.seconds_per_liquidity_inside_last);
// Compute the latest value of `seconds_per_liquidity_inside` between the lower and upper ticks of stake position
// Run the `snapshot_cumulatives_inside` onchain view of the cfmm to do that easily
const { seconds_per_liquidity_inside } = await cfmm.contractViews.snapshot_cumulatives_inside(
{ lower_tick_index: <lower-tick-index-of-position>, upper_tick_index: <upper-tick-index-of-position> }
).executeView({ viewCaller: <any-tz-address> });
// Compute unclaimed rewards
const unclaimedRewards = stake.computeUnclaimedReward(seconds_per_liquidity_inside);
})();
Using the SDK, you can construct Taquito's TransferParams for v3 contract calls and send them very flexibly in an operation, either as a standalone or within a batch.
⚠️ Please follow through the essential setup before proceeding.
You can perform a swap in a pool from token x to token y or vice versa using the Swap Portal.
import { OpKind, ParamsWithKind } from "@taquito/taquito";
import { SwapPortal } from "@plenty-labs/v3-sdk";
(async () => {
// .....
// Essential setup
// Create Taquito contract instance for token x
const tokenX = await tezos.contract.at(process.env.TOKEN_X as string);
// Amount of token x being swapped for token y
const tokenXIn = new BigNumber(10);
// Minimum amount of token y expected to be received
const minTokenYOut = new BigNumber(0);
// Transaction deadline (optional value)
// const deadline =
// Output recipient
const recipient = <tz-KT-address>;
// Create a batch params
const batchParams: ParamsWithKind[] = [
// Allow cfmm contract to spend token x (For this demonstration, token x is fa2)
{
kind: OpKind.TRANSACTION,
...Approvals.updateOperatorsFA2(tokenX, [
{ add_operator: { owner: <tz-address>, operator: cfmm.address, token_id: <token-id> } },
]),
},
{
kind: OpKind.TRANSACTION,
...SwapPortal.swapXToY(
cfmm,
{ tokenXIn, minTokenYOut, recipient }
),
}
];
// Send the batch
await tezos.contract.batch(batchParams).send();
})();
- Similarly,
SwapPortal.swapYToX(...)
can be used to swap from token y to token x. - You can use the Approvals utility for approving token spendings for both FA1.2 and FA2 tokens.
- By using the swap output estimation utility as explained in this section, you can estimate the maximum amount of output that you may receive. You can set this as the value of
minTokenYOut
orminTokenXOut
for a zero slippage trade. deadline
is an optional timestamp field. It defaults to 15 minutes from now. If the trade is not completed within the deadline, it does not go through.
You can create a new position and increase or decrease the liquidity in an existing one through the Position Manager. In a segmented cfmm, liquidity is always added in a specific price range represented by tick values - lower tick and upper tick.
To create a new position, you must determine the quantity of each token in the pair being added as liquidity. You can achieve this by either fixing one token's amount and then following the instructions under estimating liquidity to calculate the required amount of the other token. Alternatively, for greater convenience, you can start with the maximum value of the individual tokens you intend to contribute and allow the SDK to calculate the required individual amounts.
async () => {
//....
const sqrtPriceAx80 = Tick.computeSqrtPriceFromTick(lowerTickIndex); // Sqrt price at lower index
const sqrtPriceBx80 = Tick.computeSqrtPriceFromTick(upperTickIndex); // Sqrt price at upper index
const sqrtPriceCx80 = storage.sqrt_price;
// Arbitrary initial amounts
const amount = {
x: number(50 * DECIMALS),
y: number(50 * DECIMALS),
};
// SDK resolves the correct liquidity and associated amounts
const liquidity = Liquidity.computeLiquidityFromAmount(amount, sqrtPriceCx80, sqrtPriceAx80, sqrtPriceBx80);
const finalAmounts = Liquidity.computeAmountFromLiquidity(liquidity, sqrtPriceCx80, sqrtPriceAx80, sqrtPriceBx80);
//....
};
Once you have the final amounts, you can prepare the options to be passed in PositionManager.setPositionOp(...)
. As you can see below the options require a lower and upper tick witness. The tick witness is essentially the greatest tick less than the individual ticks. To retrieve the tick witness you may either run a binary search on the initialised ticks, or use the utility provided by Plenty's unified API.
import { OpKind, ParamsWithKind } from "@taquito/taquito";
import { PositionManager, SetPositionOptions } from "@plenty-labs/v3-sdk";
(async () => {
// .....
// Essential setup and figuring out final amounts
// Create Taquito contract instance for token x and y
const tokenX = await tezos.contract.at(process.env.TOKEN_X as string);
const tokenY = await tezos.contract.at(process.env.TOKEN_Y as string);
const options: SetPositionOptions = {
lowerTickIndex,
upperTickIndex,
lowerTickWitness: <lower-tick-witness>,
upperTickWitness: <upper-tick-witness>,
liquidity,
maximumTokensContributed: finalAmounts,
// deadline - optional numeric UNIX timestamp field that defaults to 15 minutes from current time
};
// Create a batch params
const batchParams: ParamsWithKind[] = [
// Allow cfmm contract to spend token x and y
{
kind: OpKind.TRANSACTION,
...Approvals.updateOperatorsFA2(tokenX, [
{ add_operator: { owner: <tz-address>, operator: cfmm.address, token_id: <token-id> } },
]),
},
{
kind: OpKind.TRANSACTION,
...Approvals.updateOperatorsFA2(tokenY, [
{ add_operator: { owner: <tz-address>, operator: cfmm.address, token_id: <token-id> } },
]),
},
{
kind: OpKind.TRANSACTION,
...PositionManager.setPositionOp(
cfmm,
options,
),
},
];
// Send the batch
await tezos.contract.batch(batchParams).send();
})();
If you have an existing position, you can adjust the liquidity within it by using PositionManager.updatePositionOp(...)
.
Within this utility, you will find a field named liquidityDelta
. A positive liquidity delta indicates that liquidity is being added to the position, while a negative value implies removal. When adding liquidity, you can calculate the required value of liquidityDelta
in the same manner as you calculate liquidity
, as demonstrated in the initial code snippet under Managing positions. Conversely, when removing liquidity, you can consider liquidityDelta
as a percentage of liquidity being withdrawn. For instance, if the current position's liquidity
is 120
, and you intend to remove 10% of the liquidity, the liquidityDelta
would be -12
.
Another important field is tokensLimit
. When increasing liquidity, this value represents the maximum number of tokens that can be added to the position. Conversely, when removing liquidity, it signifies the minimum number of tokens that must be received. Failure to meet the tokens limit will result in the transaction being reverted.
import { OpKind, ParamsWithKind } from "@taquito/taquito";
import { PositionManager, UpdatePositionOptions } from "@plenty-labs/v3-sdk";
(async () => {
// .....
// Essential setup and calculating `liquidityDelta`
const options: UpdatePositionOptions = {
positionId: 1, //
liquidityDelta, // Remember to make it negative for liquidity removal
toX: <tz-KT-address>, // Only relevant for liquidity removal. This address is where the tokens are sent.
toY: <tz-KT-address>, // Same as above.
// deadline - optional numeric UNIX timestamp field
tokensLimit: finalAmounts, // If you intend to remove liquidity, you can reverse-calculate this through `liquidityDelta`
};
// Create a batch params
// Approvals are only required for liquidity addition
const batchParams: ParamsWithKind[] = [
// Allow cfmm contract to spend token x and y
{
kind: OpKind.TRANSACTION,
...Approvals.updateOperatorsFA2(tokenX, [
{ add_operator: { owner: <tz-address>, operator: cfmm.address, token_id: <token-id> } },
]),
},
{
kind: OpKind.TRANSACTION,
...Approvals.updateOperatorsFA2(tokenY, [
{ add_operator: { owner: <tz-address>, operator: cfmm.address, token_id: <token-id> } },
]),
},
{
kind: OpKind.TRANSACTION,
...PositionManager.updatePositonOp(
cfmm,
options,
),
},
];
// Send the batch
await tezos.contract.batch(batchParams).send();
})();
There is another utility PositionManager.collectFeesOp(...)
provided that internally utilises updatePositionOp
and only retrieves fees collected by the position.
You can stake your liquidity to farm rewards in a Plenty v3 farm contract via the StakeManager.
import { OpKind, ParamsWithKind } from "@taquito/taquito";
import { StakeManager, StakeOptions } from "@plenty-labs/v3-sdk";
(async () => {
// .....
// Essential setup
// Initialise Taquito contract instance of the farm
const farm = await tezos.contract.at(process.env.FARM_CONTRACT as string);
const options: StakeOptions = {
incentiveId: <incentive-id>,
tokenId: <position-id>,
};
// Create a batch params
// Approval is only required if it is the first stake
const batchParams: ParamsWithKind[] = [
// Allow farm contract to spend cfmm position token
{
kind: OpKind.TRANSACTION,
...Approvals.updateOperatorsFA2(cfmm, [
{ add_operator: { owner: <tz-address>, operator: farm.address, token_id: <token-id> } },
]),
},
{
kind: OpKind.TRANSACTION,
...StakeManager.stake(
farm,
options,
),
},
];
// Send the batch
await tezos.contract.batch(batchParams).send();
})();
Most of the mathematics used in the segmented CFMM is comprehensively explained in this Specification. The SDK exposes two abstract classes with TypeScript utilities that mirror the mathematical operations. These utilities are valuable in various instances if you are developing on top of Plenty v3.
- Liquidity: This is most useful when managing positions. You can use it to calculate the
liquidity
via token amounts and vice versa. - Tick: This is primarily utilised in the conversion of tick <> square root price used in the cfmm. It is also often used in tandem with Price utility to calculate tick directly from the human-readable price.