@saberhq/stableswap-sdk
Version:
Solana SDK for Saber's StableSwap program.
286 lines (256 loc) • 8.19 kB
text/typescript
import type { BigintIsh } from "@saberhq/token-utils";
import { Percent, Token as SToken, TokenAmount } from "@saberhq/token-utils";
import { PublicKey } from "@solana/web3.js";
import { BN } from "bn.js";
import { mapValues } from "lodash";
import { SWAP_PROGRAM_ID } from "../constants.js";
import type { IExchangeInfo } from "../entities/exchange.js";
import { RECOMMENDED_FEES, ZERO_FEES } from "../state/fees.js";
import {
calculateEstimatedMintAmount,
calculateEstimatedSwapOutputAmount,
calculateEstimatedWithdrawAmount,
calculateEstimatedWithdrawOneAmount,
calculateVirtualPrice,
} from "./amounts.js";
const exchange = {
swapAccount: new PublicKey("YAkoNb6HKmSxQN9L8hiBE5tPJRsniSSMzND1boHmZxe"),
programID: SWAP_PROGRAM_ID,
lpToken: new SToken({
symbol: "LP",
name: "StableSwap LP",
address: "2poo1w1DL6yd2WNTCnNTzDqkC6MBXq7axo77P16yrBuf",
decimals: 6,
chainId: 100,
}),
tokens: [
new SToken({
symbol: "TOKA",
name: "Token A",
address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
decimals: 6,
chainId: 100,
}),
new SToken({
symbol: "TOKB",
name: "Token B",
address: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
decimals: 6,
chainId: 100,
}),
],
} as const;
const makeExchangeInfo = (
{
lpTotalSupply = BigInt(200_000_000),
tokenAAmount = BigInt(100_000_000),
tokenBAmount = BigInt(100_000_000),
}: {
lpTotalSupply?: bigint;
tokenAAmount?: bigint;
tokenBAmount?: bigint;
} = {
lpTotalSupply: BigInt(200_000_000),
tokenAAmount: BigInt(100_000_000),
tokenBAmount: BigInt(100_000_000),
},
): IExchangeInfo => ({
ampFactor: BigInt(100),
fees: ZERO_FEES,
lpTotalSupply: new TokenAmount(exchange.lpToken, lpTotalSupply),
reserves: [
{
reserveAccount: new PublicKey(
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
),
adminFeeAccount: new PublicKey(
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
),
amount: new TokenAmount(exchange.tokens[0], tokenAAmount),
},
{
reserveAccount: new PublicKey(
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
),
adminFeeAccount: new PublicKey(
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
),
amount: new TokenAmount(exchange.tokens[1], tokenBAmount),
},
],
});
const exchangeInfo = makeExchangeInfo();
const exchangeInfoWithFees = {
...exchangeInfo,
fees: RECOMMENDED_FEES,
} as const;
const assertTokenAmounts = (actual: TokenAmount, expected: TokenAmount) => {
expect(actual.equalTo(expected) && actual.token.equals(expected.token)).toBe(
true,
);
};
const assertTokenAmount = (actual: TokenAmount, expected: BigintIsh) => {
expect(actual.raw.toString()).toEqual(expected.toString());
};
describe("Calculated amounts", () => {
describe("#calculateVirtualPrice", () => {
it("works", () => {
const result = calculateVirtualPrice(exchangeInfo);
expect(result?.toFixed(4)).toBe("1.0000");
});
it("is symmetric", () => {
const result = calculateVirtualPrice(
makeExchangeInfo({
lpTotalSupply: BigInt("200000000"),
tokenAAmount: BigInt("10000000"),
tokenBAmount: BigInt("190000000"),
}),
);
expect(result?.toFixed(4)).toBe("0.9801");
const result2 = calculateVirtualPrice(
makeExchangeInfo({
lpTotalSupply: BigInt(200_000_000),
tokenAAmount: BigInt(190_000_000),
tokenBAmount: BigInt(10_000_000),
}),
);
expect(result2?.toFixed(4)).toBe("0.9801");
});
it("can quote both prices", () => {
const exchange = makeExchangeInfo({
lpTotalSupply: BigInt(200_000_000),
tokenAAmount: BigInt(10_000_000),
tokenBAmount: BigInt(190_000_000),
});
const result = calculateVirtualPrice(exchange);
expect(result?.toFixed(4)).toBe("0.9801");
});
});
describe("#calculateEstimatedSwapOutputAmount", () => {
it("no fees", () => {
const result = calculateEstimatedSwapOutputAmount(
exchangeInfo,
new TokenAmount(exchange.tokens[0], BigInt(10_000_000)),
);
assertTokenAmounts(result.outputAmount, result.outputAmountBeforeFees);
});
it("fees are different", () => {
const result = calculateEstimatedSwapOutputAmount(
{
...exchangeInfoWithFees,
fees: {
...exchangeInfoWithFees.fees,
trade: new Percent(50, 100),
},
},
new TokenAmount(exchange.tokens[0], BigInt(100)),
);
// 50 percent fee
assertTokenAmount(result.outputAmountBeforeFees, BigInt(100));
assertTokenAmount(result.outputAmount, BigInt(50));
});
});
describe("#calculateEstimatedMintAmount", () => {
it("no fees if equal liquidity provision", () => {
const result = calculateEstimatedMintAmount(
{
...exchangeInfo,
fees: {
...ZERO_FEES,
trade: new Percent(50, 100),
},
},
BigInt(100),
BigInt(100),
);
assertTokenAmounts(result.mintAmount, result.mintAmountBeforeFees);
});
it("fees if unequal liquidity provision", () => {
const result = calculateEstimatedMintAmount(
{
...exchangeInfo,
fees: {
...ZERO_FEES,
trade: new Percent(50, 100),
},
},
BigInt(100_000),
BigInt(0),
);
assertTokenAmount(result.mintAmountBeforeFees, new BN(99_999));
// 3/4 because only half of the swapped amount (100 tokens) should have fees on it (so 1/4)
const expectedMintAmount =
(result.mintAmountBeforeFees.raw * BigInt(3)) / BigInt(4);
assertTokenAmount(result.mintAmount, expectedMintAmount);
assertTokenAmount(
result.fees,
result.mintAmountBeforeFees.raw - expectedMintAmount,
);
});
});
describe("#calculateEstimatedWithdrawAmount", () => {
it("works", () => {
calculateEstimatedWithdrawAmount({
...exchangeInfo,
poolTokenAmount: new TokenAmount(exchange.lpToken, 100_000),
});
});
it("works with fees", () => {
calculateEstimatedWithdrawAmount({
...exchangeInfoWithFees,
poolTokenAmount: new TokenAmount(exchange.lpToken, 100_000),
});
});
it("works zero with fees", () => {
calculateEstimatedWithdrawAmount({
...exchangeInfoWithFees,
poolTokenAmount: new TokenAmount(exchange.lpToken, 0),
});
});
});
describe("#calculateEstimatedWithdrawOneAmount", () => {
it("works", () => {
calculateEstimatedWithdrawOneAmount({
exchange: exchangeInfo,
poolTokenAmount: new TokenAmount(exchange.lpToken, 100_000),
withdrawToken: exchange.tokens[0],
});
});
it("works with fees", () => {
const result = calculateEstimatedWithdrawOneAmount({
exchange: exchangeInfoWithFees,
poolTokenAmount: new TokenAmount(exchange.lpToken, 100_000),
withdrawToken: exchange.tokens[0],
});
const resultMapped = mapValues(result, (q) => q.raw.toString());
expect(resultMapped).toEqual({
withdrawAmount: "99301",
withdrawAmountBeforeFees: "99900",
swapFee: "100",
withdrawFee: "500",
lpSwapFee: "50",
lpWithdrawFee: "250",
adminSwapFee: "50",
adminWithdrawFee: "250",
});
});
it("works zero with fees", () => {
const result = calculateEstimatedWithdrawOneAmount({
exchange: exchangeInfoWithFees,
poolTokenAmount: new TokenAmount(exchange.lpToken, 0),
withdrawToken: exchange.tokens[0],
});
const resultMapped = mapValues(result, (q) => q.raw.toString());
expect(resultMapped).toEqual({
withdrawAmount: "0",
withdrawAmountBeforeFees: "0",
swapFee: "0",
withdrawFee: "0",
lpSwapFee: "0",
lpWithdrawFee: "0",
adminSwapFee: "0",
adminWithdrawFee: "0",
});
});
});
});