Skip to content

Commit

Permalink
Add the calculated swap price to a hook for the frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
xbtmatt committed Nov 25, 2024
1 parent ee26bdf commit 1465f60
Show file tree
Hide file tree
Showing 7 changed files with 359 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { TradeOptions } from "components/selects/trade-options";
import { getMaxSlippageSettings } from "utils/slippage";
import { Emoji } from "utils/emoji";
import { EmojiPill } from "components/EmojiPill";
import { useCalculateSwapPrice } from "lib/hooks/use-calculate-swap-price";
import { INITIAL_REAL_RESERVES, INITIAL_VIRTUAL_RESERVES } from "@sdk/const";

const SimulateInputsWrapper = ({ children }: PropsWithChildren) => (
<div className="flex flex-col relative gap-[19px]">{children}</div>
Expand Down Expand Up @@ -89,10 +91,14 @@ export default function SwapComponent({
(s) => s.getMarket(marketEmojis)?.swapEvents.length ?? initNumSwaps
);

useEffect(() => {
const emojicoinType = toCoinTypes(marketAddress).emojicoin.toString();
setEmojicoinType(emojicoinType);
}, [marketAddress, setEmojicoinType]);
const lastSwapEvent = useEventStore((s) => s.getMarket(marketEmojis)?.swapEvents?.at(0));

const netProceeds = useCalculateSwapPrice({
lastSwapEvent,
isSell,
inputAmount,
userEmojicoinBalance: emojicoinBalance,
});

const swapData = useSimulateSwap({
marketAddress,
Expand All @@ -101,6 +107,31 @@ export default function SwapComponent({
numSwaps,
});

useEffect(() => {
console.dir(
{
msg: "num swaps for simulated vs client-side",
simulated: {
numSwaps,
},
calculated: {
numSwaps: lastSwapEvent?.state.cumulativeStats.numSwaps,
clamm: lastSwapEvent?.state.clammVirtualReserves ?? INITIAL_VIRTUAL_RESERVES,
cpamm: lastSwapEvent?.state.cpammRealReserves ?? INITIAL_REAL_RESERVES,
bondingCurve: lastSwapEvent?.swap.startsInBondingCurve,
},
},
{ depth: null }
);
console.log("swap result from simulated swap data:", swapData?.swapResult);
console.log("net proceeds from client-side calculation", netProceeds);
}, [swapData, netProceeds]);

useEffect(() => {
const emojicoinType = toCoinTypes(marketAddress).emojicoin.toString();
setEmojicoinType(emojicoinType);
}, [marketAddress, setEmojicoinType]);

const { swapResult, gasCost, gasCostWasUndefined } = swapData
? {
swapResult: swapData.swapResult,
Expand Down
72 changes: 72 additions & 0 deletions src/typescript/frontend/src/lib/hooks/use-calculate-swap-price.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { INITIAL_REAL_RESERVES, INITIAL_VIRTUAL_RESERVES } from "@sdk/const";
import {
calculateSwapNetProceeds,
SwapNetProceedsArgs,
SwapNotEnoughBaseError,
} from "@sdk/emojicoin_dot_fun/calculate-swap-price";
import { DatabaseModels } from "@sdk/indexer-v2/types";
import { AnyNumberString, Types } from "@sdk/types/types";

/**
* This hook calls the client-side calculation of the swap net proceeds amount.
* If the Move contract logic would result in an error being thrown, it's captured
* in the `error` field returned by the calculation function called in this hook.
*
* In order to not disrupt the execution flow and see the resulting output swap price
* regardless of the input user balance, we re-run the client-side simulation if the input
* is invalid due to insufficient balance, but if the simulation results in a divide by
* zero error, we don't return, since that is due to invalid input amount.
*/
export const useCalculateSwapPrice = ({
lastSwapEvent,
isSell,
inputAmount,
userEmojicoinBalance,
}: {
lastSwapEvent?: DatabaseModels["swap_events"];
isSell: boolean;
inputAmount: AnyNumberString;
userEmojicoinBalance: AnyNumberString;
}) => {
const args: SwapNetProceedsArgs = {
...getReservesAndBondingCurveStateWithDefault(lastSwapEvent),
isSell,
inputAmount,
userEmojicoinBalance,
};
const res = calculateSwapNetProceeds(args);
// If the error is due to an insufficient user balance,
// simulate the calculation again, ensuring that the
// user emojicoin balance is sufficient.
if (res.error instanceof SwapNotEnoughBaseError) {
const { netProceeds: recalculatedNetProceeds } = calculateSwapNetProceeds({
...args,
userEmojicoinBalance: BigInt(args.inputAmount) + 1n,
});
// Force the return type to show that the error was an insufficient balance.
return {
netProceeds: recalculatedNetProceeds,
error: res.error,
};
}
// Otherwise, just return the result.
// Note that this \may return a divide by zero error still.
return res;
};

const getReservesAndBondingCurveStateWithDefault = (
lastSwapEvent?: DatabaseModels["swap_events"]
) => {
if (lastSwapEvent) {
return {
clammVirtualReserves: lastSwapEvent.state.clammVirtualReserves,
cpammRealReserves: lastSwapEvent.state.cpammRealReserves,
startsInBondingCurve: lastSwapEvent.swap.startsInBondingCurve,
};
}
return {
clammVirtualReserves: INITIAL_VIRTUAL_RESERVES,
cpammRealReserves: INITIAL_REAL_RESERVES,
startsInBondingCurve: false,
};
};
22 changes: 22 additions & 0 deletions src/typescript/sdk/src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import Big from "big.js";
import { type ValueOf } from "./utils/utility-types";
import { type DatabaseStructType } from "./indexer-v2/types/json-types";
import { Types } from "./types";

Check failure on line 11 in src/typescript/sdk/src/const.ts

View workflow job for this annotation

GitHub Actions / pre-commit

All imports in the declaration are only used as types. Use `import type`

export const VERCEL = process.env.VERCEL === "1";
if (
Expand Down Expand Up @@ -98,6 +99,27 @@ export const MARKET_REGISTRATION_FEE = ONE_APT_BIGINT;
export const MARKET_REGISTRATION_DEPOSIT = 1n * ONE_APT_BIGINT;
export const MARKET_REGISTRATION_GAS_ESTIMATION_NOT_FIRST = ONE_APT * 0.005;
export const MARKET_REGISTRATION_GAS_ESTIMATION_FIRST = ONE_APT * 0.6;
export const BASIS_POINTS_PER_UNIT = 10_000n;
/**
* A market's virtual reserves upon creation. Used to calculate the swap price
* of a market when no swaps exist yet.
*
* @see {@link https://github.com/econia-labs/emojicoin-dot-fun/blob/295cf611950f66651452baa3e6ad6d6aef583f9b/src/move/emojicoin_dot_fun/sources/emojicoin_dot_fun.move#L2030}
*/
export const INITIAL_VIRTUAL_RESERVES: Types["Reserves"] = {
base: BASE_VIRTUAL_CEILING,
quote: QUOTE_VIRTUAL_FLOOR,
}

/**
* A market's real reserves upon creation.
*
* @see {@link INITIAL_VIRTUAL_RESERVES}
*/
export const INITIAL_REAL_RESERVES: Types["Reserves"] = {
base: 0n,
quote: 0n,
}

/// As defined in the database, aka the enum string.
export enum Period {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import {
// BASE_VIRTUAL_CEILING,
BASE_VIRTUAL_FLOOR,
BASIS_POINTS_PER_UNIT,
EMOJICOIN_REMAINDER,
// EMOJICOIN_SUPPLY,
INTEGRATOR_FEE_RATE_BPS,
POOL_FEE_RATE_BPS,
QUOTE_REAL_CEILING,
QUOTE_VIRTUAL_CEILING,
} from "@sdk/const";
import { type AnyNumberString, type Types } from "@sdk/types/types";
} from "../const";
import { type AnyNumberString, type Types } from "../types";
import Big from "big.js";

export class CustomCalculatedSwapError extends Error {
Expand All @@ -31,64 +30,66 @@ export class DivideByZeroError extends CustomCalculatedSwapError {
}
}

export type SwapNetProceedsArgs = {
clammVirtualReserves: Types["Reserves"];
cpammRealReserves: Types["Reserves"];
startsInBondingCurve: boolean;
isSell: boolean;
inputAmount: AnyNumberString;
userEmojicoinBalance: AnyNumberString;
}

/**
* NOTE: This function throws an error just like the move code.
* Use a try/catch around it to catch invalid inputs/invalid on-chain state.
*
* @throws {@link SwapNotEnoughBaseError} if the user does not have enough base
* @throws {@link DivideByZeroError} if the cpamm output calculation results in a divide by zero
*/
export const calculatedSwap = ({
marketData,
inputAmountMaybeString,
userEmojicoinBalance,
}: {
marketData: {
clammVirtualReserves: Types["Reserves"];
cpammRealReserves: Types["Reserves"];
// inBondingCurve: boolean;
isSell: boolean;
startsInBondingCurve: boolean;
};
inputAmountMaybeString: AnyNumberString;
userEmojicoinBalance: AnyNumberString;
}) => {
const inputAmount = Big(BigInt(inputAmountMaybeString).toString());
const calculateExactSwapNetProceeds = (args: SwapNetProceedsArgs) => {
const {
clammVirtualReserves,
cpammRealReserves,
startsInBondingCurve,
userEmojicoinBalance,
isSell,
} = args;
const inputAmount = Big(BigInt(args.inputAmount).toString());
const balanceBefore = Big(BigInt(userEmojicoinBalance).toString());
const { clammVirtualReserves, cpammRealReserves, isSell, startsInBondingCurve } = marketData;

let poolFee: Big = Big(0);
let baseVolume: Big;
let quoteVolume: Big;
let integratorFee: Big;
let resultsInStateTransition = false;

// SELLING.
// -------------------------- Selling --------------------------
if (isSell) {
const ammQuoteOutput = startsInBondingCurve
? cpammSimpleSwapOutputAmount({
inputAmount,
isSell,
reserves: clammVirtualReserves,
})
: cpammSimpleSwapOutputAmount({
inputAmount,
isSell,
reserves: cpammRealReserves,
});
let ammQuoteOutput: Big;
if (startsInBondingCurve) {
poolFee = getBPsFee(ammQuoteOutput);
ammQuoteOutput = cpammSimpleSwapOutputAmount({

Check failure on line 67 in src/typescript/sdk/src/emojicoin_dot_fun/calculate-swap-price.ts

View workflow job for this annotation

GitHub Actions / pre-commit

'cpammSimpleSwapOutputAmount' was used before it was defined
inputAmount,
isSell,
reserves: clammVirtualReserves,
});
} else {
ammQuoteOutput = cpammSimpleSwapOutputAmount({

Check failure on line 73 in src/typescript/sdk/src/emojicoin_dot_fun/calculate-swap-price.ts

View workflow job for this annotation

GitHub Actions / pre-commit

'cpammSimpleSwapOutputAmount' was used before it was defined
inputAmount,
isSell,
reserves: cpammRealReserves,
});
poolFee = getBPsFee(ammQuoteOutput, POOL_FEE_RATE_BPS);

Check failure on line 78 in src/typescript/sdk/src/emojicoin_dot_fun/calculate-swap-price.ts

View workflow job for this annotation

GitHub Actions / pre-commit

'getBPsFee' was used before it was defined
}
integratorFee = getBPsFee(ammQuoteOutput);
integratorFee = getBPsFee(ammQuoteOutput, INTEGRATOR_FEE_RATE_BPS);

Check failure on line 80 in src/typescript/sdk/src/emojicoin_dot_fun/calculate-swap-price.ts

View workflow job for this annotation

GitHub Actions / pre-commit

'getBPsFee' was used before it was defined
baseVolume = inputAmount; /* eslint-disable-line */
quoteVolume = ammQuoteOutput.minus(poolFee).minus(integratorFee);
const netProceeds = quoteVolume;
if (!inputAmount.lte(balanceBefore)) {
throw new SwapNotEnoughBaseError();
}
// const balanceAfter = balanceBefore.minus(inputAmount);
// Unused, but kept here to preserve the Move code flow.
const _balanceAfter = balanceBefore.minus(inputAmount);
return netProceeds;
} else {
// BUYING.
integratorFee = getBPsFee(inputAmount);
// -------------------------- Buying --------------------------
integratorFee = getBPsFee(inputAmount, INTEGRATOR_FEE_RATE_BPS);

Check failure on line 92 in src/typescript/sdk/src/emojicoin_dot_fun/calculate-swap-price.ts

View workflow job for this annotation

GitHub Actions / pre-commit

'getBPsFee' was used before it was defined
quoteVolume = inputAmount.minus(integratorFee);
if (startsInBondingCurve) {
const maxQuoteVolumeInClamm = Big(
Expand Down Expand Up @@ -118,9 +119,8 @@ export const calculatedSwap = ({
quote: QUOTE_REAL_CEILING,
},
});
poolFee = getBPsFee(cpammBaseOutput);
poolFee = getBPsFee(cpammBaseOutput, POOL_FEE_RATE_BPS);

Check failure on line 122 in src/typescript/sdk/src/emojicoin_dot_fun/calculate-swap-price.ts

View workflow job for this annotation

GitHub Actions / pre-commit

'getBPsFee' was used before it was defined
baseVolume = baseVolume.plus(cpammBaseOutput).minus(poolFee);
// baseVolume = cpammBaseOutput.minus(poolFee);
}
}
} else {
Expand All @@ -130,17 +130,65 @@ export const calculatedSwap = ({
isSell,
reserves: cpammRealReserves,
});
const poolFee = getBPsFee(cpammBaseOutput);
const poolFee = getBPsFee(cpammBaseOutput, POOL_FEE_RATE_BPS);
baseVolume = cpammBaseOutput.minus(poolFee);
}
const netProceeds = baseVolume;
// Unused, but kept here to preserve the Move code flow.
const _balanceAfter = balanceBefore.plus(netProceeds);
return netProceeds;
}
};

const getBPsFee = (principal: Big) =>
principal.mul(POOL_FEE_RATE_BPS).div(BASIS_POINTS_PER_UNIT.toString());
type NetProceedsReturnTypes = {
netProceeds: bigint;
error: null;
} | {
netProceeds: 0n;
error: CustomCalculatedSwapError;
}

/**
* The wrapper function for calculating the swap proceeds. This function rounds
* the returned value down like the Move contract does, since technically
* this code is more precise than the Move code with truncated uint values.
*
* @returns the total net proceeds- denominated in quote or volume based on `isSell`.
* @returns 0 if the calculation results in an error.
*/
export const calculateSwapNetProceeds = (args: {
clammVirtualReserves: Types["Reserves"];
cpammRealReserves: Types["Reserves"];
startsInBondingCurve: boolean;
isSell: boolean;
inputAmount: AnyNumberString;
userEmojicoinBalance: AnyNumberString;
}): NetProceedsReturnTypes => {
try {
const res = calculateExactSwapNetProceeds(args);
// Round down to zero decimal places like an unsigned integer will in Move.
const netProceeds = BigInt(res.round(0, Big.roundDown).toString());
return {
netProceeds,
error: null,
};
} catch (e) {
if (e instanceof CustomCalculatedSwapError) {
return {
netProceeds: 0n,
error: e,
};
}
console.warn(`Unexpected error when calculating swap ${e}`);
return {
netProceeds: 0n,
error: null,
};
}
};

const getBPsFee = (principal: Big, feeRateBPs: AnyNumberString) =>
principal.mul(feeRateBPs.toString()).div(BASIS_POINTS_PER_UNIT.toString());

const cpammSimpleSwapOutputAmount = ({
inputAmount,
Expand All @@ -161,26 +209,3 @@ const cpammSimpleSwapOutputAmount = ({
}
return numerator.div(denominator);
};

// const assignSupplyMinuendAndReserves = ({
// clammVirtualReserves,
// cpammRealReserves,
// inBondingCurve,
// }: {
// clammVirtualReserves: Types["Reserves"];
// cpammRealReserves: Types["Reserves"];
// inBondingCurve: boolean;
// }): {
// supplyMinuend: bigint;
// reserves: Types["Reserves"];
// } => {
// return inBondingCurve
// ? {
// supplyMinuend: BASE_VIRTUAL_CEILING,
// reserves: clammVirtualReserves,
// }
// : {
// supplyMinuend: EMOJICOIN_SUPPLY,
// reserves: cpammRealReserves,
// };
// };
Loading

0 comments on commit 1465f60

Please sign in to comment.