Skip to content

Commit

Permalink
[ECO-2104] Update market registration components (#234)
Browse files Browse the repository at this point in the history
  • Loading branch information
CRBl69 authored Sep 11, 2024
1 parent d0d6bb4 commit 89b13d4
Show file tree
Hide file tree
Showing 9 changed files with 192 additions and 60 deletions.
4 changes: 2 additions & 2 deletions src/docker/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ services:
NEXT_PUBLIC_INTEGRATOR_FEE_RATE_BPS: '${FEE_RATE_BPS}'
NEXT_PUBLIC_IS_ALLOWLIST_ENABLED: 'false'
NEXT_PUBLIC_MODULE_ADDRESS: '${EMOJICOIN_MODULE_ADDRESS}'
NEXT_PUBLIC_MQTT_URL: 'ws://broker:${BROKER_PORT}'
NEXT_PUBLIC_MQTT_URL: 'ws://localhost:${BROKER_PORT}'
NEXT_PUBLIC_REWARDS_MODULE_ADDRESS: >-
${EMOJICOIN_REWARDS_MODULE_ADDRESS}
REVALIDATION_TIME: '${REVALIDATION_TIME}'
Expand All @@ -120,7 +120,7 @@ services:
NEXT_PUBLIC_INTEGRATOR_FEE_RATE_BPS: '${FEE_RATE_BPS}'
NEXT_PUBLIC_IS_ALLOWLIST_ENABLED: 'false'
NEXT_PUBLIC_MODULE_ADDRESS: '${EMOJICOIN_MODULE_ADDRESS}'
NEXT_PUBLIC_MQTT_URL: 'ws://broker:${BROKER_PORT}'
NEXT_PUBLIC_MQTT_URL: 'ws://localhost:${BROKER_PORT}'
NEXT_PUBLIC_REWARDS_MODULE_ADDRESS: '${EMOJICOIN_REWARDS_MODULE_ADDRESS}'
REVALIDATION_TIME: '${REVALIDATION_TIME}'
healthcheck:
Expand Down
8 changes: 4 additions & 4 deletions src/docker/frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ FROM node:20.12.2

SHELL ["/bin/bash", "-o", "pipefail"]

RUN curl -fsSL https://get.pnpm.io/install.sh | \
ENV="$HOME/.bashrc" SHELL="$(which bash)" bash - \
&& mv /root/.local/share/pnpm/pnpm /usr/local/bin/
RUN ["bash", "-c", "curl -fsSL https://get.pnpm.io/install.sh | ENV=$HOME/.bashrc SHELL=$(which bash) bash -"]

RUN ["mv", "/root/.local/share/pnpm/pnpm", "/usr/local/bin"]

COPY . /

Expand All @@ -31,6 +31,6 @@ ENV INBOX_URL=$INBOX_URL \
NEXT_PUBLIC_REWARDS_MODULE_ADDRESS=$NEXT_PUBLIC_REWARDS_MODULE_ADDRESS \
REVALIDATION_TIME=$REVALIDATION_TIME

RUN pnpm install && pnpm run build
RUN ["bash", "-c", "pnpm install && pnpm run build"]

CMD ["pnpm", "run", "start", "-H", "0.0.0.0"]
7 changes: 5 additions & 2 deletions src/typescript/frontend/src/components/info/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import Image from "next/image";
import info from "../../../public/images/infoicon.svg";
import Popup from "components/popup";

const Info: React.FC<React.PropsWithChildren> = ({ children }) => (
<Popup className={"max-w-[300px]"} content={children}>
const Info: React.FC<React.PropsWithChildren<{ className?: string }>> = ({
children,
className,
}) => (
<Popup className={`max-w-[300px] ${className}`} content={children}>
<Image src={info} alt="info" />
</Popup>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { RegisterMarket, RegistryView } from "@sdk/emojicoin_dot_fun/emojicoin-dot-fun";
import { RegisterMarket } from "@sdk/emojicoin_dot_fun/emojicoin-dot-fun";
import { useAptos } from "context/wallet-context/AptosContextProvider";
import {
Ed25519PublicKey,
isUserTransactionResponse,
type PendingTransactionResponse,
type UserTransactionResponse,
} from "@aptos-labs/ts-sdk";
import { INTEGRATOR_ADDRESS } from "lib/env";
import { DEFAULT_REGISTER_MARKET_GAS_OPTIONS } from "@sdk/const";
import { toRegistryView } from "@sdk/types";
import {
MARKET_REGISTRATION_FEE,
MARKET_REGISTRATION_GAS_ESTIMATION_FIRST,
MARKET_REGISTRATION_GAS_ESTIMATION_NOT_FIRST,
} from "@sdk/const";
import { useEmojiPicker } from "context/emoji-picker-context";
import { SYMBOL_DATA } from "@sdk/emoji_data";
import { useNumMarkets } from "lib/hooks/queries/use-num-markets";
import { useQuery } from "@tanstack/react-query";

export const useRegisterMarket = () => {
const emojis = useEmojiPicker((state) => state.emojis);
Expand All @@ -18,7 +24,45 @@ export const useRegisterMarket = () => {
);
const clear = useEmojiPicker((state) => state.clear);
const setPickerInvisible = useEmojiPicker((state) => state.setPickerInvisible);
const { aptos, account, submit, signThenSubmit } = useAptos();
const { aptos, account, signThenSubmit } = useAptos();

const { data: numMarkets } = useNumMarkets();

const emojiBytes = emojis.map((e) => SYMBOL_DATA.byEmoji(e)!.bytes);

const { data: gasResult } = useQuery({
queryKey: ["register-market-cost", numMarkets, account?.address, emojiBytes],
queryFn: async () => {
const publicKey = new Ed25519PublicKey(
typeof account!.publicKey === "string" ? account!.publicKey : account!.publicKey[0]
);
const r = await RegisterMarket.getGasCost({
aptosConfig: aptos.config,
registrant: account!.address,
registrantPubKey: publicKey,
emojis: numMarkets === 0 ? [SYMBOL_DATA.byName("Virgo")!.bytes] : emojiBytes,
});
return r;
},
staleTime: 1000,
enabled:
numMarkets !== undefined && account !== null && (numMarkets === 0 || emojis.length > 0),
});

let amount: number, unitPrice: number;

if (gasResult && !gasResult.error) {
amount = gasResult.data.amount;
unitPrice = gasResult.data.unitPrice;
} else {
// If numMarkets is undefined (request not completed yet), we are ok with displaying the bigger number.
// And in most cases (every time except for the first market), it will actually be the correct one.
amount =
numMarkets === 0
? MARKET_REGISTRATION_GAS_ESTIMATION_FIRST / 100
: MARKET_REGISTRATION_GAS_ESTIMATION_NOT_FIRST / 100;
unitPrice = 100;
}

const registerMarket = async () => {
if (!account) {
Expand All @@ -31,37 +75,21 @@ export const useRegisterMarket = () => {
const builderArgs = {
aptosConfig: aptos.config,
registrant: account.address,
emojis: emojis.map((e) => SYMBOL_DATA.byEmoji(e)!.bytes),
emojis: emojiBytes,
integrator: INTEGRATOR_ADDRESS,
};
try {
const builderLambda = () => RegisterMarket.builder(builderArgs);
await submit(builderLambda).then((r) => {
res = r?.response ?? null;
error = r?.error;
const builderLambda = () =>
RegisterMarket.builder({
...builderArgs,
options: {
maxGasAmount: Math.round(amount * 1.2),
gasUnitPrice: unitPrice,
},
});
} catch (e) {
// TODO: Check if this works.
// If the market registration fails, it's possibly because it's the first market and the gas limit
// needs to be set very high. We'll check if the registry has 0 markets, and then try to manually
// set the gas limits and submit again.
const registryView = await RegistryView.view({
aptos,
}).then((r) => toRegistryView(r));

const builderLambda = () =>
RegisterMarket.builder({
...builderArgs,
options: DEFAULT_REGISTER_MARKET_GAS_OPTIONS,
});

if (registryView.numMarkets === 0n) {
await signThenSubmit(builderLambda).then((r) => {
res = r?.response ?? null;
error = r?.error;
});
}
}
await signThenSubmit(builderLambda).then((r) => {
res = r?.response ?? null;
error = r?.error;
});

if (res && isUserTransactionResponse(res)) {
clear();
Expand All @@ -79,5 +107,19 @@ export const useRegisterMarket = () => {
}
};

return registerMarket;
// By default, just consider that this is the price, since in 99.99% of cases, this will be the most accurate estimate.
let cost: number = Number(MARKET_REGISTRATION_FEE);

// If numMarkets is undefined (request not completed yet), we are ok with choosing the second option.
// And in most cases (every time except for the first market), it will actually be the correct one.
if (numMarkets === 0) {
cost = amount * unitPrice;
} else {
cost += amount * unitPrice;
}

return {
registerMarket,
cost,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,10 @@ import LaunchButtonOrGoToMarketLink from "./components/launch-or-goto";
import { sumBytes } from "@sdk/utils/sum-emoji-bytes";
import { useAptos } from "context/wallet-context/AptosContextProvider";
import { toCoinDecimalString } from "lib/utils/decimals";
import { MARKET_REGISTRATION_FEE, ONE_APTN } from "@sdk/const";
import { MARKET_REGISTRATION_DEPOSIT, ONE_APTN } from "@sdk/const";
import Info from "components/info";

const labelClassName = "whitespace-nowrap body-sm md:body-lg text-light-gray uppercase font-forma";
// This is the value that most wallets use. It's an estimate, possibly incorrect, but better for UX.
const ESTIMATED_GAS_REQUIREMENT = 300000;
const ESTIMATED_TOTAL_COST = Number(MARKET_REGISTRATION_FEE) + ESTIMATED_GAS_REQUIREMENT;

export const MemoizedLaunchAnimation = ({
loading,
Expand All @@ -34,17 +32,26 @@ export const MemoizedLaunchAnimation = ({
);
const { aptBalance, refetchIfStale } = useAptos();

const registerMarket = useRegisterMarket();
const { registerMarket, cost } = useRegisterMarket();
const { invalid, registered } = useIsMarketRegistered();

useEffect(() => {
return () => setIsLoadingRegisteredMarket(false);
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, []);

const sufficientBalance = useMemo(() => aptBalance >= MARKET_REGISTRATION_FEE, [aptBalance]);
const sufficientBalanceWithGas = useMemo(() => aptBalance >= ESTIMATED_TOTAL_COST, [aptBalance]);
const totalCost = useMemo(
() => (cost ? cost + Number(MARKET_REGISTRATION_DEPOSIT) : undefined),
[cost]
);

const sufficientBalance = useMemo(
() => (totalCost ? aptBalance >= totalCost : undefined),
[aptBalance, totalCost]
);

const displayCost = toCoinDecimalString(cost, 2);
const displayDeposit = toCoinDecimalString(MARKET_REGISTRATION_DEPOSIT, 2);
const numBytes = useMemo(() => {
return sumBytes(emojis);
}, [emojis]);
Expand Down Expand Up @@ -112,9 +119,21 @@ export const MemoizedLaunchAnimation = ({
<div className="flex flex-col justify-center m-auto pt-2 pixel-heading-4 uppercase">
<div className="flex flex-col text-dark-gray">
<div className="flex flex-row justify-between">
<span className="mr-[2ch]">{t("Cost to deploy") + ": "}</span>
<div className="flex flex-row items-center justify-left mr-[2ch]">
<span>{t("Cost to deploy")}</span>
<div className="mx-[5px]">
<Info className="uppercase">{`
The cost to deploy a market is ${displayCost} APT. You
will also make a ${displayDeposit} APT deposit that will
automatically be refunded when your market exits the
bonding curve.
`}</Info>
</div>
:
</div>
<div>
<span className="">1</span>&nbsp;APT
<span className="">{displayCost}</span>&nbsp;APT (+{" "}
<span className="">{displayDeposit}</span>&nbsp;APT)
</div>
</div>
<div className="flex flex-row justify-between">
Expand All @@ -130,6 +149,20 @@ export const MemoizedLaunchAnimation = ({
&nbsp;APT
</div>
</div>
<div className="flex flex-row justify-between">
<div className="flex flex-row items-center justify-left mr-[2ch]">
<span>{t("Grace period")}</span>
<div className="mx-[5px]">
<Info className="uppercase">
After a market is launched, there will be a grace period during which only the
account that launched the market can trade. The grace period ends after 5
minutes or after the first trade, whichever comes first.
</Info>
</div>
:
</div>
<div>5 minutes</div>
</div>
</div>
</div>
<motion.div
Expand All @@ -149,14 +182,6 @@ export const MemoizedLaunchAnimation = ({
/>
</motion.div>

{!sufficientBalanceWithGas && sufficientBalance && (
<div className="flex flex-row pixel-heading-4 uppercase mt-[1ch]">
<span className="text-error absolute w-full text-center">
{t("Your transaction may fail due to gas costs.")}
</span>
<span className="relative opacity-0">{"placeholder"}</span>
</div>
)}
<div className="h-[1ch] opacity-0">{"placeholder"}</div>
</motion.div>
) : (
Expand Down
22 changes: 22 additions & 0 deletions src/typescript/frontend/src/lib/hooks/queries/use-num-markets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { RegistryView } from "@sdk/emojicoin_dot_fun/emojicoin-dot-fun";
import { useQuery } from "@tanstack/react-query";
import { getAptos } from "lib/utils/aptos-client";
import { useEventStore } from "context/state-store-context";

async function getNumMarkets(): Promise<number> {
const aptos = getAptos();
return RegistryView.view({ aptos }).then((res) => Number(res.n_markets));
}

export const useNumMarkets = () => {
const numMarkets = useEventStore((s) => s.symbols.size);
const res = useQuery({
queryKey: ["num-markets", numMarkets],
queryFn: () => {
return getNumMarkets();
},
staleTime: 15000,
});

return res;
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const fetchNumMarkets = async () => {
return await cached(
async () => {
const { count, error } = await postgrest
.from("market_data")
.from("inbox_latest_state")
.select("*", { count: "exact", head: true });
if (error || count === null) {
throw new Error("Failed to fetch the number of markets.");
Expand Down
5 changes: 4 additions & 1 deletion src/typescript/sdk/src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ export const QUOTE_VIRTUAL_FLOOR = 400_000_000_000n;
export const BASE_VIRTUAL_CEILING = 49_000_000_000_000_000n;
export const QUOTE_VIRTUAL_CEILING = 1_400_000_000_000n;
export const POOL_FEE_RATE_BPS = 25;
export const MARKET_REGISTRATION_FEE = 100_000_000n;
export const MARKET_REGISTRATION_FEE = ONE_APTN;
export const MARKET_REGISTRATION_DEPOSIT = 4n * ONE_APTN;
export const MARKET_REGISTRATION_GAS_ESTIMATION_NOT_FIRST = 6000;
export const MARKET_REGISTRATION_GAS_ESTIMATION_FIRST = ONE_APT * 0.6;

export enum StateTrigger {
PACKAGE_PUBLICATION = 0,
Expand Down
37 changes: 37 additions & 0 deletions src/typescript/sdk/src/emojicoin_dot_fun/emojicoin-dot-fun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
type WaitForTransactionOptions,
type UserTransactionResponse,
type LedgerVersionArg,
type PublicKey,
} from "@aptos-labs/ts-sdk";
import {
type Option,
Expand Down Expand Up @@ -292,6 +293,42 @@ export class RegisterMarket extends EntryFunctionPayloadBuilder {
this.feePayer = feePayer !== undefined ? AccountAddress.from(feePayer) : undefined;
}

static async getGasCost(args: {
aptosConfig: AptosConfig;
registrant: AccountAddressInput; // &signer
registrantPubKey: PublicKey;
emojis: Array<HexInput>; // vector<vector<u8>>
}): Promise<{ data: { amount: number; unitPrice: number }; error: boolean }> {
const { aptosConfig } = args;
const payloadBuilder = new this({
...args,
integrator: AccountAddress.ONE,
});
const aptos = new Aptos(aptosConfig);
const transaction = await aptos.transaction.build.simple({
sender: payloadBuilder.primarySender,
data: {
function: `${payloadBuilder.moduleAddress}::${payloadBuilder.moduleName}::${payloadBuilder.functionName}`,
functionArguments: payloadBuilder.argsToArray(),
},
});
const [userTransactionResponse] = await aptos.transaction.simulate.simple({
signerPublicKey: args.registrantPubKey,
transaction,
options: {
estimateGasUnitPrice: true,
estimateMaxGasAmount: true,
},
});
return {
data: {
amount: Number(userTransactionResponse.gas_used),
unitPrice: Number(userTransactionResponse.gas_unit_price),
},
error: !userTransactionResponse.success,
};
}

static async builder(args: {
aptosConfig: AptosConfig;
registrant: AccountAddressInput; // &signer
Expand Down

0 comments on commit 89b13d4

Please sign in to comment.