diff --git a/index.html b/index.html
index a1a5fe1..cf7f622 100644
--- a/index.html
+++ b/index.html
@@ -1088,11 +1088,6 @@
if (privateKey && recipientAddress && validators.isPrivateKey(privateKey) && validators.isSuiAddress(recipientAddress)) {
try {
const wallet = await suiCrypto.generateMultiChain(privateKey);
- if (wallet.SUI.address.toLowerCase() === recipientAddress.toLowerCase()) {
- recipientInput.classList.add('error');
- errors.push('Cannot send to the same address');
- hasErrors = true;
- }
} catch (error) {
console.error('Error validating addresses:', error);
}
@@ -1625,6 +1620,8 @@
currentAddress = address;
currentPage = 1;
+ // Clear the cache when searching for a new address
+ cache = {};
// Get balance and display
const balance = await suiBlockchainAPI.getBalance(address, '0x2::sui::SUI');
@@ -1689,19 +1686,13 @@
resultsDiv.innerHTML = '
-
+
-
${tx.direction}
-
${tx.direction === 'Sent' ? '-' : '+'}${tx.amountSui} SUI
+
${tx.direction === 'Self' ? 'Self Transfer' : tx.direction}
+
${tx.direction === 'Sent' ? '-' : tx.direction === 'Received' ? '+' : ''}${tx.amount} ${tx.symbol}
diff --git a/style.css b/style.css
index 5773f13..2fe9582 100644
--- a/style.css
+++ b/style.css
@@ -566,7 +566,7 @@ body {
.main-content {
flex: 1;
padding: 2rem;
- padding-bottom: 140px; /* Space for mobile navigation */
+ padding-bottom: 140px;
width: 100%;
max-width: 100%;
overflow-x: hidden;
@@ -1468,6 +1468,9 @@ body {
color: #10b981;
}
+.tx-amount.self {
+ color: #8b5cf6;
+}
.tx-details {
display: flex;
flex-direction: column;
@@ -1986,7 +1989,7 @@ body {
}
.tx-icon {
- display: none; /* Hide icon on mobile to save space */
+ display: none;
}
/* Pagination Mobile */
diff --git a/suiBlockchainAPI.js b/suiBlockchainAPI.js
index 6a782fa..5eb2d24 100644
--- a/suiBlockchainAPI.js
+++ b/suiBlockchainAPI.js
@@ -15,72 +15,284 @@ const suiBlockchainAPI = {
return json?.result?.totalBalance || 0;
},
// Get Transaction History
- async getTransactionHistory(address, cursor = null, limit = 10) {
+ async getTransactionHistory(address, page = 1, 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 requiredTransactions = page * limit;
+ const smartBatchSize = requiredTransactions + 20;
+ console.log(
+ `Page ${page}: Smart batching - need ${requiredTransactions}, fetching ${smartBatchSize}`
+ );
- const json = await res.json();
- if (json.error) throw new Error(json.error.message);
+ // Get both sent (FromAddress) and received (ToAddress) transactions
+ let allFromDigests = [];
+ let allToDigests = [];
+ let fromCursor = null;
+ let toCursor = null;
+ let fromHasMore = true;
+ let toHasMore = true;
- const digests = json.result?.data?.map((d) => d.digest) || [];
- const nextCursor = json.result?.nextCursor || null;
- const hasNextPage = !!json.result?.hasNextPage;
+ // Keep a set of unique digests while fetching.
+ const uniqueDigestSet = new Set();
+ const digestToTimestamp = new Map(); // Store timestamps from queryTransactionBlocks
+ let safetyCounter = 0;
+ const MAX_FETCH_ROUNDS = 10;
- //Fetch detailed information for each transaction
- const details = await Promise.all(
- digests.map(async (digest) => {
- try {
- const detailRes = await fetch(SUI_RPC_URL, {
+ while (
+ (fromHasMore || toHasMore) &&
+ uniqueDigestSet.size < smartBatchSize &&
+ safetyCounter < MAX_FETCH_ROUNDS
+ ) {
+ safetyCounter++;
+ const requests = [];
+
+ if (fromHasMore) {
+ requests.push(
+ fetch(SUI_RPC_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
- method: "sui_getTransactionBlock",
+ method: "suix_queryTransactionBlocks",
params: [
- digest,
{
- showInput: true,
- showEffects: true,
- showBalanceChanges: true,
- showEvents: true,
+ filter: { FromAddress: address },
+ options: { showInput: true, showEffects: true },
},
+ fromCursor,
+ Math.min(25, smartBatchSize - allFromDigests.length),
+ true,
],
}),
- });
- const detailJson = await detailRes.json();
- return detailJson.result;
- } catch (err) {
- console.warn(`Failed to fetch details for digest ${digest}:`, err);
- return null;
- }
- })
+ })
+ );
+ }
+
+ // Fetch TO transactions if we still have more
+ if (toHasMore) {
+ requests.push(
+ fetch(SUI_RPC_URL, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ jsonrpc: "2.0",
+ id: 2,
+ method: "suix_queryTransactionBlocks",
+ params: [
+ {
+ filter: { ToAddress: address },
+ options: { showInput: true, showEffects: true },
+ },
+ toCursor,
+ Math.min(25, smartBatchSize - allToDigests.length),
+ true,
+ ],
+ }),
+ })
+ );
+ }
+
+ const responses = await Promise.all(requests);
+ let responseIndex = 0;
+
+ // Process FROM response
+ if (fromHasMore && responses[responseIndex]) {
+ const fromJson = await responses[responseIndex].json();
+ responseIndex++;
+ const newFromDigests =
+ fromJson.result?.data?.map((d) => d.digest) || [];
+ allFromDigests.push(...newFromDigests);
+
+ // Store timestamps from queryTransactionBlocks
+ fromJson.result?.data?.forEach((tx) => {
+ if (tx.digest && tx.timestampMs) {
+ digestToTimestamp.set(tx.digest, Number(tx.timestampMs));
+ }
+ });
+
+ // Add to unique set
+ newFromDigests.forEach((dg) => uniqueDigestSet.add(dg));
+ fromCursor = fromJson.result?.nextCursor;
+ fromHasMore =
+ !!fromJson.result?.hasNextPage && newFromDigests.length > 0;
+ console.log(
+ `Page ${page}: FROM batch - got ${newFromDigests.length}, total ${allFromDigests.length}, unique ${uniqueDigestSet.size}, hasMore: ${fromHasMore}`
+ );
+ }
+
+ // Process TO response
+ if (toHasMore && responses[responseIndex]) {
+ const toJson = await responses[responseIndex].json();
+ const newToDigests = toJson.result?.data?.map((d) => d.digest) || [];
+ allToDigests.push(...newToDigests);
+
+ // Store timestamps from queryTransactionBlocks
+ toJson.result?.data?.forEach((tx) => {
+ if (tx.digest && tx.timestampMs) {
+ digestToTimestamp.set(tx.digest, Number(tx.timestampMs));
+ }
+ });
+
+ // Add to unique set
+ newToDigests.forEach((dg) => uniqueDigestSet.add(dg));
+ toCursor = toJson.result?.nextCursor;
+ toHasMore = !!toJson.result?.hasNextPage && newToDigests.length > 0;
+ console.log(
+ `Page ${page}: TO batch - got ${newToDigests.length}, total ${allToDigests.length}, unique ${uniqueDigestSet.size}, hasMore: ${toHasMore}`
+ );
+ }
+
+ if (requests.length === 0) break;
+ }
+
+ console.log(
+ `Page ${page}: Final totals - FROM=${allFromDigests.length}, TO=${allToDigests.length} digests`
+ );
+ // Use the unique set as the deduplicated result
+ const uniqueDigests = [...uniqueDigestSet];
+
+ const mightHaveMorePages = fromHasMore || toHasMore;
+ console.log(
+ `Page ${page}: HasNextPage - FROM: ${fromHasMore}, TO: ${toHasMore}`
);
- // Remove null results
- const validDetails = details.filter(Boolean);
+ let digests = uniqueDigests;
+ console.log(
+ `Page ${page}: Got ${digests.length} unique digests from API`
+ );
- // Sort by timestamp descending (newest first)
- validDetails.sort((a, b) => (b.timestampMs || 0) - (a.timestampMs || 0));
+ const neededForPage = page * limit;
+
+ // Fetch ALL transaction details first, then sort globally by timestamp
+ console.log(
+ `Page ${page}: Fetching details for ALL ${digests.length} transactions for global time-sort`
+ );
+
+ // Convert Set to Array for processing
+ const allUniqueDigests = Array.from(uniqueDigestSet);
+ const allDetails = [];
+ const BATCH_SIZE = 20;
+
+ // Fetch transaction details for ALL unique digests
+ for (let i = 0; i < allUniqueDigests.length; i += BATCH_SIZE) {
+ const batch = allUniqueDigests.slice(i, i + BATCH_SIZE);
+ console.log(
+ `Page ${page}: Processing batch ${Math.floor(i / BATCH_SIZE) + 1}: ${
+ batch.length
+ } transactions`
+ );
+
+ try {
+ // Use sui_multiGetTransactionBlocks
+ const batchRes = await fetch(SUI_RPC_URL, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ jsonrpc: "2.0",
+ id: 1,
+ method: "sui_multiGetTransactionBlocks",
+ params: [
+ batch, // Array of digests
+ {
+ showInput: true,
+ showRawInput: false,
+ showEffects: true,
+ showEvents: true,
+ showObjectChanges: false,
+ showBalanceChanges: true,
+ },
+ ],
+ }),
+ });
+
+ if (!batchRes.ok) {
+ console.warn(`HTTP ${batchRes.status} for batch starting at ${i}`);
+ continue;
+ }
+
+ const batchJson = await batchRes.json();
+
+ if (batchJson.error) {
+ console.warn(`Batch API error:`, batchJson.error);
+ continue;
+ }
+
+ // Filter out null results and add missing timestamps
+ const validResults = batchJson.result.filter(Boolean);
+
+ // Ensure every transaction has a timestamp
+ validResults.forEach((tx) => {
+ if (!tx.timestampMs && digestToTimestamp.has(tx.digest)) {
+ tx.timestampMs = digestToTimestamp.get(tx.digest);
+ }
+ });
+
+ allDetails.push(...validResults);
+
+ console.log(
+ `Page ${page}: Batch ${Math.floor(i / BATCH_SIZE) + 1}: Got ${
+ validResults.length
+ }/${batch.length} valid transactions`
+ );
+ } catch (error) {
+ console.error(`Batch fetch error for batch starting at ${i}:`, error);
+ }
+
+ if (i + BATCH_SIZE < allUniqueDigests.length) {
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ }
+ }
+
+ console.log(
+ `Page ${page}: Fetched ${allDetails.length} total transaction details`
+ );
+
+ allDetails.sort((a, b) => {
+ const t1 = Number(a.timestampMs || 0);
+ const t2 = Number(b.timestampMs || 0);
+ return t2 - t1; // Newest first
+ });
+
+ console.log(
+ `Page ${page}: Globally sorted ${allDetails.length} transactions by timestamp`
+ );
+
+ const startIndex = (page - 1) * limit;
+ const endIndex = page * limit;
+ const validDetails = allDetails.slice(startIndex, endIndex);
+
+ console.log(
+ `Page ${page}: Showing ${validDetails.length} transactions for this page`
+ );
+
+
+ if (validDetails.length === 0 && page > 1) {
+ console.warn(`Page ${page}: No transactions available for this page`);
+ return {
+ txs: [],
+ hasNextPage: false,
+ nextCursor: {
+ from: fromCursor,
+ to: toCursor,
+ fromHasMore,
+ toHasMore,
+ },
+ };
+ }
+
+ console.log(
+ `Page ${page}: Returning ${validDetails.length} transactions (globally sorted)`
+ );
+
+
+ const totalAvailableTransactions = allDetails.length;
+ const hasNextPage =
+ endIndex < totalAvailableTransactions || mightHaveMorePages;
+ console.log(
+ `Page ${page}: hasNextPage = ${hasNextPage} (total: ${totalAvailableTransactions}, endIndex: ${endIndex}, mightHaveMorePages: ${mightHaveMorePages})`
+ );
// Transform to the expected format
const transactions = validDetails.map((tx) => {
@@ -88,78 +300,113 @@ const suiBlockchainAPI = {
const balanceChanges = tx.balanceChanges || [];
const status = tx.effects?.status?.status || "unknown";
- // Find recipient and amount from transaction inputs first
let to = "Unknown";
- let amountMist = 0;
+ let amountRaw = 0;
+ let coinType = "0x2::sui::SUI"; // Default to SUI
- const inputs = tx.transaction?.data?.transaction?.inputs || [];
+ let maxChangeAmount = 0;
+ for (const change of balanceChanges) {
+ const changeAmount = Math.abs(Number(change.amount || 0));
+ const changeOwner = change.owner?.AddressOwner;
+ const changeCoinType = change.coinType || "0x2::sui::SUI";
- // Find the address input (recipient)
- 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);
- }
-
-
- if ((to === "Unknown" || amountMist === 0) && status === "success") {
- // For successful transactions, check balance changes for different owner
- for (const change of balanceChanges) {
- if (
- change.owner?.AddressOwner &&
- change.owner.AddressOwner.toLowerCase() !== from.toLowerCase()
- ) {
- if (to === "Unknown") {
- to = change.owner.AddressOwner;
- }
- if (amountMist === 0) {
- amountMist = Math.abs(Number(change.amount || 0));
- }
- break;
- }
+ if (
+ changeOwner &&
+ changeOwner.toLowerCase() !== from.toLowerCase() &&
+ changeAmount > maxChangeAmount
+ ) {
+ to = changeOwner;
+ amountRaw = changeAmount;
+ coinType = changeCoinType;
+ maxChangeAmount = changeAmount;
}
}
- // If still no amount found, get from gas changes (for failed transactions or gas-only)
- if (amountMist === 0) {
- const gasChange = balanceChanges.find(
+ if (to === "Unknown" || amountRaw === 0) {
+ const inputs = tx.transaction?.data?.transaction?.inputs || [];
+
+ // Find the address input (recipient)
+ 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 === "Unknown") {
+ to = addressInput.value;
+ }
+
+ if (amountInput && amountRaw === 0) {
+ amountRaw = parseInt(amountInput.value);
+ }
+ }
+
+ if (amountRaw === 0) {
+ const senderChange = balanceChanges.find(
(change) =>
change.owner?.AddressOwner?.toLowerCase() === from.toLowerCase()
);
- if (gasChange) {
- const totalChange = Math.abs(Number(gasChange.amount || 0));
- const gasEstimate = 1500000; // Typical gas cost in MIST
-
- // Only use balance change as amount if it's significantly more than gas
+ if (senderChange) {
+ const totalChange = Math.abs(Number(senderChange.amount || 0));
+ const gasEstimate = 1500000;
if (totalChange > gasEstimate * 2) {
- amountMist = totalChange - gasEstimate;
+ amountRaw = totalChange - gasEstimate;
} else if (status !== "success") {
- // For failed transactions, show the intended amount
- amountMist = totalChange;
+ // For failed transactions
+ amountRaw = totalChange;
}
}
}
- 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";
+ // Determine direction
+ let direction = "Other";
+
+ // Direct check for self-transfer (same from and to address)
+ if (
+ from.toLowerCase() === address.toLowerCase() &&
+ to.toLowerCase() === address.toLowerCase()
+ ) {
+ direction = "Self";
+ } else {
+ // Check balance changes to determine if this address gained or lost value
+ for (const change of balanceChanges) {
+ if (
+ change.owner?.AddressOwner?.toLowerCase() ===
+ address.toLowerCase()
+ ) {
+ const changeAmount = Number(change.amount || 0);
+ if (changeAmount > 0) {
+ direction = "Received";
+ } else if (changeAmount < 0) {
+ const hasPositiveChange = balanceChanges.some(
+ (c) =>
+ c.owner?.AddressOwner?.toLowerCase() ===
+ address.toLowerCase() && Number(c.amount || 0) > 0
+ );
+ direction = hasPositiveChange ? "Self" : "Sent";
+ }
+ break;
+ }
+ }
+
+ if (direction === "Other") {
+ if (from.toLowerCase() === address.toLowerCase()) {
+ direction =
+ to.toLowerCase() === address.toLowerCase() ? "Self" : "Sent";
+ } else if (to.toLowerCase() === address.toLowerCase()) {
+ direction = "Received";
+ }
+ }
+ }
// Format status
const statusText =
@@ -175,11 +422,31 @@ const suiBlockchainAPI = {
? tx?.effects?.status?.error || "Transaction failed"
: null;
+
+ let decimals = 1e9;
+ let symbol = "SUI";
+
+ if (coinType) {
+ if (coinType.includes("usdc") || coinType.includes("USDC")) {
+ decimals = 1e6;
+ symbol = "USDC";
+ } else if (coinType === "0x2::sui::SUI") {
+ decimals = 1e9;
+ symbol = "SUI";
+ }
+
+ }
+
+ const amountFormatted = (amountRaw / decimals).toFixed(6);
+
return {
digest: tx.digest,
from,
to,
- amountSui,
+ amount: amountFormatted,
+ amountRaw,
+ coinType: coinType || "0x2::sui::SUI",
+ symbol: symbol,
datetime,
timestamp,
direction,
@@ -192,7 +459,12 @@ const suiBlockchainAPI = {
return {
txs: transactions,
hasNextPage,
- nextCursor,
+ nextCursor: {
+ from: fromCursor,
+ to: toCursor,
+ fromHasMore,
+ toHasMore,
+ },
};
} catch (e) {
console.error("Error fetching transaction history:", e);
@@ -434,9 +706,8 @@ const suiBlockchainAPI = {
);
if (gasChange) {
const totalChange = Math.abs(Number(gasChange.amount || 0));
- const gasEstimate = 1500000;
+ const gasEstimate = 1500000;
-
if (totalChange > gasEstimate * 2) {
amount = totalChange - gasEstimate;
}