Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(arena): improved pool creation flow #1099

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { useIsMobile } from "~/hooks/use-is-mobile";

import { Dialog, DialogContent, DialogTrigger } from "~/components/ui/dialog";
import { Button } from "~/components/ui/button";
import type { TokenData } from "~/types";

import { PoolData, PoolMintData, CreatePoolState } from "./types";
import {
Expand All @@ -23,6 +22,7 @@ import {
CreatePoolConfigure,
CreatePoolReview,
} from "./components";
import { TokenData } from "~/types";

type CreatePoolDialogProps = {
trigger?: React.ReactNode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
getConfig,
instructions,
} from "@mrgnlabs/marginfi-client-v2";
import { cn, getBearerToken, getFeeAccount, createReferalTokenAccount } from "@mrgnlabs/mrgn-utils";
import { cn, getBearerToken, getFeeAccount, createReferalTokenAccountIxs } from "@mrgnlabs/mrgn-utils";
import { addTransactionMetadata, SolanaTransaction, TransactionType } from "@mrgnlabs/mrgn-common";

import { Button } from "~/components/ui/button";
Expand Down Expand Up @@ -54,11 +54,12 @@ export const CreatePoolLoading = ({ poolData, setPoolData, setCreatePoolState }:

const steps = React.useMemo(
() => [
{ label: "Step 1", description: "Setting up a switchboard oracle" },
{ label: "Step 2", description: "Generating transactions" },
{ label: "Step 3", description: "Indexing pool" },
{ label: "Step 4", description: "Executing transactions" },
{ label: "Step 5", description: "Finalizing pool" },
{ label: "Step 1", description: "Setting up oracles" },
{ label: "Step 2", description: "Saving token image data" },
{ label: "Step 3", description: "Generating transactions" },
{ label: "Step 4", description: "Indexing pool" },
{ label: "Step 5", description: "Executing transactions" },
{ label: "Step 6", description: "Finalizing pool" },
],
[]
);
Expand Down Expand Up @@ -86,15 +87,20 @@ export const CreatePoolLoading = ({ poolData, setPoolData, setCreatePoolState }:
[connection, wallet]
);

const savePermissionlessPool = async (poolObject: { group: string; asset: string; quote: string; lut: string }) => {
const savePermissionlessPool = async (poolObject: {
group: string;
asset: string;
quote: string;
lut: string;
admin: string;
}) => {
try {
const formattedPoolObject = {
base_bank: poolObject.asset,
created_by: "mfi1dtjy2mJ9J21UoaQ5dsRnbcg4MBU1CTacVyBp1HF", // TODO: update this
featured: true,
created_by: poolObject.admin,
group: poolObject.group,
lookup_tables: [poolObject.lut],
quote_banks: [poolObject.quote],
quote_bank: poolObject.quote,
};

const response = await fetch("/api/pool/create", {
Expand All @@ -107,7 +113,6 @@ export const CreatePoolLoading = ({ poolData, setPoolData, setCreatePoolState }:

if (response.ok) {
const data = await response.json();
console.log("Pool added:", data);
return data;
} else {
console.error("Failed to add pool");
Expand All @@ -123,7 +128,8 @@ export const CreatePoolLoading = ({ poolData, setPoolData, setCreatePoolState }:

const verifiedPoolData = verifyPoolData(poolData);
if (!verifiedPoolData) return;
const { tokenMint, quoteMint, tokenSymbol, quoteSymbol, tokenConfig, quoteConfig } = verifiedPoolData;
const { tokenMint, quoteMint, tokenSymbol, quoteSymbol, tokenConfig, quoteConfig, tokenIcon, quoteIcon } =
verifiedPoolData;

setStatus("loading");

Expand All @@ -142,21 +148,19 @@ export const CreatePoolLoading = ({ poolData, setPoolData, setCreatePoolState }:
let updatedTokenOracleConfig = { ...tokenConfig.oracleConfig };
let updatedQuoteOracleConfig = { ...quoteConfig.oracleConfig };

if (updatedTokenOracleConfig?.keys?.length === 0) {
if (!updatedTokenOracleConfig?.keys || updatedTokenOracleConfig?.keys?.length === 0) {
const oracleCreationToken = await initializeOracle(tokenMint, tokenSymbol);
if (!oracleCreationToken) throw new Error("Oracle creation failed");

updatedTokenOracleConfig = {
setup: OracleSetup.SwitchboardPull,
keys: [oracleCreationToken.feedPubkey],
};
pullFeedIx.push(oracleCreationToken);
}

if (updatedQuoteOracleConfig?.keys?.length === 0) {
if (!updatedQuoteOracleConfig?.keys || updatedQuoteOracleConfig?.keys?.length === 0) {
const oracleCreationQuote = await initializeOracle(quoteMint, quoteSymbol);
if (!oracleCreationQuote) throw new Error("Oracle creation failed");

updatedQuoteOracleConfig = {
setup: OracleSetup.SwitchboardPull,
keys: [oracleCreationQuote.feedPubkey],
Expand All @@ -166,6 +170,23 @@ export const CreatePoolLoading = ({ poolData, setPoolData, setCreatePoolState }:

setActiveStep(1);

// upload images
const uploadImageResponse = await uploadImage(tokenIcon, tokenMint.toBase58());
if (!uploadImageResponse) {
setStatus("error");
console.error("Failed to upload image");
return;
}

const uploadQuoteImageResponse = await uploadImage(quoteIcon, quoteMint.toBase58());
if (!uploadQuoteImageResponse) {
setStatus("error");
console.error("Failed to upload image");
return;
}

setActiveStep(2);

// create group ix
const groupIxWrapped = await client.makeCreateMarginfiGroupIx(seeds.marginfiGroupSeed.publicKey);
// create bank ix wrapper (quote)
Expand Down Expand Up @@ -194,7 +215,6 @@ export const CreatePoolLoading = ({ poolData, setPoolData, setCreatePoolState }:
);

// add oracle to banks

if (
!updatedTokenOracleConfig.setup ||
!updatedTokenOracleConfig.keys ||
Expand All @@ -204,22 +224,22 @@ export const CreatePoolLoading = ({ poolData, setPoolData, setCreatePoolState }:
throw new Error("Oracle setup or keys not found");
}

const addOracleToBanksIxs = [];
addOracleToBanksIxs.push(
await addOracleToBanksIx(
client.program,
seeds.stableBankSeed.publicKey,
updatedQuoteOracleConfig.keys[0],
updatedQuoteOracleConfig.setup
)
const addOracleToQuoteBankIx = await addOracleToBanksIx(
client.program,
seeds.stableBankSeed.publicKey,
updatedQuoteOracleConfig.keys[0],
updatedQuoteOracleConfig.setup,
seeds.marginfiGroupSeed.publicKey,
wallet.publicKey
);
addOracleToBanksIxs.push(
await addOracleToBanksIx(
client.program,
seeds.tokenBankSeed.publicKey,
updatedTokenOracleConfig.keys[0],
updatedTokenOracleConfig.setup
)

const addOracleToTokenBankIx = await addOracleToBanksIx(
client.program,
seeds.tokenBankSeed.publicKey,
updatedTokenOracleConfig.keys[0],
updatedTokenOracleConfig.setup,
seeds.marginfiGroupSeed.publicKey,
wallet.publicKey
);

// create lut ix
Expand All @@ -237,8 +257,7 @@ export const CreatePoolLoading = ({ poolData, setPoolData, setCreatePoolState }:
const feeAccount = getFeeAccount(tokenMint);
let referralTokenAccountIxs: TransactionInstruction[] = [];
if (feeAccount) {
referralTokenAccountIxs = (await createReferalTokenAccount(connection, wallet.publicKey, tokenMint))
.instructions;
referralTokenAccountIxs = await createReferalTokenAccountIxs(connection, wallet.publicKey, tokenMint);
}

// transactions
Expand All @@ -247,7 +266,7 @@ export const CreatePoolLoading = ({ poolData, setPoolData, setCreatePoolState }:
value: { blockhash },
} = await connection.getLatestBlockhashAndContext();

// bundle tip & create oracle transaction
// create oracle transaction
pullFeedIx.forEach((ix) => {
transactions.push(createTransaction([ix.pullFeedIx], wallet.publicKey, [ix.feedSeed], blockhash));
});
Expand All @@ -262,34 +281,35 @@ export const CreatePoolLoading = ({ poolData, setPoolData, setCreatePoolState }:
)
);

// create quote bank & referal token account transaction
// create quote bank & referal token account transaction ...referralTokenAccountIxs
transactions.push(
createTransaction(
[...quoteBankIxWrapper.instructions, ...referralTokenAccountIxs],
[...quoteBankIxWrapper.instructions, ...addOracleToQuoteBankIx.instructions],
wallet.publicKey,
[seeds.stableBankSeed, ...quoteBankIxWrapper.keys],
[seeds.stableBankSeed, ...quoteBankIxWrapper.keys, ...addOracleToQuoteBankIx.keys],
blockhash
)
);

// create token bank transaction
transactions.push(
createTransaction(
[...tokenBankIxWrapper.instructions],
[...tokenBankIxWrapper.instructions, ...addOracleToTokenBankIx.instructions],
wallet.publicKey,
[seeds.tokenBankSeed, ...tokenBankIxWrapper.keys],
blockhash
)
);

setActiveStep(2);
setActiveStep(3);

try {
const response = await savePermissionlessPool({
group: seeds.marginfiGroupSeed.publicKey.toBase58(),
asset: seeds.tokenBankSeed.publicKey.toBase58(),
quote: seeds.stableBankSeed.publicKey.toBase58(),
lut: lutAddress.toBase58(),
admin: wallet.publicKey.toBase58(),
});
console.log("Pool saved:", response);
} catch (error) {
Expand All @@ -298,7 +318,7 @@ export const CreatePoolLoading = ({ poolData, setPoolData, setCreatePoolState }:
throw error;
}

setActiveStep(3);
setActiveStep(4);

// transaction execution
const sigs = await client.processTransactions(transactions, {
Expand All @@ -318,14 +338,14 @@ export const CreatePoolLoading = ({ poolData, setPoolData, setCreatePoolState }:
console.error("Failed to create permissionless pool");
console.error(error);
}
}, [connection, createOracleIx, initializeClient, poolData, setCreatePoolState, setPoolData, wallet]);
}, [connection, initializeClient, initializeOracle, poolData, setCreatePoolState, setPoolData, wallet.publicKey]);

React.useEffect(() => {
if (!initialized.current) {
initialized.current = true;
createPermissionlessBankBundle();
}
}, []);
}, [createPermissionlessBankBundle]);

return (
<>
Expand Down Expand Up @@ -398,12 +418,22 @@ type VerifiedPoolData = {
quoteMint: PublicKey;
tokenSymbol: string;
quoteSymbol: string;
tokenIcon: string;
quoteIcon: string;
tokenConfig: { bankConfig: BankConfigOpt; oracleConfig: OracleConfigOpt | null };
quoteConfig: { bankConfig: BankConfigOpt; oracleConfig: OracleConfigOpt | null };
};

const verifyPoolData = (poolData: PoolData | null): VerifiedPoolData | null => {
if (!poolData || !poolData.token || !poolData.quoteToken || !poolData.tokenConfig || !poolData.quoteTokenConfig) {
if (
!poolData ||
!poolData.token ||
!poolData.quoteToken ||
!poolData.tokenConfig ||
!poolData.quoteTokenConfig ||
!poolData.token.icon ||
!poolData.quoteToken.icon
) {
return null;
}

Expand All @@ -414,5 +444,46 @@ const verifyPoolData = (poolData: PoolData | null): VerifiedPoolData | null => {
quoteSymbol: poolData.quoteToken.symbol,
tokenConfig: poolData.tokenConfig,
quoteConfig: poolData.quoteTokenConfig,
tokenIcon: poolData.token.icon,
quoteIcon: poolData.quoteToken.icon,
};
};

const uploadImage = async (fileUrl: string, mint: string): Promise<boolean> => {
try {
// Fetch the file from the URL
const fileResponse = await fetch(fileUrl);
if (!fileResponse.ok) throw new Error("Failed to fetch file from URL");

// Convert the response to a Blob
const fileBlob = await fileResponse.blob();
const fileParts = fileUrl.split("/");
const fileName = fileParts[fileParts.length - 1];
const extension = fileName.split(".").pop() || "";
const filename = `${mint}.${extension}`;

// Prepare the upload request
const response = await fetch(`/api/pool/addImage?filename=${filename}`, {
method: "POST",
});

if (response.status === 409) return true;

const { url, fields } = await response.json();
const formData = new FormData();
Object.entries({ ...fields, file: fileBlob }).forEach(([key, value]) => {
formData.append(key, value as string | Blob);
});

// Upload the file
const upload = await fetch(url, {
method: "POST",
body: formData,
});

return upload.ok;
} catch (error) {
console.error("Error uploading image:", error);
return false;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { Input } from "~/components/ui/input";
import { Skeleton } from "~/components/ui/skeleton";
import { useTradeStoreV2 } from "~/store";
import { ArenaPoolSummary } from "~/types/trade-store.types";
import { TOKEN_ICON_BASE_URL } from "~/config/trade";

import type { PoolMintData } from "../types";

type CreatePoolMintProps = {
Expand Down Expand Up @@ -152,7 +154,7 @@ export const CreatePoolQuote = ({ tokenData, isSearchingToken, setIsOpen, fetchT
<div className="flex flex-col justify-center items-center w-full max-w-sm">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={getTokenImageURL(new PublicKey(poolExists.tokenSummary.mint))}
src={getTokenImageURL(new PublicKey(poolExists.tokenSummary.mint), TOKEN_ICON_BASE_URL)}
className="rounded-full"
width={48}
height={48}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,6 @@ export const CreatePoolToken = ({ isSearchingToken, setIsOpen, fetchTokenInfo }:
fetchTrendingTokens();
}, [trendingTokens, fetchTrendingTokens, initialized]);

console.log(trendingTokens);

return (
<>
<div className="text-center space-y-2 w-full mx-auto">
Expand Down
Loading