sui wallet first commit
This commit is contained in:
commit
2fee2263e8
2440
index.html
Normal file
2440
index.html
Normal file
File diff suppressed because it is too large
Load Diff
11473
lib.sui.js
Normal file
11473
lib.sui.js
Normal file
File diff suppressed because it is too large
Load Diff
461
suiBlockchainAPI.js
Normal file
461
suiBlockchainAPI.js
Normal 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
217
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
suiSearchDB.js
Normal file
105
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