From 3cf40d2a31cad30d2ad2a41bd64aad1b2169036f Mon Sep 17 00:00:00 2001 From: SGiaccobasso Date: Mon, 24 Feb 2025 15:10:20 -0300 Subject: [PATCH 1/5] chore: add selecting type and storing correctly to db --- ...ndRegisterRemoteInterchainTokenMutation.ts | 4 +- .../steps/token-details/TokenDetails.tsx | 48 ++++++++++++++----- .../src/features/suiHooks/useDeployToken.ts | 20 +++----- apps/maestro/src/server/routers/sui/index.ts | 29 ++++++++--- .../src/server/routers/sui/utils/utils.ts | 17 ++----- 5 files changed, 70 insertions(+), 48 deletions(-) diff --git a/apps/maestro/src/features/InterchainTokenDeployment/hooks/useDeployAndRegisterRemoteInterchainTokenMutation.ts b/apps/maestro/src/features/InterchainTokenDeployment/hooks/useDeployAndRegisterRemoteInterchainTokenMutation.ts index eb4a51fa2..584a0c825 100644 --- a/apps/maestro/src/features/InterchainTokenDeployment/hooks/useDeployAndRegisterRemoteInterchainTokenMutation.ts +++ b/apps/maestro/src/features/InterchainTokenDeployment/hooks/useDeployAndRegisterRemoteInterchainTokenMutation.ts @@ -267,6 +267,7 @@ export function useDeployAndRegisterRemoteInterchainTokenMutation( name: input.tokenName, decimals: input.decimals, destinationChainIds: input.destinationChainIds, + minterAddress: input.minterAddress, }); if (result?.digest && result.deploymentMessageId) { const token: any = result?.events?.[0]?.parsedJson; @@ -279,8 +280,9 @@ export function useDeployAndRegisterRemoteInterchainTokenMutation( tokenName: input.tokenName, tokenSymbol: input.tokenSymbol, tokenDecimals: input.decimals, + tokenManagerType: result.tokenManagerType, axelarChainId: input.sourceChainId, - originalMinterAddress: input.minterAddress, + originalMinterAddress: result.minterAddress, destinationAxelarChainIds: input.destinationChainIds, tokenManagerAddress: result.tokenManagerAddress, tokenAddress: result.tokenAddress, diff --git a/apps/maestro/src/features/InterchainTokenDeployment/steps/token-details/TokenDetails.tsx b/apps/maestro/src/features/InterchainTokenDeployment/steps/token-details/TokenDetails.tsx index a1dcc815a..898608487 100644 --- a/apps/maestro/src/features/InterchainTokenDeployment/steps/token-details/TokenDetails.tsx +++ b/apps/maestro/src/features/InterchainTokenDeployment/steps/token-details/TokenDetails.tsx @@ -11,6 +11,8 @@ import { Maybe } from "@axelarjs/utils"; import { ComponentRef, useMemo, useRef, type FC } from "react"; import { type FieldError, type SubmitHandler } from "react-hook-form"; +import { isValidSuiAddress } from "@mysten/sui/utils"; + import "~/features/InterchainTokenDeployment"; import { @@ -32,6 +34,33 @@ import { const MAX_UINT64 = BigInt(2) ** BigInt(64) - BigInt(1); +const validateMinterAddress = (minter: string, chainId: number) => { + if (!minter) { + return { + type: "validate", + message: "Minter address is required", + }; + } + + if (chainId === SUI_CHAIN_ID) { + if (!isValidSuiAddress(minter)) { + return { + type: "validate", + message: "Invalid Sui minter address", + }; + } + } else { + if (!isValidEVMAddress(minter)) { + return { + type: "validate", + message: "Invalid EVM minter address", + }; + } + } + + return true; +}; + const TokenDetails: FC = () => { const { state, actions } = useInterchainTokenDeploymentStateContainer(); const chainId = useChainId(); @@ -51,20 +80,13 @@ const TokenDetails: FC = () => { return; } - if (!minter) { - return { - type: "required", - message: "Minter address is required", - }; - } - - if (!isValidEVMAddress(minter)) { - return { - type: "validate", - message: "Invalid minter address", - }; + if (minter) { + const minterValidation = validateMinterAddress(minter, chainId); + if (minterValidation !== true) { + return minterValidation; + } } - }, [isMintable, minter]); + }, [isMintable, minter, chainId]); const initialSupplyErrorMessage = useMemo(() => { if (isMintable) { diff --git a/apps/maestro/src/features/suiHooks/useDeployToken.ts b/apps/maestro/src/features/suiHooks/useDeployToken.ts index 00f7e3a84..d92a52876 100644 --- a/apps/maestro/src/features/suiHooks/useDeployToken.ts +++ b/apps/maestro/src/features/suiHooks/useDeployToken.ts @@ -28,13 +28,15 @@ export type DeployTokenParams = { decimals: number; destinationChainIds: string[]; skipRegister?: boolean; + minterAddress?: string; }; export type DeployTokenResult = SuiTransactionBlockResponse & { tokenManagerAddress: string; tokenAddress: string; - tokenManagerType: "mint/burn" | "lock/unlock"; + tokenManagerType: "mint_burn" | "lock_unlock"; deploymentMessageId?: string; + minterAddress?: string; }; type SuiObjectCreated = @@ -95,6 +97,7 @@ export default function useTokenDeploy() { decimals, destinationChainIds, skipRegister = false, + minterAddress, }: DeployTokenParams): Promise => { if (!currentAccount) { throw new Error("Wallet not connected"); @@ -135,9 +138,6 @@ export default function useTokenDeploy() { throw new Error("Failed to deploy token"); } - // Mint tokens before registering, as the treasury cap will be transferred to the ITS contract - // TODO: should merge this with above to avoid multiple transactions. - // we can do this once we know whether the token is mint/burn or lock/unlock if (treasuryCap) { const mintTxJSON = await getMintTx({ sender: currentAccount.address, @@ -157,6 +157,7 @@ export default function useTokenDeploy() { tokenPackageId: tokenAddress, metadataId: metadata.objectId, destinationChains: destinationChainIds, + minterAddress: minterAddress, }); if (!sendTokenTxJSON) { @@ -170,15 +171,7 @@ export default function useTokenDeploy() { }); const coinManagementObjectId = findCoinDataObject(sendTokenResult); - // find treasury cap in the sendTokenResult to determine the token manager type - const sendTokenObjects = sendTokenResult?.objectChanges; - const treasuryCapSendTokenResult = findObjectByType( - sendTokenObjects as SuiObjectChange[], - "TreasuryCap" - ); - const tokenManagerType = treasuryCapSendTokenResult - ? "mint/burn" - : "lock/unlock"; + const tokenManagerType = minterAddress ? "mint_burn" : "lock_unlock"; const txIndex = sendTokenResult?.events?.[3]?.id?.eventSeq ?? 0; // TODO: find the correct txIndex, it seems to be always 3 const deploymentMessageId = `${sendTokenResult?.digest}-${txIndex}`; return { @@ -187,6 +180,7 @@ export default function useTokenDeploy() { tokenManagerAddress: coinManagementObjectId || "0x", tokenAddress, tokenManagerType, + minterAddress, }; } catch (error) { console.error("Token deployment failed:", error); diff --git a/apps/maestro/src/server/routers/sui/index.ts b/apps/maestro/src/server/routers/sui/index.ts index 83b699a1b..7d3e501e5 100644 --- a/apps/maestro/src/server/routers/sui/index.ts +++ b/apps/maestro/src/server/routers/sui/index.ts @@ -70,6 +70,7 @@ export const suiRouter = router({ destinationChains: z.array(z.string()), tokenPackageId: z.string(), metadataId: z.string(), + minterAddress: z.string().optional(), }) ) .mutation(async ({ input }) => { @@ -97,13 +98,27 @@ export const suiRouter = router({ const { Example, ITS } = chainConfig.contracts; const itsObjectId = ITS.objects.ITS; - - // TODO: handle register type properly, whether it's mint/burn or lock/unlock. - await txBuilder.moveCall({ - target: `${Example.address}::its::register_coin`, - typeArguments: [tokenType], - arguments: [itsObjectId, metadataId], - }); + const treasuryCap = await getTreasuryCap(tokenPackageId); + + if (input.minterAddress) { + await txBuilder.moveCall({ + target: `${Example.address}::its::register_coin`, + typeArguments: [tokenType], + arguments: [itsObjectId, metadataId], + }); + txBuilder.tx.transferObjects( + [treasuryCap as string], + txBuilder.tx.pure.address(input.minterAddress) + ); + } else { + await txBuilder + .moveCall({ + target: `${Example.address}::its::register_coin_with_cap`, + typeArguments: [tokenType], + arguments: [itsObjectId, metadataId, treasuryCap], + }) + .catch((e) => console.log("error with register coin treasury", e)); + } for (const destinationChain of destinationChains) { await deployRemoteInterchainToken( diff --git a/apps/maestro/src/server/routers/sui/utils/utils.ts b/apps/maestro/src/server/routers/sui/utils/utils.ts index 07146a50d..ac3e99ac1 100644 --- a/apps/maestro/src/server/routers/sui/utils/utils.ts +++ b/apps/maestro/src/server/routers/sui/utils/utils.ts @@ -61,26 +61,15 @@ export const getCoinType = async (tokenAddress: string) => { // get token owner from token address // TODO: this is wrong the the destination chain is sui where the token owner is axelar relayer export const getTokenOwner = async (tokenAddress: string) => { + const treasuryCap = await getTreasuryCap(tokenAddress); const object = await client.getObject({ - id: tokenAddress, + id: treasuryCap as string, options: { showOwner: true, showPreviousTransaction: true, }, }); - - if (object?.data?.owner === "Immutable") { - const previousTx = object.data.previousTransaction; - - // Fetch the transaction details to find the sender - const transactionDetails = await client.getTransactionBlock({ - digest: previousTx as string, - options: { showInput: true, showEffects: true }, - }); - return transactionDetails.transaction?.data.sender; - } else { - throw new Error("Coin owner not found"); - } + return object?.data?.owner?.AddressOwner; }; function findTreasuryCap(txData: PaginatedTransactionResponse) { From 8fe91d4b98e6f598604c0250b3208ba49e8662fa Mon Sep 17 00:00:00 2001 From: SGiaccobasso Date: Mon, 24 Feb 2025 18:34:21 -0300 Subject: [PATCH 2/5] chore: remove logs and coments --- .../interchainToken/getInterchainTokenBalanceForOwner.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/maestro/src/server/routers/interchainToken/getInterchainTokenBalanceForOwner.ts b/apps/maestro/src/server/routers/interchainToken/getInterchainTokenBalanceForOwner.ts index 2a53bc06e..3edfa044e 100644 --- a/apps/maestro/src/server/routers/interchainToken/getInterchainTokenBalanceForOwner.ts +++ b/apps/maestro/src/server/routers/interchainToken/getInterchainTokenBalanceForOwner.ts @@ -71,12 +71,10 @@ export const getInterchainTokenBalanceForOwner = publicProcedure // This happens when the token is deployed on sui as a remote chain if (!metadata) { const coinInfo = await getCoinInfoByCoinType(client, coinType); - console.log("coinInfo", coinInfo); decimals = coinInfo?.decimals; } let tokenOwner = null; - // TODO: address this properly try { tokenOwner = await getTokenOwner(input.tokenAddress); } catch (error) { From 02ed878492a7792018a4bebac558759a78945bf1 Mon Sep 17 00:00:00 2001 From: npty Date: Tue, 25 Feb 2025 12:18:56 +0700 Subject: [PATCH 3/5] chore: fix build error --- apps/maestro/src/server/routers/sui/utils/utils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/maestro/src/server/routers/sui/utils/utils.ts b/apps/maestro/src/server/routers/sui/utils/utils.ts index ac3e99ac1..4c080b28d 100644 --- a/apps/maestro/src/server/routers/sui/utils/utils.ts +++ b/apps/maestro/src/server/routers/sui/utils/utils.ts @@ -69,7 +69,9 @@ export const getTokenOwner = async (tokenAddress: string) => { showPreviousTransaction: true, }, }); - return object?.data?.owner?.AddressOwner; + + const owner = object?.data?.owner as { AddressOwner: string } | undefined; + return owner?.AddressOwner; }; function findTreasuryCap(txData: PaginatedTransactionResponse) { From 2b229ab718ad676fc1c923e1d2a4a5af0a3d08a5 Mon Sep 17 00:00:00 2001 From: SGiaccobasso Date: Tue, 25 Feb 2025 03:41:52 -0300 Subject: [PATCH 4/5] chore: fix remote deployments --- .../useRegisterRemoteInterchainTokenOnSui.ts | 18 +++++++------- apps/maestro/src/server/routers/sui/index.ts | 3 ++- .../src/server/routers/sui/utils/txUtils.ts | 24 +++++++++++-------- .../src/server/routers/sui/utils/utils.ts | 12 ++++++++-- 4 files changed, 34 insertions(+), 23 deletions(-) diff --git a/apps/maestro/src/features/RegisterRemoteTokens/hooks/useRegisterRemoteInterchainTokenOnSui.ts b/apps/maestro/src/features/RegisterRemoteTokens/hooks/useRegisterRemoteInterchainTokenOnSui.ts index b2d157fe3..56b006c04 100644 --- a/apps/maestro/src/features/RegisterRemoteTokens/hooks/useRegisterRemoteInterchainTokenOnSui.ts +++ b/apps/maestro/src/features/RegisterRemoteTokens/hooks/useRegisterRemoteInterchainTokenOnSui.ts @@ -4,7 +4,7 @@ import { suiClient as client } from "~/lib/clients/suiClient"; import { useAccount } from "~/lib/hooks"; import { trpc } from "~/lib/trpc"; -type RegisterRemoteInterchainTokenOnSuiInput ={ +type RegisterRemoteInterchainTokenOnSuiInput = { axelarChainIds: string[]; originChainId: number; tokenAddress: string; @@ -48,14 +48,13 @@ export function useRegisterRemoteInterchainTokenOnSui() { } try { - const registerTokenTxJSON = - await getRegisterRemoteInterchainTokenTx({ - tokenAddress: input.tokenAddress, - destinationChainIds: input.axelarChainIds, - originChainId: input.originChainId, - sender: currentAccount.address, - symbol: input.symbol, - }); + const registerTokenTxJSON = await getRegisterRemoteInterchainTokenTx({ + tokenAddress: input.tokenAddress, + destinationChainIds: input.axelarChainIds, + originChainId: input.originChainId, + sender: currentAccount.address, + symbol: input.symbol, + }); const registerTokenResult = await signAndExecuteTransaction({ transaction: registerTokenTxJSON, @@ -74,4 +73,3 @@ export function useRegisterRemoteInterchainTokenOnSui() { account: currentAccount, }; } - diff --git a/apps/maestro/src/server/routers/sui/index.ts b/apps/maestro/src/server/routers/sui/index.ts index 7d3e501e5..2b4b6853a 100644 --- a/apps/maestro/src/server/routers/sui/index.ts +++ b/apps/maestro/src/server/routers/sui/index.ts @@ -5,6 +5,7 @@ import { } from "@axelar-network/axelar-cgp-sui"; import { z } from "zod"; +import { NEXT_PUBLIC_NETWORK_ENV } from "~/config/env"; import { suiClient } from "~/lib/clients/suiClient"; import { publicProcedure, router } from "~/server/trpc"; import { @@ -256,7 +257,7 @@ export const suiRouter = router({ .mutation(async ({ input }) => { try { const response = await fetch( - `${suiServiceBaseUrl}/chain/devnet-amplifier` + `${suiServiceBaseUrl}/chain/${NEXT_PUBLIC_NETWORK_ENV}` ); const _chainConfig = await response.json(); const chainConfig = _chainConfig.chains.sui; diff --git a/apps/maestro/src/server/routers/sui/utils/txUtils.ts b/apps/maestro/src/server/routers/sui/utils/txUtils.ts index 01ac0dcaa..1531b0d07 100644 --- a/apps/maestro/src/server/routers/sui/utils/txUtils.ts +++ b/apps/maestro/src/server/routers/sui/utils/txUtils.ts @@ -1,7 +1,11 @@ import { TxBuilder } from "@axelar-network/axelar-cgp-sui"; import { suiClient } from "~/lib/clients/suiClient"; -import { suiServiceBaseUrl } from "./utils"; +import { + getCoinAddressFromType, + getTokenOwner, + suiServiceBaseUrl, +} from "./utils"; export async function getChainConfig() { const response = await fetch(`${suiServiceBaseUrl}/chain/devnet-amplifier`); @@ -19,14 +23,12 @@ export function setupTxBuilder(sender: string) { export async function getTokenId( txBuilder: TxBuilder, tokenId: string, - ITS: any, + ITS: any ) { - const [TokenId] = await txBuilder.moveCall({ + const [TokenId] = await txBuilder.moveCall({ target: `${ITS.address}::token_id::from_u256`, - arguments: [ - tokenId.toString(), - ], - }) + arguments: [tokenId.toString()], + }); return TokenId; } @@ -35,9 +37,11 @@ export async function getTokenIdByCoinMetadata( txBuilder: TxBuilder, coinType: string, ITS: any, - coinMetadata: any, + coinMetadata: any ) { - const [TokenId] = await txBuilder.moveCall({ + const address = getCoinAddressFromType(coinType); + const tokenOwner = await getTokenOwner(address); + const [TokenId] = await txBuilder.moveCall({ target: `${ITS.address}::token_id::from_info`, typeArguments: [coinType], arguments: [ @@ -45,7 +49,7 @@ export async function getTokenIdByCoinMetadata( coinMetadata.symbol, txBuilder.tx.pure.u8(coinMetadata.decimals), txBuilder.tx.pure.bool(false), - txBuilder.tx.pure.bool(false), + txBuilder.tx.pure.bool(!tokenOwner), // true for mint_burn, false for lock_unlock as this checks whether an address owns the treasury cap ], }); return TokenId; diff --git a/apps/maestro/src/server/routers/sui/utils/utils.ts b/apps/maestro/src/server/routers/sui/utils/utils.ts index ac3e99ac1..98c0eebcc 100644 --- a/apps/maestro/src/server/routers/sui/utils/utils.ts +++ b/apps/maestro/src/server/routers/sui/utils/utils.ts @@ -72,6 +72,15 @@ export const getTokenOwner = async (tokenAddress: string) => { return object?.data?.owner?.AddressOwner; }; +export const getCoinAddressFromType = (coinType: string) => { + // check for long format + let addressMatch = coinType.match(/CoinData<(0x[^:]+)/); + if (addressMatch) return addressMatch?.[1]; + // check for the shorter format {address}::{symbol}::{SYMBOL} + addressMatch = coinType.match(/0x[^:]+/); + return addressMatch?.[0] as string; +}; + function findTreasuryCap(txData: PaginatedTransactionResponse) { // Find the mint transaction const mintTx = txData.data.find((tx) => { @@ -239,8 +248,7 @@ function extractTokenDetails(filteredResult: DynamicFieldInfo) { // Extract the address from the objectType const objectType = filteredResult.objectType; - const addressMatch = objectType.match(/CoinData<(0x[^:]+)/); - const address = addressMatch?.[1] as string; + const address = getCoinAddressFromType(objectType); return { tokenManager, address, From 2d316c5df1ab390594c9d3ab064f5f5a031ebb06 Mon Sep 17 00:00:00 2001 From: SGiaccobasso Date: Tue, 25 Feb 2025 03:46:27 -0300 Subject: [PATCH 5/5] chore: correct classification --- apps/maestro/src/features/suiHooks/useDeployToken.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/maestro/src/features/suiHooks/useDeployToken.ts b/apps/maestro/src/features/suiHooks/useDeployToken.ts index d92a52876..340d90bd7 100644 --- a/apps/maestro/src/features/suiHooks/useDeployToken.ts +++ b/apps/maestro/src/features/suiHooks/useDeployToken.ts @@ -171,7 +171,7 @@ export default function useTokenDeploy() { }); const coinManagementObjectId = findCoinDataObject(sendTokenResult); - const tokenManagerType = minterAddress ? "mint_burn" : "lock_unlock"; + const tokenManagerType = minterAddress ? "lock_unlock" : "mint_burn"; const txIndex = sendTokenResult?.events?.[3]?.id?.eventSeq ?? 0; // TODO: find the correct txIndex, it seems to be always 3 const deploymentMessageId = `${sendTokenResult?.digest}-${txIndex}`; return {