import { NATIVE_MINT } from "@solana/spl-token"; import { PublicKey } from "@solana/web3.js"; import { BondingHierarchy } from "."; /** * Traverse a bonding hierarchy, executing func and accumulating * the results until destination token * * @param param0 * @returns */ function reduce({ hierarchy, func, initial, destination, wrappedSolMint, }: { hierarchy?: BondingHierarchy; func: (acc: A, current: BondingHierarchy) => A; initial: A; destination: PublicKey; wrappedSolMint: PublicKey; }): A { if ( !hierarchy || hierarchy.child?.tokenBonding.baseMint.equals(destination) ) { return initial; } if (destination?.equals(NATIVE_MINT)) { destination = wrappedSolMint; } let current: BondingHierarchy | undefined = hierarchy; let value = func(initial, current!); while (!current!.tokenBonding.baseMint.equals(destination)) { current = current!.parent; if (!current) { throw new Error( `Base mint ${destination.toBase58()} is not in the hierarchy for ${hierarchy.tokenBonding.publicKey.toBase58()}` ); } value = func(value, current); } return value; } /** * Traverse a bonding hierarchy, executing func and accumulating * the results until destination token starting from parent going to children * * @param param0 * @returns */ function reduceFromParent({ hierarchy, func, initial, destination, wrappedSolMint, }: { hierarchy?: BondingHierarchy; func: (acc: A, current: BondingHierarchy) => A; initial: A; destination: PublicKey; wrappedSolMint: PublicKey; }): A { if (!hierarchy) { return initial; } if (destination?.equals(NATIVE_MINT)) { destination = wrappedSolMint; } let current: BondingHierarchy | undefined = hierarchy; while (!current!.tokenBonding.baseMint.equals(destination)) { current = current!.parent; if (!current) { throw new Error( `Base mint ${destination.toBase58()} is not in the hierarchy for ${hierarchy.tokenBonding.publicKey.toBase58()}` ); } } destination = hierarchy.tokenBonding.targetMint; let value = func(initial, current!); while (!current!.tokenBonding.targetMint.equals(destination)) { current = current!.child; value = func(value, current!); } return value; } function now(): number { return new Date().valueOf() / 1000; } export interface IBondingPricing { get hierarchy(): BondingHierarchy; current(baseMint: PublicKey, unixTime?: number): number; locked(baseMint?: PublicKey): number; swap( baseAmount: number, baseMint: PublicKey, targetMint: PublicKey, ignoreFrozen: boolean, unixTime?: number, ): number; isBuying( lowMint: PublicKey, targetMint: PublicKey, ): boolean; swapTargetAmount( targetAmount: number, baseMint: PublicKey, targetMint: PublicKey, /** Ignore frozen curves, just compute the value. */ ignoreFreeze: boolean, unixTime?: number, ): number; sellTargetAmount( targetAmountNum: number, baseMint?: PublicKey, unixTime?: number ): number; buyTargetAmount( targetAmountNum: number, baseMint?: PublicKey, unixTime?: number ): number; buyWithBaseAmount( baseAmountNum: number, baseMint?: PublicKey, unixTime?: number ): number ; } export class BondingPricing implements IBondingPricing { hierarchy: BondingHierarchy; constructor(args: { hierarchy: BondingHierarchy }) { this.hierarchy = args.hierarchy; } current( baseMint?: PublicKey, unixTime?: number ): number { return reduce({ hierarchy: this.hierarchy, func: (acc: number, current: BondingHierarchy) => { return ( acc * current.pricingCurve.current( unixTime || now(), current.tokenBonding.buyBaseRoyaltyPercentage, current.tokenBonding.buyTargetRoyaltyPercentage ) ); }, initial: 1, destination: baseMint || this.hierarchy.tokenBonding.baseMint, wrappedSolMint: this.hierarchy.wrappedSolMint, }); } locked(baseMint?: PublicKey): number { return reduce({ hierarchy: this.hierarchy.parent, func: (acc: number, current: BondingHierarchy) => { return ( acc * current.pricingCurve.current( now(), current.tokenBonding.buyBaseRoyaltyPercentage, current.tokenBonding.buyTargetRoyaltyPercentage ) ); }, initial: this.hierarchy.pricingCurve.locked(), destination: baseMint || this.hierarchy.tokenBonding.baseMint, wrappedSolMint: this.hierarchy.wrappedSolMint, }); } swap( baseAmount: number, baseMint: PublicKey, targetMint: PublicKey, ignoreFrozen: boolean = false, unixTime?: number, ): number { const lowMint = this.hierarchy.lowest(baseMint, targetMint); const highMint = this.hierarchy.highest(baseMint, targetMint); const isBuying = this.isBuying( lowMint, targetMint, ); const path = this.hierarchy.path(lowMint, highMint, ignoreFrozen); if (path.length == 0) { throw new Error(`No path from ${baseMint} to ${targetMint}`); } if (isBuying) { return path.reverse().reduce((amount, { pricingCurve, tokenBonding }) => { return pricingCurve.buyWithBaseAmount( amount, tokenBonding.buyBaseRoyaltyPercentage, tokenBonding.buyTargetRoyaltyPercentage, unixTime ); }, baseAmount); } else { return path.reduce((amount, { pricingCurve, tokenBonding }) => { return pricingCurve.sellTargetAmount( amount, tokenBonding.sellBaseRoyaltyPercentage, tokenBonding.sellTargetRoyaltyPercentage, unixTime ); }, baseAmount); } } isBuying( lowMint: PublicKey, targetMint: PublicKey, ) { return lowMint.equals(targetMint); } swapTargetAmount( targetAmount: number, baseMint: PublicKey, targetMint: PublicKey, /** Ignore frozen curves, just compute the value. */ ignoreFreeze: boolean = false, unixTime?: number, ): number { const lowMint = this.hierarchy.lowest(baseMint, targetMint); const highMint = this.hierarchy.highest(baseMint, targetMint); const isBuying = this.isBuying( lowMint, targetMint, ); const path = this.hierarchy.path(lowMint, highMint, ignoreFreeze); if (path.length == 0) { throw new Error(`No path from ${baseMint} to ${targetMint}`); } return isBuying ? path.reverse().reduce((amount, { pricingCurve, tokenBonding }) => { return pricingCurve.buyWithBaseAmount( -amount, tokenBonding.sellBaseRoyaltyPercentage, tokenBonding.sellTargetRoyaltyPercentage, unixTime ); }, targetAmount) : path.reverse().reduce((amount, { pricingCurve, tokenBonding }) => { return pricingCurve.buyTargetAmount( amount, tokenBonding.buyBaseRoyaltyPercentage, tokenBonding.buyTargetRoyaltyPercentage, unixTime ); }, targetAmount); } sellTargetAmount( targetAmountNum: number, baseMint?: PublicKey, unixTime?: number ): number { return reduce({ hierarchy: this.hierarchy, func: (acc: number, current: BondingHierarchy) => { return current.pricingCurve.sellTargetAmount( acc, current.tokenBonding.sellBaseRoyaltyPercentage, current.tokenBonding.sellTargetRoyaltyPercentage, unixTime ); }, initial: targetAmountNum, destination: baseMint || this.hierarchy.tokenBonding.baseMint, wrappedSolMint: this.hierarchy.wrappedSolMint, }); } buyTargetAmount( targetAmountNum: number, baseMint?: PublicKey, unixTime?: number ): number { return reduce({ hierarchy: this.hierarchy, func: (acc: number, current: BondingHierarchy) => { return current.pricingCurve.buyTargetAmount( acc, current.tokenBonding.buyBaseRoyaltyPercentage, current.tokenBonding.buyTargetRoyaltyPercentage, unixTime ); }, initial: targetAmountNum, destination: baseMint || this.hierarchy.tokenBonding.baseMint, wrappedSolMint: this.hierarchy.wrappedSolMint, }); } buyWithBaseAmount( baseAmountNum: number, baseMint?: PublicKey, unixTime?: number ): number { return reduceFromParent({ hierarchy: this.hierarchy, func: (acc: number, current: BondingHierarchy) => { return current.pricingCurve.buyWithBaseAmount( acc, current.tokenBonding.buyBaseRoyaltyPercentage, current.tokenBonding.buyTargetRoyaltyPercentage, unixTime ); }, initial: baseAmountNum, destination: baseMint || this.hierarchy.tokenBonding.baseMint, wrappedSolMint: this.hierarchy.wrappedSolMint, }); } }