462 lines
14 KiB
JavaScript
462 lines
14 KiB
JavaScript
const suiBlockchainAPI = {
|
|
// Get Balance
|
|
async getBalance(address, coinType = "0x2::sui::SUI") {
|
|
const res = await fetch("https://fullnode.mainnet.sui.io:443", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "suix_getBalance",
|
|
params: [address, coinType],
|
|
}),
|
|
});
|
|
const json = await res.json();
|
|
return json?.result?.totalBalance || 0;
|
|
},
|
|
// Get Transaction History
|
|
async getTransactionHistory(address, cursor = null, limit = 10) {
|
|
const SUI_RPC_URL = "https://fullnode.mainnet.sui.io:443";
|
|
|
|
try {
|
|
//Query transaction digests using ToAddress filter
|
|
const res = await fetch(SUI_RPC_URL, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "suix_queryTransactionBlocks",
|
|
params: [
|
|
{
|
|
filter: { ToAddress: address },
|
|
},
|
|
cursor,
|
|
limit,
|
|
true,
|
|
],
|
|
}),
|
|
});
|
|
|
|
const json = await res.json();
|
|
if (json.error) throw new Error(json.error.message);
|
|
|
|
const digests = json.result?.data?.map((d) => d.digest) || [];
|
|
const nextCursor = json.result?.nextCursor || null;
|
|
const hasNextPage = !!json.result?.hasNextPage;
|
|
|
|
//Fetch detailed information for each transaction
|
|
const details = await Promise.all(
|
|
digests.map(async (digest) => {
|
|
try {
|
|
const detailRes = await fetch(SUI_RPC_URL, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "sui_getTransactionBlock",
|
|
params: [
|
|
digest,
|
|
{
|
|
showInput: true,
|
|
showEffects: true,
|
|
showBalanceChanges: true,
|
|
showEvents: true,
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
const detailJson = await detailRes.json();
|
|
return detailJson.result;
|
|
} catch (err) {
|
|
console.warn(`Failed to fetch details for digest ${digest}:`, err);
|
|
return null;
|
|
}
|
|
})
|
|
);
|
|
|
|
// Remove null results
|
|
const validDetails = details.filter(Boolean);
|
|
|
|
// Sort by timestamp descending (newest first)
|
|
validDetails.sort((a, b) => (b.timestampMs || 0) - (a.timestampMs || 0));
|
|
|
|
// Transform to the expected format
|
|
const transactions = validDetails.map((tx) => {
|
|
const from = tx.transaction?.data?.sender || "Unknown";
|
|
const balanceChanges = tx.balanceChanges || [];
|
|
const status = tx.effects?.status?.status || "unknown";
|
|
|
|
// Find recipient and amount from balance changes or transaction inputs
|
|
let to = "Unknown";
|
|
let amountMist = 0;
|
|
|
|
if (status === "success") {
|
|
// For successful transactions, get from balance changes
|
|
for (const change of balanceChanges) {
|
|
if (
|
|
change.owner?.AddressOwner &&
|
|
change.owner.AddressOwner.toLowerCase() !== from.toLowerCase()
|
|
) {
|
|
to = change.owner.AddressOwner;
|
|
amountMist = Math.abs(Number(change.amount || 0));
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
// For failed transactions, get intended recipient from transaction inputs
|
|
const inputs = tx.transaction?.data?.transaction?.inputs || [];
|
|
|
|
const addressInput = inputs.find(
|
|
(input) => input.type === "pure" && input.valueType === "address"
|
|
);
|
|
|
|
// Find the amount input
|
|
const amountInput = inputs.find(
|
|
(input) => input.type === "pure" && input.valueType === "u64"
|
|
);
|
|
|
|
if (addressInput) {
|
|
to = addressInput.value;
|
|
}
|
|
|
|
if (amountInput) {
|
|
amountMist = parseInt(amountInput.value);
|
|
}
|
|
|
|
const gasChange = balanceChanges.find(
|
|
(change) =>
|
|
change.owner?.AddressOwner?.toLowerCase() === from.toLowerCase()
|
|
);
|
|
if (gasChange && !amountMist) {
|
|
amountMist = Math.abs(Number(gasChange.amount || 0));
|
|
}
|
|
}
|
|
|
|
const amountSui = (amountMist / 1e9).toFixed(6);
|
|
const datetime = tx.timestampMs
|
|
? new Date(Number(tx.timestampMs)).toLocaleString()
|
|
: "N/A";
|
|
const timestamp = Number(tx.timestampMs || 0);
|
|
|
|
// Determine direction based on address
|
|
const direction =
|
|
from.toLowerCase() === address.toLowerCase() ? "Sent" : "Received";
|
|
|
|
// Format status
|
|
const statusText =
|
|
status === "success"
|
|
? "Confirmed"
|
|
: status === "failure"
|
|
? "Failed"
|
|
: status.charAt(0).toUpperCase() + status.slice(1);
|
|
|
|
// Get error message if failed
|
|
const errorMessage =
|
|
status === "failure"
|
|
? tx?.effects?.status?.error || "Transaction failed"
|
|
: null;
|
|
|
|
return {
|
|
digest: tx.digest,
|
|
from,
|
|
to,
|
|
amountSui,
|
|
datetime,
|
|
timestamp,
|
|
direction,
|
|
status: statusText,
|
|
rawStatus: status,
|
|
errorMessage,
|
|
};
|
|
});
|
|
|
|
return {
|
|
txs: transactions,
|
|
hasNextPage,
|
|
nextCursor,
|
|
};
|
|
} catch (e) {
|
|
console.error("Error fetching transaction history:", e);
|
|
return {
|
|
txs: [],
|
|
hasNextPage: false,
|
|
nextCursor: null,
|
|
};
|
|
}
|
|
},
|
|
|
|
// SUI Transaction
|
|
async prepareSuiTransaction(privateKey, recipientAddress, amount) {
|
|
const SUI_RPC_URL = "https://fullnode.mainnet.sui.io:443";
|
|
|
|
if (!privateKey || !recipientAddress || !amount)
|
|
throw new Error("Missing required parameters.");
|
|
|
|
const amt = parseFloat(amount);
|
|
if (isNaN(amt) || amt <= 0) throw new Error("Invalid amount specified.");
|
|
|
|
// Get sender's address and private key from any supported format
|
|
const wallet = await suiCrypto.generateMultiChain(privateKey);
|
|
const senderAddress = wallet.SUI.address;
|
|
const suiPrivateKey = wallet.SUI.privateKey;
|
|
|
|
// Get sender coins
|
|
const coinResponse = await fetch(SUI_RPC_URL, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "suix_getCoins",
|
|
params: [senderAddress, "0x2::sui::SUI"],
|
|
}),
|
|
});
|
|
const coinJson = await coinResponse.json();
|
|
if (coinJson.error) throw new Error(coinJson.error.message);
|
|
const coins = coinJson.result.data;
|
|
if (!coins.length) throw new Error("No SUI balance found.");
|
|
|
|
const amountInMist = Math.floor(amt * 1e9).toString();
|
|
|
|
const txResponse = await fetch(SUI_RPC_URL, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "unsafe_paySui",
|
|
params: [
|
|
senderAddress,
|
|
[coins[0].coinObjectId],
|
|
[recipientAddress],
|
|
[amountInMist],
|
|
"50000000",
|
|
],
|
|
}),
|
|
});
|
|
const txJson = await txResponse.json();
|
|
|
|
if (txJson.error) throw new Error(`Build failed: ${txJson.error.message}`);
|
|
const txBytes = txJson.result.txBytes;
|
|
|
|
// Simulate for gas estimate
|
|
const dryRunResponse = await fetch(SUI_RPC_URL, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "sui_dryRunTransactionBlock",
|
|
params: [txBytes],
|
|
}),
|
|
});
|
|
const dryRunJson = await dryRunResponse.json();
|
|
if (dryRunJson.error)
|
|
throw new Error(`Dry run failed: ${dryRunJson.error.message}`);
|
|
|
|
const gasUsed = dryRunJson.result.effects.gasUsed;
|
|
const gasFee =
|
|
parseInt(gasUsed.computationCost) +
|
|
parseInt(gasUsed.storageCost) -
|
|
parseInt(gasUsed.storageRebate);
|
|
|
|
return {
|
|
senderAddress,
|
|
suiPrivateKey,
|
|
txBytes,
|
|
gasFee: gasFee,
|
|
};
|
|
},
|
|
|
|
// Sign and Send SUI Transaction
|
|
async signAndSendSuiTransaction(preparedTx) {
|
|
const SUI_RPC_URL = "https://fullnode.mainnet.sui.io:443";
|
|
|
|
// Sign the transaction bytes
|
|
const signature = await suiCrypto.sign(
|
|
preparedTx.txBytes,
|
|
preparedTx.suiPrivateKey
|
|
);
|
|
|
|
// Execute the transaction
|
|
const response = await fetch(SUI_RPC_URL, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "sui_executeTransactionBlock",
|
|
params: [
|
|
preparedTx.txBytes,
|
|
[signature],
|
|
null,
|
|
"WaitForLocalExecution",
|
|
],
|
|
}),
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (result.error) throw new Error(result.error.message);
|
|
return result;
|
|
},
|
|
|
|
// Get Transaction Details by Hash
|
|
async getTransactionDetails(transactionHash) {
|
|
const SUI_RPC_URL = "https://fullnode.mainnet.sui.io:443";
|
|
|
|
if (!transactionHash) {
|
|
throw new Error("Transaction hash is required");
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(SUI_RPC_URL, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "sui_getTransactionBlock",
|
|
params: [
|
|
transactionHash,
|
|
{
|
|
showInput: true,
|
|
showEffects: true,
|
|
showEvents: true,
|
|
showBalanceChanges: true,
|
|
showObjectChanges: true,
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
|
|
const json = await res.json();
|
|
|
|
if (json.error) {
|
|
throw new Error(json.error.message);
|
|
}
|
|
|
|
const txData = json.result;
|
|
if (!txData) {
|
|
throw new Error("Transaction not found");
|
|
}
|
|
|
|
// Extract transaction details
|
|
const digest = txData.digest;
|
|
const sender = txData.transaction?.data?.sender || "Unknown";
|
|
const gasUsed = txData.effects?.gasUsed;
|
|
const status = txData.effects?.status?.status || "Unknown";
|
|
const timestamp = txData.timestampMs
|
|
? new Date(Number(txData.timestampMs)).toLocaleString()
|
|
: "N/A";
|
|
|
|
// Extract transfer information
|
|
let recipient = "Unknown";
|
|
let amount = 0;
|
|
let coinType = "0x2::sui::SUI";
|
|
|
|
if (status === "success") {
|
|
// For successful transactions, check events first
|
|
for (const event of txData.events || []) {
|
|
if (
|
|
event.type?.includes("TransferEvent") ||
|
|
event.type?.includes("::coin::Transfer")
|
|
) {
|
|
recipient = event.parsedJson?.recipient || "Unknown";
|
|
amount = Number(event.parsedJson?.amount || 0);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If no transfer event found, check balance changes
|
|
if (recipient === "Unknown" && txData.balanceChanges?.length) {
|
|
const change = txData.balanceChanges.find(
|
|
(c) => c.owner?.AddressOwner && c.owner.AddressOwner !== sender
|
|
);
|
|
if (change) {
|
|
recipient = change.owner.AddressOwner;
|
|
amount = Math.abs(Number(change.amount || 0));
|
|
coinType = change.coinType || coinType;
|
|
}
|
|
}
|
|
} else {
|
|
// For failed transactions, get intended recipient from transaction inputs
|
|
const inputs = txData.transaction?.data?.transaction?.inputs || [];
|
|
|
|
// Find the address input
|
|
const addressInput = inputs.find(
|
|
(input) => input.type === "pure" && input.valueType === "address"
|
|
);
|
|
|
|
// Find the amount input
|
|
const amountInput = inputs.find(
|
|
(input) => input.type === "pure" && input.valueType === "u64"
|
|
);
|
|
|
|
if (addressInput) {
|
|
recipient = addressInput.value;
|
|
}
|
|
|
|
if (amountInput) {
|
|
amount = parseInt(amountInput.value);
|
|
}
|
|
|
|
if (!amount && txData.balanceChanges?.length) {
|
|
const gasChange = txData.balanceChanges.find(
|
|
(change) =>
|
|
change.owner?.AddressOwner?.toLowerCase() === sender.toLowerCase()
|
|
);
|
|
if (gasChange) {
|
|
amount = Math.abs(Number(gasChange.amount || 0));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate gas fee
|
|
const gasFee = gasUsed
|
|
? parseInt(gasUsed.computationCost) +
|
|
parseInt(gasUsed.storageCost) -
|
|
parseInt(gasUsed.storageRebate)
|
|
: 0;
|
|
|
|
// Format status for display
|
|
const statusText =
|
|
status === "success"
|
|
? "Confirmed"
|
|
: status === "failure"
|
|
? "Failed"
|
|
: status === "unknown"
|
|
? "Unknown"
|
|
: status.charAt(0).toUpperCase() + status.slice(1);
|
|
|
|
// Get error message if transaction failed
|
|
const errorMessage =
|
|
status === "failure"
|
|
? txData?.effects?.status?.error ||
|
|
txData?.effects?.status?.errorMessage ||
|
|
"Transaction failed"
|
|
: null;
|
|
|
|
return {
|
|
digest,
|
|
sender,
|
|
recipient,
|
|
amount: (amount / 1e9).toFixed(6), // Convert MIST to SUI
|
|
coinType,
|
|
status: statusText,
|
|
rawStatus: status,
|
|
timestamp,
|
|
gasUsed: (gasFee / 1e9).toFixed(6), // Convert MIST to SUI
|
|
errorMessage,
|
|
rawData: txData,
|
|
};
|
|
} catch (error) {
|
|
console.error("Error fetching transaction details:", error);
|
|
throw error;
|
|
}
|
|
},
|
|
};
|
|
|
|
window.suiBlockchainAPI = suiBlockchainAPI;
|