Skip to content

Commit

Permalink
chore: transfer flow wip
Browse files Browse the repository at this point in the history
  • Loading branch information
SGiaccobasso committed Jan 25, 2025
1 parent ce9173d commit de704c3
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function useSendInterchainTokenState(props: {
sourceChain: ChainConfig;
kind: "canonical" | "interchain";
isModalOpen?: boolean;
destinationAddress?: string;
}) {
const { combinedComputed } = useAllChainConfigsQuery();

Expand Down Expand Up @@ -121,6 +122,8 @@ export function useSendInterchainTokenState(props: {
destinationChainName: selectedToChain?.chain_name,
sourceChainName: props.sourceChain.chain_name,
gas,
tokenId: props.tokenId,
destinationAddress: props.destinationAddress,
});

const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { useForm, type SubmitHandler } from "react-hook-form";
import type { SuiTransactionBlockResponse } from "@mysten/sui/client";
import { formatUnits, parseUnits } from "viem";

import { useAccount } from "~/lib/hooks";
import { logger } from "~/lib/logger";
import { preventNonNumericInput } from "~/lib/utils/validation";
import BigNumberText from "~/ui/components/BigNumberText";
Expand All @@ -27,6 +28,7 @@ import { useSendInterchainTokenState } from "./SendInterchainToken.state";

type FormState = {
amountToTransfer: string;
destinationAddress: string;
};

type Props = {
Expand All @@ -46,6 +48,7 @@ type Props = {
};

export const SendInterchainToken: FC<Props> = (props) => {
const { address } = useAccount();
const [state, actions] = useSendInterchainTokenState({
tokenAddress: props.tokenAddress,
tokenId: props.tokenId,
Expand Down Expand Up @@ -79,9 +82,11 @@ export const SendInterchainToken: FC<Props> = (props) => {
{
tokenAddress: props.tokenAddress,
amount: data.amountToTransfer,
tokenId: props.tokenId,
destinationAddress: data.destinationAddress,
decimals: Number(props.balance.decimals),
},
{
// handles unhandled errors in the mutation
onError(error) {
if (error instanceof Error) {
toast.error("Failed to transfer token. Please try again.");
Expand Down Expand Up @@ -121,7 +126,8 @@ export const SendInterchainToken: FC<Props> = (props) => {
return {
children:
formState.errors.amountToTransfer?.message ??
"Amount is required",
formState.errors.destinationAddress?.message ??
"Please fill in all required fields",
status: "error",
};
}
Expand All @@ -146,6 +152,7 @@ export const SendInterchainToken: FC<Props> = (props) => {
}, [
amountToTransfer,
formState.errors.amountToTransfer?.message,
formState.errors.destinationAddress?.message,
formState.isValid,
state.hasInsufficientGasBalance,
state.nativeTokenSymbol,
Expand Down Expand Up @@ -208,7 +215,7 @@ export const SendInterchainToken: FC<Props> = (props) => {
logger.error("Failed to track transaction", error);
});
}
}, [actions, suiTxDigest, state.txState.status]);
}, [actions, suiTxDigest, state.txState.status, address]);

const handleAllChainsExecuted = useCallback(async () => {
await actions.refetchBalances();
Expand Down Expand Up @@ -260,6 +267,32 @@ export const SendInterchainToken: FC<Props> = (props) => {
[actions, resetForm]
);

const isEvmChainsOnly = useMemo(() => {
return (
state.selectedToChain?.chain_type === "evm" &&
props.sourceChain.chain_type === "evm"
);
}, [state.selectedToChain?.chain_type, props.sourceChain.chain_type]);

useEffect(() => {
console.log(
"chain_type",
state.selectedToChain?.chain_type,
props.sourceChain.chain_type
);
if (isEvmChainsOnly) {
setValue("destinationAddress", address ?? "", {
shouldValidate: true,
});
}
}, [
state.selectedToChain?.chain_type,
props.sourceChain.chain_type,
address,
setValue,
isEvmChainsOnly,
]);

return (
<Modal
trigger={props.trigger}
Expand Down Expand Up @@ -375,6 +408,31 @@ export const SendInterchainToken: FC<Props> = (props) => {
/>
</FormControl>

<FormControl>
<Label htmlFor="destinationAddress">
<Label.Text>Destination Address</Label.Text>
{isEvmChainsOnly && (
<Label.AltText>(Using connected wallet address)</Label.AltText>
)}
</Label>
<TextInput
id="destinationAddress"
$bordered
placeholder="Enter destination address"
className="bg-base-200"
disabled={isEvmChainsOnly}
{...register("destinationAddress", {
required: "Destination address is required",
validate: (value) => {
if (value.length < 42) {
return "Invalid address length";
}
return true;
},
})}
/>
</FormControl>

{state.txState.status === "idle" &&
state.estimatedWaitTimeInMinutes > 2 && (
<Alert icon={<EyeIcon />}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,39 @@
import { INTERCHAIN_TOKEN_ENCODERS } from "@axelarjs/evm";
import { toast } from "@axelarjs/ui/toaster";

import { useSignAndExecuteTransaction } from "@mysten/dapp-kit";
import { getFullnodeUrl, SuiClient } from "@mysten/sui/client";
import { useMutation } from "@tanstack/react-query";
import { parseUnits, TransactionExecutionError } from "viem";

import {
useReadInterchainTokenDecimals,
useWriteInterchainTokenInterchainTransfer,
} from "~/lib/contracts/InterchainToken.hooks";
import { useWriteInterchainTokenInterchainTransfer } from "~/lib/contracts/InterchainToken.hooks";
import { useAccount, useChainId } from "~/lib/hooks";
import { useTransactionState } from "~/lib/hooks/useTransactionState";
import { logger } from "~/lib/logger";
import { trpc } from "~/lib/trpc";
import { getCoinType } from "~/server/routers/sui/utils/utils";

export type UseSendInterchainTokenConfig = {
tokenAddress: `0x${string}`;
sourceChainName: string;
destinationChainName: string;
gas?: bigint;
tokenId?: string;
destinationAddress?: string;
};

export type UseSendInterchainTokenInput = {
tokenAddress: `0x${string}`;
amount: string;
tokenId?: string;
destinationAddress?: string;
decimals?: number;
};

export function useInterchainTransferMutation(
config: UseSendInterchainTokenConfig
) {
const [txState, setTxState] = useTransactionState();
const { data: decimals } = useReadInterchainTokenDecimals({
address: config.tokenAddress,
});

const chainId = useChainId();

Expand All @@ -39,29 +42,72 @@ export function useInterchainTransferMutation(
const { writeContractAsync: transferAsync } =
useWriteInterchainTokenInterchainTransfer();

const { mutateAsync: getSendTokenTx } = trpc.sui.getSendTokenTx.useMutation({
onError(error) {
console.log("error in getSendTokenTx", error.message);
},
});

const client = new SuiClient({ url: getFullnodeUrl("testnet") });

const { mutateAsync: signAndExecuteTransaction } =
useSignAndExecuteTransaction({
execute: async ({ bytes, signature }) => {
const result = await client.executeTransactionBlock({
transactionBlock: bytes,
signature,
options: {
showObjectChanges: true,
showEvents: true,
showEffects: true,
showRawEffects: true,
},
});
return result;
},
});

const mutation = useMutation<void, unknown, UseSendInterchainTokenInput>({
mutationFn: async ({ amount }) => {
mutationFn: async ({ amount, tokenId, destinationAddress, decimals }) => {
if (!(decimals && address && config.gas)) {
return;
}

const bnAmount = parseUnits(amount, decimals);

try {
setTxState({
status: "awaiting_approval",
});

const txHash = await transferAsync({
address: config.tokenAddress,
value: config.gas ?? 0n,
args: INTERCHAIN_TOKEN_ENCODERS.interchainTransfer.args({
let txHash: any;
if (config.sourceChainName === "sui") {
const coinObjectId = await getCoinType(config.tokenAddress);
console.log("coinObjectId", coinObjectId);
const sendTokenTxJSON = await getSendTokenTx({
sender: address,
tokenId: tokenId,
tokenAddress: config.tokenAddress,
amount: bnAmount.toString(),
destinationChain: config.destinationChainName,
recipient: address,
amount: bnAmount,
metadata: "0x",
}),
});
destinationAddress: destinationAddress,
gas: config.gas.toString() ?? "0",
coinObjectId: coinObjectId,
});
txHash = await signAndExecuteTransaction({
transaction: sendTokenTxJSON,
chain: "sui:testnet", //TODO: make this dynamic
});
} else {
txHash = await transferAsync({
address: config.tokenAddress,
value: config.gas ?? 0n,
args: INTERCHAIN_TOKEN_ENCODERS.interchainTransfer.args({
destinationChain: config.destinationChainName,
recipient: address,
amount: bnAmount,
metadata: "0x",
}),
});
}
if (txHash) {
setTxState({
status: "submitted",
Expand Down
90 changes: 89 additions & 1 deletion apps/maestro/src/server/routers/sui/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { SUI_PACKAGE_ID, TxBuilder } from "@axelar-network/axelar-cgp-sui";
import {
CLOCK_PACKAGE_ID,
SUI_PACKAGE_ID,
TxBuilder,
} from "@axelar-network/axelar-cgp-sui";
import { SuiClient } from "@mysten/sui/client";
import { z } from "zod";

Expand Down Expand Up @@ -194,4 +198,88 @@ export const suiRouter = router({
const txJSON = await tx.toJSON();
return txJSON;
}),

getSendTokenTx: publicProcedure
.input(
z.object({
sender: z.string(),
tokenId: z.string(),
tokenAddress: z.string(),
amount: z.string(),
destinationChain: z.string(),
destinationAddress: z.string(),
gas: z.string(),
coinObjectId: z.string(),
})
)
.mutation(async ({ input }) => {
try {
const response = await fetch(
`${suiServiceBaseUrl}/chain/devnet-amplifier`
);
const _chainConfig = await response.json();
const chainConfig = _chainConfig.chains.sui;

const txBuilder = new TxBuilder(suiClient);
const tx = txBuilder.tx;

// Split coins for gas
const Gas = tx.splitCoins(tx.gas, [BigInt(input.gas)]);
// keep from the object id everything before the first :
const coinObjectId = input.coinObjectId.split(":")[0];

// Split token to transfer to the destination chain
const Coin = tx.splitCoins(coinObjectId, [BigInt(input.amount)]);

const [TokenId] = await txBuilder.moveCall({
target: `${chainConfig.contracts.ITS.address}::token_id::from_u256`,
arguments: [input.tokenId],
});
console.log(
"chainConfig.contracts.Example.objects.ItsSingleton",
chainConfig.contracts.Example.objects.ItsSingleton
);
console.log(
"chainConfig.contracts.ITS.objects.ITS",
chainConfig.contracts.ITS.objects.ITS
);
console.log(
"chainConfig.contracts.AxelarGateway.objects.Gateway",
chainConfig.contracts.AxelarGateway.objects.Gateway
);
console.log(
"chainConfig.contracts.GasService.objects.GasService",
chainConfig.contracts.GasService.objects.GasService
);

await txBuilder.moveCall({
target: `${chainConfig.contracts.Example.address}::its::send_interchain_transfer_call`,
arguments: [
chainConfig.contracts.Example.objects.ItsSingleton,
chainConfig.contracts.ITS.objects.ITS,
chainConfig.contracts.AxelarGateway.objects.Gateway,
chainConfig.contracts.GasService.objects.GasService,
TokenId,
Coin,
input.destinationChain,
input.destinationAddress,
"0x",
input.sender,
Gas,
"0x",
CLOCK_PACKAGE_ID,
],
typeArguments: [input.tokenType],
});

const tx2 = await buildTx(input.sender, txBuilder);
const txJSON = await tx2.toJSON();
return txJSON;
} catch (error) {
console.error("Failed to prepare send token transaction:", error);
throw new Error(
`Send token transaction preparation failed: ${(error as Error).message}`
);
}
}),
});

0 comments on commit de704c3

Please sign in to comment.