import { useDynamicContext, useIsLoggedIn } from "@dynamic-labs/sdk-react-core";
import { isSuiWallet } from "@dynamic-labs/sui";
import { useState, useEffect, useCallback } from "react";
import { Transaction } from "@mysten/sui/transactions";
// Constants for Sui and USDC
const MIST_PER_SUI = 1_000_000_000; // 1 SUI = 1 billion MIST (smallest unit)
const USDC_COIN_TYPE =
"0xa1ec7fc00a6f40db9693ad1415d0c193ad3906494428cf252621037bd7117e29::usdc::USDC";
const USDC_DECIMALS = 6; // USDC has 6 decimal places
const USDC_PER_UNIT = Math.pow(10, USDC_DECIMALS); // Used to convert between units
export default function Send() {
// Dynamic wallet integration
const isLoggedIn = useIsLoggedIn();
const { primaryWallet } = useDynamicContext();
// UI state management
const [isLoading, setIsLoading] = useState(false);
const [result, setResult] = useState("");
const [recipientAddress, setRecipientAddress] = useState("");
const [amount, setAmount] = useState("");
const [balances, setBalances] = useState({ sui: "0", usdc: "0" });
const [isBalanceLoading, setIsBalanceLoading] = useState(false);
const [txSignature, setTxSignature] = useState<string | null>(null);
const [currentStage, setCurrentStage] = useState<string>("initial");
// Function to fetch and display user's SUI and USDC balances
const fetchBalances = useCallback(async () => {
// Check if wallet is connected and is a Sui wallet
if (!primaryWallet || !isSuiWallet(primaryWallet)) {
setBalances({ sui: "0", usdc: "0" });
return;
}
setIsBalanceLoading(true);
try {
// Get the Sui client from the wallet
const walletClient = await primaryWallet.getSuiClient();
if (!walletClient) return;
// Fetch both SUI and USDC coins in parallel for better performance
const [suiCoins, usdcCoins] = await Promise.all([
walletClient.getCoins({
owner: primaryWallet.address,
coinType: "0x2::sui::SUI",
}),
walletClient.getCoins({
owner: primaryWallet.address,
coinType: USDC_COIN_TYPE,
}),
]);
// Calculate total balances by summing all coins and converting to human-readable format
const suiBalance = (
suiCoins.data.reduce((sum, coin) => sum + Number(coin.balance), 0) /
MIST_PER_SUI
).toFixed(9);
const usdcBalance = (
usdcCoins.data.reduce((sum, coin) => sum + Number(coin.balance), 0) /
USDC_PER_UNIT
).toFixed(USDC_DECIMALS);
setBalances({ sui: suiBalance, usdc: usdcBalance });
} catch (error) {
console.error("Error fetching balances:", error);
setBalances({ sui: "0", usdc: "0" });
} finally {
setIsBalanceLoading(false);
}
}, [primaryWallet]);
useEffect(() => {
if (isLoggedIn && primaryWallet && isSuiWallet(primaryWallet)) {
fetchBalances();
} else {
setBalances({ sui: "0", usdc: "0" });
}
}, [isLoggedIn, primaryWallet, fetchBalances]);
// Main function to send USDC using gasless transactions
const sendUSDC = async () => {
// Validate wallet connection
if (!primaryWallet || !isSuiWallet(primaryWallet)) {
setResult("Wallet not connected or not a SUI wallet");
return;
}
// Validate user inputs
if (!recipientAddress || !amount) {
setResult("Please enter recipient address and amount");
return;
}
try {
setIsLoading(true);
setResult("Creating transaction...");
setCurrentStage("creating");
// Convert human-readable amount to smallest unit (e.g., 10.5 USDC -> 10,500,000)
const amountInSmallestUnit = Math.floor(
parseFloat(amount) * USDC_PER_UNIT
);
const walletClient = await primaryWallet.getSuiClient();
if (!walletClient) {
throw new Error("Failed to get SUI client from wallet");
}
// Get all USDC coins owned by the user
const ownedCoins = await walletClient.getCoins({
owner: primaryWallet.address,
coinType: USDC_COIN_TYPE,
});
if (ownedCoins.data.length === 0) {
throw new Error("No USDC coins found in wallet");
}
// Select enough coins to cover the transaction amount
// This handles cases where the user has multiple USDC coins
let totalAvailable = 0;
const coinObjectsToUse = [];
for (const coin of ownedCoins.data) {
coinObjectsToUse.push(coin.coinObjectId);
totalAvailable += Number(coin.balance);
if (totalAvailable >= amountInSmallestUnit) {
break;
}
}
// Check if user has enough USDC
if (totalAvailable < amountInSmallestUnit) {
throw new Error(
`Insufficient USDC balance. Available: ${
totalAvailable / USDC_PER_UNIT
} USDC`
);
}
// Create a new Sui transaction
const tx = new Transaction();
tx.setSender(primaryWallet.address);
// Handle multiple USDC coins by merging them into one
// This is necessary because Sui requires specific coin objects for transactions
let mergedCoin;
if (coinObjectsToUse.length > 1) {
const primaryCoin = coinObjectsToUse[0];
const otherCoins = coinObjectsToUse.slice(1);
// Merge all coins into the primary coin
tx.mergeCoins(
tx.object(primaryCoin),
otherCoins.map((id) => tx.object(id))
);
mergedCoin = tx.object(primaryCoin);
} else {
mergedCoin = tx.object(coinObjectsToUse[0]);
}
// Split the exact amount needed for the transfer
const [coinToSend] = tx.splitCoins(mergedCoin, [
tx.pure.u64(amountInSmallestUnit),
]);
// Transfer the split coin to the recipient
tx.transferObjects([coinToSend], tx.pure.address(recipientAddress));
// Build the transaction kind (without gas estimation) for sponsorship
setCurrentStage("building");
const kindBytes = await tx.build({
client: walletClient as any,
onlyTransactionKind: true,
});
// Send the transaction to our API for gas sponsorship
setResult("Requesting gas sponsorship...");
setCurrentStage("sponsoring");
const response = await fetch("/api/gas", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
tx: Array.from(kindBytes),
senderAddress: primaryWallet.address,
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to sponsor transaction");
}
// Get the sponsored transaction data from the API
const sponsorData = await response.json();
// Ask the user to sign the sponsored transaction
setResult("Requesting your signature...");
setCurrentStage("signing");
// Convert the sponsored bytes back to a Uint8Array
const sponsoredBytesArray = new Uint8Array(
Object.values(sponsorData.sponsoredBytes)
);
// Sign the transaction with the user's wallet
// Try different signing formats in case of compatibility issues
let signedTransaction;
try {
const sponsoredTransaction = Transaction.from(sponsoredBytesArray);
signedTransaction = await primaryWallet.signTransaction(
sponsoredTransaction as any
);
} catch (signError) {
signedTransaction = await primaryWallet.signTransaction(
sponsoredBytesArray as any
);
}
// Execute the transaction on the Sui network
setResult("Executing transaction...");
setCurrentStage("executing");
const executionResult = await walletClient.executeTransactionBlock({
transactionBlock: sponsoredBytesArray,
signature: [signedTransaction.signature, sponsorData.sponsorSignature], // Both user and sponsor signatures
options: {
showEffects: true,
showEvents: true,
showObjectChanges: true,
},
});
// Transaction completed successfully
setTxSignature(executionResult?.digest || null);
setResult(`USDC transfer successful! ${executionResult.digest}`);
setCurrentStage("complete");
// Wait for transaction confirmation and refresh balances
await walletClient.waitForTransaction({
digest: executionResult.digest,
});
fetchBalances();
} catch (error) {
console.error("Error:", error);
setTxSignature(null);
setCurrentStage("error");
setResult(
`Error: ${error instanceof Error ? error.message : String(error)}`
);
} finally {
setIsLoading(false);
}
};
return (
<div>
<h2>Send USDC on Sui</h2>
{isLoggedIn && primaryWallet && (
<div>
<p>
Your SUI Balance: {isBalanceLoading ? "Loading..." : `${balances.sui} SUI`}
</p>
<p>
Your USDC Balance: {isBalanceLoading ? "Loading..." : `${balances.usdc} USDC`}
</p>
<button onClick={fetchBalances} disabled={isBalanceLoading}>
Refresh Balance
</button>
</div>
)}
<div>
<input
type="text"
placeholder="Recipient Address"
value={recipientAddress}
onChange={(e) => setRecipientAddress(e.target.value)}
disabled={isLoading}
/>
<input
type="text"
placeholder="Amount in USDC"
value={amount}
onChange={(e) => setAmount(e.target.value)}
disabled={isLoading}
/>
<button
onClick={sendUSDC}
disabled={
!isLoggedIn ||
!primaryWallet ||
isLoading ||
!recipientAddress ||
!amount
}
>
{isLoading ? "Processing..." : "Send USDC"}
</button>
</div>
{result && (
<div>
<p>{result}</p>
{txSignature && (
<div>
<p>Signature: {txSignature}</p>
<a
href={`https://testnet.suivision.xyz/txblock/${txSignature}`}
target="_blank"
rel="noopener noreferrer"
>
View on Sui Vision
</a>
</div>
)}
</div>
)}
</div>
);
}