Workflow updating files of suiwallet
This commit is contained in:
parent
4a18d6c344
commit
16e0a1cc94
2449
suiwallet/index.html
Normal file
2449
suiwallet/index.html
Normal file
File diff suppressed because it is too large
Load Diff
11473
suiwallet/lib.sui.js
Normal file
11473
suiwallet/lib.sui.js
Normal file
File diff suppressed because it is too large
Load Diff
2937
suiwallet/style.css
Normal file
2937
suiwallet/style.css
Normal file
File diff suppressed because it is too large
Load Diff
762
suiwallet/suiBlockchainAPI.js
Normal file
762
suiwallet/suiBlockchainAPI.js
Normal file
@ -0,0 +1,762 @@
|
|||||||
|
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, page = 1, limit = 10) {
|
||||||
|
const SUI_RPC_URL = "https://fullnode.mainnet.sui.io:443";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requiredTransactions = page * limit;
|
||||||
|
const smartBatchSize = requiredTransactions + 20;
|
||||||
|
console.log(
|
||||||
|
`Page ${page}: Smart batching - need ${requiredTransactions}, fetching ${smartBatchSize}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get both sent (FromAddress) and received (ToAddress) transactions
|
||||||
|
let allFromDigests = [];
|
||||||
|
let allToDigests = [];
|
||||||
|
let fromCursor = null;
|
||||||
|
let toCursor = null;
|
||||||
|
let fromHasMore = true;
|
||||||
|
let toHasMore = true;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
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: "suix_queryTransactionBlocks",
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
filter: { FromAddress: address },
|
||||||
|
options: { showInput: true, showEffects: true },
|
||||||
|
},
|
||||||
|
fromCursor,
|
||||||
|
Math.min(25, smartBatchSize - allFromDigests.length),
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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}`
|
||||||
|
);
|
||||||
|
|
||||||
|
let digests = uniqueDigests;
|
||||||
|
console.log(
|
||||||
|
`Page ${page}: Got ${digests.length} unique digests from API`
|
||||||
|
);
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
const from = tx.transaction?.data?.sender || "Unknown";
|
||||||
|
const balanceChanges = tx.balanceChanges || [];
|
||||||
|
const status = tx.effects?.status?.status || "unknown";
|
||||||
|
|
||||||
|
let to = "Unknown";
|
||||||
|
let amountRaw = 0;
|
||||||
|
let coinType = "0x2::sui::SUI"; // Default to SUI
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
if (
|
||||||
|
changeOwner &&
|
||||||
|
changeOwner.toLowerCase() !== from.toLowerCase() &&
|
||||||
|
changeAmount > maxChangeAmount
|
||||||
|
) {
|
||||||
|
to = changeOwner;
|
||||||
|
amountRaw = changeAmount;
|
||||||
|
coinType = changeCoinType;
|
||||||
|
maxChangeAmount = changeAmount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (senderChange) {
|
||||||
|
const totalChange = Math.abs(Number(senderChange.amount || 0));
|
||||||
|
const gasEstimate = 1500000;
|
||||||
|
if (totalChange > gasEstimate * 2) {
|
||||||
|
amountRaw = totalChange - gasEstimate;
|
||||||
|
} else if (status !== "success") {
|
||||||
|
// For failed transactions
|
||||||
|
amountRaw = totalChange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const datetime = tx.timestampMs
|
||||||
|
? new Date(Number(tx.timestampMs)).toLocaleString()
|
||||||
|
: "N/A";
|
||||||
|
const timestamp = Number(tx.timestampMs || 0);
|
||||||
|
|
||||||
|
// 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 =
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
amount: amountFormatted,
|
||||||
|
amountRaw,
|
||||||
|
coinType: coinType || "0x2::sui::SUI",
|
||||||
|
symbol: symbol,
|
||||||
|
datetime,
|
||||||
|
timestamp,
|
||||||
|
direction,
|
||||||
|
status: statusText,
|
||||||
|
rawStatus: status,
|
||||||
|
errorMessage,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
txs: transactions,
|
||||||
|
hasNextPage,
|
||||||
|
nextCursor: {
|
||||||
|
from: fromCursor,
|
||||||
|
to: toCursor,
|
||||||
|
fromHasMore,
|
||||||
|
toHasMore,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} 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";
|
||||||
|
|
||||||
|
// First, try to get recipient and amount from transaction inputs (works for all transactions)
|
||||||
|
const inputs = txData.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) {
|
||||||
|
recipient = addressInput.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amountInput) {
|
||||||
|
amount = parseInt(amountInput.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "success" && (recipient === "Unknown" || amount === 0)) {
|
||||||
|
// Check events for transfer information
|
||||||
|
for (const event of txData.events || []) {
|
||||||
|
if (
|
||||||
|
event.type?.includes("TransferEvent") ||
|
||||||
|
event.type?.includes("::coin::Transfer")
|
||||||
|
) {
|
||||||
|
if (recipient === "Unknown") {
|
||||||
|
recipient = event.parsedJson?.recipient || recipient;
|
||||||
|
}
|
||||||
|
if (amount === 0) {
|
||||||
|
amount = Number(event.parsedJson?.amount || 0);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still no recipient found, check balance changes for different owner
|
||||||
|
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;
|
||||||
|
if (amount === 0) {
|
||||||
|
amount = Math.abs(Number(change.amount || 0));
|
||||||
|
}
|
||||||
|
coinType = change.coinType || coinType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still no amount, try to get it from balance changes
|
||||||
|
if (amount === 0 && txData.balanceChanges?.length) {
|
||||||
|
const gasChange = txData.balanceChanges.find(
|
||||||
|
(change) =>
|
||||||
|
change.owner?.AddressOwner?.toLowerCase() === sender.toLowerCase()
|
||||||
|
);
|
||||||
|
if (gasChange) {
|
||||||
|
const totalChange = Math.abs(Number(gasChange.amount || 0));
|
||||||
|
const gasEstimate = 1500000;
|
||||||
|
|
||||||
|
if (totalChange > gasEstimate * 2) {
|
||||||
|
amount = totalChange - gasEstimate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
217
suiwallet/suiCrypto.js
Normal file
217
suiwallet/suiCrypto.js
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
(function (EXPORTS) {
|
||||||
|
"use strict";
|
||||||
|
const suiCrypto = EXPORTS;
|
||||||
|
|
||||||
|
// Generate a new random key
|
||||||
|
function generateNewID() {
|
||||||
|
var key = new Bitcoin.ECKey(false);
|
||||||
|
key.setCompressed(true);
|
||||||
|
return {
|
||||||
|
floID: key.getBitcoinAddress(),
|
||||||
|
pubKey: key.getPubKeyHex(),
|
||||||
|
privKey: key.getBitcoinWalletImportFormat(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperties(suiCrypto, {
|
||||||
|
newID: {
|
||||||
|
get: () => generateNewID(),
|
||||||
|
},
|
||||||
|
hashID: {
|
||||||
|
value: (str) => {
|
||||||
|
let bytes = ripemd160(Crypto.SHA256(str, { asBytes: true }), {
|
||||||
|
asBytes: true,
|
||||||
|
});
|
||||||
|
bytes.unshift(bitjs.pub);
|
||||||
|
var hash = Crypto.SHA256(Crypto.SHA256(bytes, { asBytes: true }), {
|
||||||
|
asBytes: true,
|
||||||
|
});
|
||||||
|
var checksum = hash.slice(0, 4);
|
||||||
|
return bitjs.Base58.encode(bytes.concat(checksum));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tmpID: {
|
||||||
|
get: () => {
|
||||||
|
let bytes = Crypto.util.randomBytes(20);
|
||||||
|
bytes.unshift(bitjs.pub);
|
||||||
|
var hash = Crypto.SHA256(Crypto.SHA256(bytes, { asBytes: true }), {
|
||||||
|
asBytes: true,
|
||||||
|
});
|
||||||
|
var checksum = hash.slice(0, 4);
|
||||||
|
return bitjs.Base58.encode(bytes.concat(checksum));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Multi-chain Generator (BTC, FLO, SUI) ---
|
||||||
|
suiCrypto.generateMultiChain = async function (inputWif) {
|
||||||
|
const versions = {
|
||||||
|
BTC: { pub: 0x00, priv: 0x80 },
|
||||||
|
FLO: { pub: 0x23, priv: 0xa3 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const origBitjsPub = bitjs.pub;
|
||||||
|
const origBitjsPriv = bitjs.priv;
|
||||||
|
const origBitjsCompressed = bitjs.compressed;
|
||||||
|
const origCoinJsCompressed = coinjs.compressed;
|
||||||
|
|
||||||
|
bitjs.compressed = true;
|
||||||
|
coinjs.compressed = true;
|
||||||
|
|
||||||
|
let privKeyHex;
|
||||||
|
let compressed = true;
|
||||||
|
|
||||||
|
// --- Decode input or generate new ---
|
||||||
|
if (typeof inputWif === "string" && inputWif.trim().length > 0) {
|
||||||
|
const trimmedInput = inputWif.trim();
|
||||||
|
const hexOnly = /^[0-9a-fA-F]+$/.test(trimmedInput);
|
||||||
|
|
||||||
|
if (trimmedInput.startsWith('suiprivkey1')) {
|
||||||
|
try {
|
||||||
|
const decoded = coinjs.bech32_decode(trimmedInput);
|
||||||
|
if (!decoded) throw new Error('Invalid SUI private key checksum');
|
||||||
|
const bytes = coinjs.bech32_convert(decoded.data, 5, 8, false);
|
||||||
|
// First byte is the scheme flag (should be 0x00 for Ed25519), the rest is the 32-byte private key.
|
||||||
|
if (bytes[0] !== 0) throw new Error('Unsupported SUI private key scheme');
|
||||||
|
const privateKeyBytes = bytes.slice(1);
|
||||||
|
privKeyHex = Crypto.util.bytesToHex(privateKeyBytes);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Invalid SUI private key, generating new key:", e);
|
||||||
|
const newKey = generateNewID();
|
||||||
|
const decode = Bitcoin.Base58.decode(newKey.privKey);
|
||||||
|
const keyWithVersion = decode.slice(0, decode.length - 4);
|
||||||
|
let key = keyWithVersion.slice(1);
|
||||||
|
if (key.length >= 33 && key[key.length - 1] === 0x01)
|
||||||
|
key = key.slice(0, key.length - 1);
|
||||||
|
privKeyHex = Crypto.util.bytesToHex(key);
|
||||||
|
}
|
||||||
|
} else if (hexOnly && (trimmedInput.length === 64 || trimmedInput.length === 128)) {
|
||||||
|
privKeyHex =
|
||||||
|
trimmedInput.length === 128 ? trimmedInput.substring(0, 64) : trimmedInput;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const decode = Bitcoin.Base58.decode(trimmedInput);
|
||||||
|
const keyWithVersion = decode.slice(0, decode.length - 4);
|
||||||
|
let key = keyWithVersion.slice(1);
|
||||||
|
if (key.length >= 33 && key[key.length - 1] === 0x01) {
|
||||||
|
key = key.slice(0, key.length - 1);
|
||||||
|
compressed = true;
|
||||||
|
}
|
||||||
|
privKeyHex = Crypto.util.bytesToHex(key);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Invalid WIF, generating new key:", e);
|
||||||
|
const newKey = generateNewID();
|
||||||
|
const decode = Bitcoin.Base58.decode(newKey.privKey);
|
||||||
|
const keyWithVersion = decode.slice(0, decode.length - 4);
|
||||||
|
let key = keyWithVersion.slice(1);
|
||||||
|
if (key.length >= 33 && key[key.length - 1] === 0x01)
|
||||||
|
key = key.slice(0, key.length - 1);
|
||||||
|
privKeyHex = Crypto.util.bytesToHex(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Generate new key if no input
|
||||||
|
const newKey = generateNewID();
|
||||||
|
const decode = Bitcoin.Base58.decode(newKey.privKey);
|
||||||
|
const keyWithVersion = decode.slice(0, decode.length - 4);
|
||||||
|
let key = keyWithVersion.slice(1);
|
||||||
|
if (key.length >= 33 && key[key.length - 1] === 0x01)
|
||||||
|
key = key.slice(0, key.length - 1);
|
||||||
|
privKeyHex = Crypto.util.bytesToHex(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Derive addresses for each chain ---
|
||||||
|
const result = { BTC: {}, FLO: {}, SUI: {} };
|
||||||
|
|
||||||
|
// BTC
|
||||||
|
bitjs.pub = versions.BTC.pub;
|
||||||
|
bitjs.priv = versions.BTC.priv;
|
||||||
|
const pubKeyBTC = bitjs.newPubkey(privKeyHex);
|
||||||
|
result.BTC.address = coinjs.bech32Address(pubKeyBTC).address;
|
||||||
|
result.BTC.privateKey = bitjs.privkey2wif(privKeyHex);
|
||||||
|
|
||||||
|
// FLO
|
||||||
|
bitjs.pub = versions.FLO.pub;
|
||||||
|
bitjs.priv = versions.FLO.priv;
|
||||||
|
const pubKeyFLO = bitjs.newPubkey(privKeyHex);
|
||||||
|
result.FLO.address = bitjs.pubkey2address(pubKeyFLO);
|
||||||
|
result.FLO.privateKey = bitjs.privkey2wif(privKeyHex);
|
||||||
|
|
||||||
|
// SUI
|
||||||
|
|
||||||
|
try {
|
||||||
|
const privBytes = Crypto.util.hexToBytes(privKeyHex.substring(0, 64));
|
||||||
|
const seed = new Uint8Array(privBytes.slice(0, 32));
|
||||||
|
|
||||||
|
// Generate Ed25519 keypair from seed
|
||||||
|
const keyPair = nacl.sign.keyPair.fromSeed(seed);
|
||||||
|
const pubKey = keyPair.publicKey;
|
||||||
|
|
||||||
|
const prefixedPubKey = new Uint8Array([0x00, ...pubKey]);
|
||||||
|
|
||||||
|
// Hash with BLAKE2b-256
|
||||||
|
const hash = blakejs.blake2b(prefixedPubKey, null, 32);
|
||||||
|
|
||||||
|
// Convert to hex address
|
||||||
|
const suiAddress = "0x" + Crypto.util.bytesToHex(hash);
|
||||||
|
|
||||||
|
// Encode the private key in Sui's Bech32 format
|
||||||
|
const privateKeyBytes = new Uint8Array([0x00, ...seed]);
|
||||||
|
const words = coinjs.bech32_convert(Array.from(privateKeyBytes), 8, 5, true);
|
||||||
|
const suiPrivateKey = coinjs.bech32_encode('suiprivkey', words);
|
||||||
|
|
||||||
|
result.SUI.address = suiAddress;
|
||||||
|
result.SUI.privateKey = suiPrivateKey;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error generating SUI address:", error);
|
||||||
|
result.SUI.address = "Error generating address";
|
||||||
|
result.SUI.privateKey = privKeyHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
bitjs.pub = origBitjsPub;
|
||||||
|
bitjs.priv = origBitjsPriv;
|
||||||
|
bitjs.compressed = origBitjsCompressed;
|
||||||
|
coinjs.compressed = origCoinJsCompressed;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sign Transaction
|
||||||
|
suiCrypto.sign = async function (txBytes, suiPrivateKey) {
|
||||||
|
// Decode the private key from Bech32
|
||||||
|
const decoded = coinjs.bech32_decode(suiPrivateKey);
|
||||||
|
if (!decoded) throw new Error("Invalid SUI private key format.");
|
||||||
|
const keyBytes = coinjs.bech32_convert(decoded.data, 5, 8, false);
|
||||||
|
|
||||||
|
// The first byte is the scheme flag (0x00 for Ed25519), the rest is the seed.
|
||||||
|
if (keyBytes[0] !== 0x00) {
|
||||||
|
throw new Error("Unsupported SUI private key scheme.");
|
||||||
|
}
|
||||||
|
const seed = new Uint8Array(keyBytes.slice(1));
|
||||||
|
|
||||||
|
// Re-derive the keypair from the seed
|
||||||
|
const keypair = nacl.sign.keyPair.fromSeed(seed);
|
||||||
|
|
||||||
|
// Decode the transaction bytes from base64
|
||||||
|
const txData = new Uint8Array(atob(txBytes).split('').map(c => c.charCodeAt(0)));
|
||||||
|
|
||||||
|
// Create the message to sign
|
||||||
|
const INTENT_BYTES = [0, 0, 0];
|
||||||
|
const messageToSign = new Uint8Array(INTENT_BYTES.length + txData.length);
|
||||||
|
messageToSign.set(INTENT_BYTES);
|
||||||
|
messageToSign.set(txData, INTENT_BYTES.length);
|
||||||
|
|
||||||
|
// Sign the message digest
|
||||||
|
const digest = blakejs.blake2b(messageToSign, null, 32);
|
||||||
|
const signature = nacl.sign.detached(digest, keypair.secretKey);
|
||||||
|
|
||||||
|
// Combine signature scheme flag (0x00 for Ed25519) with the signature and public key
|
||||||
|
const suiSignature = new Uint8Array(1 + signature.length + keypair.publicKey.length);
|
||||||
|
suiSignature[0] = 0x00; // Ed25519 scheme
|
||||||
|
suiSignature.set(signature, 1);
|
||||||
|
suiSignature.set(keypair.publicKey, 1 + signature.length);
|
||||||
|
|
||||||
|
return btoa(String.fromCharCode.apply(null, suiSignature));
|
||||||
|
};
|
||||||
|
|
||||||
|
})("object" === typeof module ? module.exports : (window.suiCrypto = {}));
|
||||||
105
suiwallet/suiSearchDB.js
Normal file
105
suiwallet/suiSearchDB.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
class SearchedAddressDB {
|
||||||
|
constructor() {
|
||||||
|
this.dbName = "SuiWalletDB";
|
||||||
|
this.version = 1;
|
||||||
|
this.storeName = "searchedAddresses";
|
||||||
|
this.db = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open(this.dbName, this.version);
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
request.onsuccess = () => {
|
||||||
|
this.db = request.result;
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
request.onupgradeneeded = (event) => {
|
||||||
|
const db = event.target.result;
|
||||||
|
if (!db.objectStoreNames.contains(this.storeName)) {
|
||||||
|
const store = db.createObjectStore(this.storeName, {
|
||||||
|
keyPath: "address",
|
||||||
|
});
|
||||||
|
store.createIndex("timestamp", "timestamp", { unique: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSearchedAddress(
|
||||||
|
suiAddress,
|
||||||
|
balance,
|
||||||
|
timestamp = Date.now(),
|
||||||
|
sourceInfo = null
|
||||||
|
) {
|
||||||
|
if (!this.db) await this.init();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = this.db.transaction([this.storeName], "readwrite");
|
||||||
|
const store = transaction.objectStore(this.storeName);
|
||||||
|
const getRequest = store.get(suiAddress);
|
||||||
|
getRequest.onsuccess = () => {
|
||||||
|
const existingRecord = getRequest.result;
|
||||||
|
let finalSourceInfo = sourceInfo;
|
||||||
|
if (existingRecord && existingRecord.sourceInfo && !sourceInfo) {
|
||||||
|
finalSourceInfo = existingRecord.sourceInfo;
|
||||||
|
} else if (
|
||||||
|
existingRecord &&
|
||||||
|
existingRecord.sourceInfo &&
|
||||||
|
sourceInfo === null
|
||||||
|
) {
|
||||||
|
finalSourceInfo = existingRecord.sourceInfo;
|
||||||
|
}
|
||||||
|
const data = {
|
||||||
|
address: suiAddress,
|
||||||
|
balance,
|
||||||
|
timestamp,
|
||||||
|
formattedBalance: `${balance} SUI`,
|
||||||
|
sourceInfo: finalSourceInfo,
|
||||||
|
};
|
||||||
|
const putRequest = store.put(data);
|
||||||
|
putRequest.onsuccess = () => resolve();
|
||||||
|
putRequest.onerror = () => reject(putRequest.error);
|
||||||
|
};
|
||||||
|
getRequest.onerror = () => reject(getRequest.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSearchedAddresses() {
|
||||||
|
if (!this.db) await this.init();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = this.db.transaction([this.storeName], "readonly");
|
||||||
|
const store = transaction.objectStore(this.storeName);
|
||||||
|
const index = store.index("timestamp");
|
||||||
|
const request = index.getAll();
|
||||||
|
request.onsuccess = () => {
|
||||||
|
const results = request.result.sort(
|
||||||
|
(a, b) => b.timestamp - a.timestamp
|
||||||
|
);
|
||||||
|
resolve(results);
|
||||||
|
};
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSearchedAddress(suiAddress) {
|
||||||
|
if (!this.db) await this.init();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = this.db.transaction([this.storeName], "readwrite");
|
||||||
|
const store = transaction.objectStore(this.storeName);
|
||||||
|
const request = store.delete(suiAddress);
|
||||||
|
request.onsuccess = () => resolve();
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearAllSearchedAddresses() {
|
||||||
|
if (!this.db) await this.init();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = this.db.transaction([this.storeName], "readwrite");
|
||||||
|
const store = transaction.objectStore(this.storeName);
|
||||||
|
const request = store.clear();
|
||||||
|
request.onsuccess = () => resolve();
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user