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

chore: fetching token data #478

Merged
merged 13 commits into from
Dec 19, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@ export const RegisteredInterchainTokenCard: FC<Props> = (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) {
Expand Down
48 changes: 27 additions & 21 deletions apps/maestro/src/features/suiHooks/useDeployToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 });
SGiaccobasso marked this conversation as resolved.
Show resolved Hide resolved

// 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
);
Expand All @@ -39,15 +99,15 @@ 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)),
]);

const itClient = ctx.contracts.createInterchainTokenClient(
chainConfig,
input.tokenAddress
tokenAddress
);

const [
Expand All @@ -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,
Expand Down
58 changes: 58 additions & 0 deletions apps/maestro/src/server/routers/erc20/getERC20TokenDetails.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -15,6 +16,56 @@ const overrides: Record<string, Record<string, string>> = {
},
};

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({
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion apps/maestro/src/server/routers/sui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down
2 changes: 0 additions & 2 deletions apps/maestro/src/services/axelarjsSDK/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
19 changes: 2 additions & 17 deletions apps/maestro/src/services/interchainToken/hooks.ts
Original file line number Diff line number Diff line change
@@ -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: {
Expand All @@ -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),
Expand All @@ -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,
}
);
Expand Down
Loading