diff --git a/apps/maestro/src/features/InterchainTokenList/RegisteredInterchainTokenCard.tsx b/apps/maestro/src/features/InterchainTokenList/RegisteredInterchainTokenCard.tsx index 7b1a1366c..9b5b01f08 100644 --- a/apps/maestro/src/features/InterchainTokenList/RegisteredInterchainTokenCard.tsx +++ b/apps/maestro/src/features/InterchainTokenList/RegisteredInterchainTokenCard.tsx @@ -61,8 +61,7 @@ export const RegisteredInterchainTokenCard: FC = (props) => { tokenAddress: props.isRegistered ? props.tokenAddress : undefined, owner: address, }); - // TODO: remove any - const balance = (result as any).data.balance; + const balance = result?.data; const { explorerUrl, explorerName } = useMemo(() => { if (!props.tokenAddress || !props.chain) { diff --git a/apps/maestro/src/features/suiHooks/useDeployToken.ts b/apps/maestro/src/features/suiHooks/useDeployToken.ts index 45c3b31db..1aae44bca 100644 --- a/apps/maestro/src/features/suiHooks/useDeployToken.ts +++ b/apps/maestro/src/features/suiHooks/useDeployToken.ts @@ -138,8 +138,22 @@ export default function useTokenDeploy() { throw new Error("Failed to deploy token"); } - // if treasury cap is null then it is lock/unlock, otherwise it is mint/burn - const tokenManagerType = treasuryCap ? "mint/burn" : "lock/unlock"; + // 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, + tokenTreasuryCap: treasuryCap?.objectId, + amount: initialSupply, + tokenPackageId: tokenAddress, + symbol, + }); + await signAndExecuteTransaction({ + transaction: mintTxJSON, + chain: "sui:testnet", //TODO: make this dynamic + }); + } const sendTokenTxJSON = await getRegisterAndSendTokenDeploymentTxBytes({ sender: currentAccount.address, @@ -159,32 +173,24 @@ export default function useTokenDeploy() { transaction: sendTokenTxJSON, chain: "sui:testnet", //TODO: make this dynamic }); + 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"; // TODO:: handle txIndex properly const txIndex = sendTokenResult?.events?.[0]?.id?.eventSeq ?? 0; const deploymentMessageId = `${sendTokenResult?.digest}-${txIndex}`; - const coinManagementObjectId = findCoinDataObject(sendTokenResult); if (!coinManagementObjectId) { throw new Error("Failed to find coin management object id"); } - - // Mint tokens - // 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, - tokenTreasuryCap: treasuryCap?.objectId, - amount: initialSupply, - tokenPackageId: tokenAddress, - symbol, - }); - await signAndExecuteTransaction({ - transaction: mintTxJSON, - chain: "sui:testnet", - }); - } - return { ...sendTokenResult, deploymentMessageId, diff --git a/apps/maestro/src/server/routers/erc20/getERC20TokenBalanceForOwner.ts b/apps/maestro/src/server/routers/erc20/getERC20TokenBalanceForOwner.ts index f65516aaa..eff6edcac 100644 --- a/apps/maestro/src/server/routers/erc20/getERC20TokenBalanceForOwner.ts +++ b/apps/maestro/src/server/routers/erc20/getERC20TokenBalanceForOwner.ts @@ -1,8 +1,8 @@ +import { getFullnodeUrl, SuiClient } from "@mysten/sui/client"; import { TRPCError } from "@trpc/server"; import { always } from "rambda"; import { z } from "zod"; -import { hex40Literal } from "~/lib/utils/validation"; import { publicProcedure } from "~/server/trpc"; export const ROLES_ENUM = ["MINTER", "OPERATOR", "FLOW_LIMITER"] as const; @@ -16,11 +16,71 @@ export const getERC20TokenBalanceForOwner = publicProcedure .input( z.object({ chainId: z.number(), - tokenAddress: hex40Literal(), - owner: hex40Literal(), + tokenAddress: z.string(), + owner: z.string(), }) ) .query(async ({ input, ctx }) => { + // Sui address length is 66 + if (input.tokenAddress?.length === 66) { + let isTokenOwner = false; + + const client = new SuiClient({ url: getFullnodeUrl("testnet") }); // TODO: make this configurable + + // Get the coin type + const modules = await client.getNormalizedMoveModulesByPackage({ + package: input.tokenAddress, + }); + const coinSymbol = Object.keys(modules)[0]; + const coinType = `${input.tokenAddress}::${coinSymbol?.toLowerCase()}::${coinSymbol?.toUpperCase()}`; + // Get the coin balance + const coins = await client.getCoins({ + owner: input.owner, + coinType: coinType, + }); + const balance = coins.data?.[0]?.balance?.toString() ?? "0"; + + // Get the coin metadata + const metadata = await client.getCoinMetadata({ coinType }); + + // Get the token owner + const object = await client.getObject({ + id: input.tokenAddress, + 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 }, + }); + isTokenOwner = + transactionDetails.transaction?.data.sender === input.owner; + } + + const result = { + isTokenOwner, + isTokenMinter: isTokenOwner, + tokenBalance: balance, + decimals: metadata?.decimals ?? 0, + isTokenPendingOwner: false, + hasPendingOwner: false, + hasMinterRole: isTokenOwner, + hasOperatorRole: isTokenOwner, + hasFlowLimiterRole: isTokenOwner, // TODO: check if this is correct + }; + return result; + } + // This is for ERC20 tokens + const balanceOwner = input.owner as `0x${string}`; + const tokenAddress = input.tokenAddress as `0x${string}`; + const chainConfig = ctx.configs.wagmiChainConfigs.find( (chain) => chain.id === input.chainId ); @@ -39,7 +99,7 @@ export const getERC20TokenBalanceForOwner = publicProcedure ); const [tokenBalance, decimals, owner, pendingOwner] = await Promise.all([ - client.reads.balanceOf({ account: input.owner }), + client.reads.balanceOf({ account: balanceOwner }), client.reads.decimals(), client.reads.owner().catch(always(null)), client.reads.pendingOwner().catch(always(null)), @@ -47,7 +107,7 @@ export const getERC20TokenBalanceForOwner = publicProcedure const itClient = ctx.contracts.createInterchainTokenClient( chainConfig, - input.tokenAddress + tokenAddress ); const [ @@ -58,31 +118,31 @@ export const getERC20TokenBalanceForOwner = publicProcedure ] = await Promise.all( [ itClient.reads.isMinter({ - addr: input.owner, + addr: balanceOwner, }), itClient.reads.hasRole({ role: getRoleIndex("MINTER"), - account: input.owner, + account: balanceOwner, }), itClient.reads.hasRole({ role: getRoleIndex("OPERATOR"), - account: input.owner, + account: balanceOwner, }), itClient.reads.hasRole({ role: getRoleIndex("FLOW_LIMITER"), - account: input.owner, + account: balanceOwner, }), ].map((p) => p.catch(always(false))) ); - const isTokenOwner = owner === input.owner; + const isTokenOwner = owner === balanceOwner; return { isTokenOwner, isTokenMinter, tokenBalance: tokenBalance.toString(), decimals, - isTokenPendingOwner: pendingOwner === input.owner, + isTokenPendingOwner: pendingOwner === balanceOwner, hasPendingOwner: pendingOwner !== null, hasMinterRole, hasOperatorRole, diff --git a/apps/maestro/src/server/routers/erc20/getERC20TokenDetails.ts b/apps/maestro/src/server/routers/erc20/getERC20TokenDetails.ts index 1081006d8..183dd239d 100644 --- a/apps/maestro/src/server/routers/erc20/getERC20TokenDetails.ts +++ b/apps/maestro/src/server/routers/erc20/getERC20TokenDetails.ts @@ -1,6 +1,7 @@ import type { IERC20BurnableMintableClient } from "@axelarjs/evm"; import { invariant } from "@axelarjs/utils"; +import { getFullnodeUrl, SuiClient } from "@mysten/sui/client"; import { TRPCError } from "@trpc/server"; import { always } from "rambda"; import { z } from "zod"; @@ -15,6 +16,56 @@ const overrides: Record> = { }, }; +async function getSuiTokenDetails(tokenAddress: string, chainId: number) { + const client = new SuiClient({ url: getFullnodeUrl("testnet") }); // TODO: make this configurable + + const modules = await client.getNormalizedMoveModulesByPackage({ + package: tokenAddress, + }); + const coinSymbol = Object.keys(modules)[0]; + + const coinType = `${tokenAddress}::${coinSymbol?.toLowerCase()}::${coinSymbol?.toUpperCase()}`; + + const metadata = await client.getCoinMetadata({ coinType }); + + // Get the token owner + const object = await client.getObject({ + id: tokenAddress, + options: { + showOwner: true, + showPreviousTransaction: true, + }, + }); + + 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 }, + }); + const tokenOwner = transactionDetails.transaction?.data.sender; + + if (!metadata) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Token metadata not found for ${tokenAddress} on chain ${chainId}`, + }); + } + + return { + name: metadata.name, + decimals: metadata.decimals, + owner: tokenOwner, + pendingOwner: null, + chainId: chainId, + chainName: "Sui", + axelarChainId: "sui", + axelarChainName: "sui", + symbol: metadata.symbol, + }; +} + export const getERC20TokenDetails = publicProcedure .input( z.object({ @@ -23,6 +74,13 @@ export const getERC20TokenDetails = publicProcedure }) ) .query(async ({ input, ctx }) => { + // Enter here if the token is a Sui token + if (input.tokenAddress.length === 66) { + return await getSuiTokenDetails( + input.tokenAddress, + input.chainId as number + ); + } try { const { wagmiChainConfigs: chainConfigs } = ctx.configs; const chainConfig = chainConfigs.find( diff --git a/apps/maestro/src/server/routers/sui/index.ts b/apps/maestro/src/server/routers/sui/index.ts index b1ae5df62..3bf088b40 100644 --- a/apps/maestro/src/server/routers/sui/index.ts +++ b/apps/maestro/src/server/routers/sui/index.ts @@ -185,7 +185,7 @@ export const suiRouter = router({ const [coin] = await txBuilder.moveCall({ target: `${SUI_PACKAGE_ID}::coin::mint`, typeArguments: [tokenType], - arguments: [tokenTreasuryCap, txBuilder.tx.pure.u64(amount)], + arguments: [tokenTreasuryCap, amount.toString()], }); txBuilder.tx.transferObjects([coin], txBuilder.tx.pure.address(sender)); diff --git a/apps/maestro/src/services/axelarjsSDK/index.ts b/apps/maestro/src/services/axelarjsSDK/index.ts index 54d85be8c..d23dfc2aa 100644 --- a/apps/maestro/src/services/axelarjsSDK/index.ts +++ b/apps/maestro/src/services/axelarjsSDK/index.ts @@ -86,8 +86,6 @@ async function getChainInfo(params: GetChainInfoInput) { environment: process.env.NEXT_PUBLIC_NETWORK_ENV as Environment, }); - console.log("chains", chains); - const chainConfig = chains.find((chain) => chain.id === params.axelarChainId); if (!chainConfig) { diff --git a/apps/maestro/src/services/interchainToken/hooks.ts b/apps/maestro/src/services/interchainToken/hooks.ts index 683c36454..b6b138fe2 100644 --- a/apps/maestro/src/services/interchainToken/hooks.ts +++ b/apps/maestro/src/services/interchainToken/hooks.ts @@ -1,8 +1,5 @@ import { Maybe } from "@axelarjs/utils"; -import { getFullnodeUrl, SuiClient } from "@mysten/sui/client"; -import { isAddress } from "viem"; - import { trpc } from "~/lib/trpc"; export function useInterchainTokenDetailsQuery(input: { @@ -22,21 +19,11 @@ export function useInterchainTokenDetailsQuery(input: { ); } -export async function useInterchainTokenBalanceForOwnerQuery(input: { +export function useInterchainTokenBalanceForOwnerQuery(input: { chainId?: number; tokenAddress?: string; - owner?: `0x${string}`; + owner?: string; }) { - // TODO: WIP - if (input.chainId === 103) { - const coinType = `${input.tokenAddress}::sui::SUI`; - const client = new SuiClient({ url: getFullnodeUrl("testnet") }); - const coins = await client.getCoins({ - owner: input.owner as string, - coinType, - }); - return coins; - } return trpc.erc20.getERC20TokenBalanceForOwner.useQuery( { chainId: Number(input.chainId), @@ -46,8 +33,6 @@ export async function useInterchainTokenBalanceForOwnerQuery(input: { { enabled: Boolean(input.chainId) && - isAddress(input.tokenAddress ?? "") && - isAddress(input.owner ?? "") && parseInt(String(input.tokenAddress), 16) !== 0, } );