sui wallet first commit

This commit is contained in:
void-57 2025-11-08 00:05:15 +05:30
commit 2fee2263e8
6 changed files with 17630 additions and 0 deletions

2440
index.html Normal file

File diff suppressed because it is too large Load Diff

11473
lib.sui.js Normal file

File diff suppressed because it is too large Load Diff

2934
style.css Normal file

File diff suppressed because it is too large Load Diff

461
suiBlockchainAPI.js Normal file
View File

@ -0,0 +1,461 @@
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;

217
suiCrypto.js Normal file
View 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
suiSearchDB.js Normal file
View 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);
});
}
}