suiwallet/suiBlockchainAPI.js
2025-11-08 00:05:15 +05:30

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;