first commit ton wallet
This commit is contained in:
commit
50d403f4b8
525
index.html
Normal file
525
index.html
Normal file
@ -0,0 +1,525 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>TON Wallet</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: monospace;
|
||||
padding: 20px;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
button {
|
||||
margin: 5px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
pre {
|
||||
background: #f4f4f4;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
#inputKey,
|
||||
#addressInput {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
#walletSection,
|
||||
#explorerSection,
|
||||
#sendSection,
|
||||
#balances,
|
||||
#transactions {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
#sendPrivKey,
|
||||
#sendToAddress,
|
||||
#sendAmount {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin: 5px 0 10px 0;
|
||||
}
|
||||
#senderBalanceDisplay {
|
||||
background: #e8f5e8;
|
||||
border: 1px solid #28a745;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin: 10px 0 15px 0;
|
||||
font-family: monospace;
|
||||
}
|
||||
#senderBalanceDisplay p {
|
||||
margin: 5px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
#senderBalance,
|
||||
#senderAddress {
|
||||
color: #155724;
|
||||
font-weight: bold;
|
||||
}
|
||||
#sendLog {
|
||||
background: #f4f4f4;
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
margin-top: 15px;
|
||||
white-space: pre-wrap;
|
||||
font-family: monospace;
|
||||
min-height: 100px;
|
||||
}
|
||||
.tx {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.tx:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.tx small {
|
||||
color: #555;
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
}
|
||||
#pagination {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>TON Wallet Test</h1>
|
||||
|
||||
<!-- Generate and Recover -->
|
||||
<div id="walletSection">
|
||||
<h2>Generate and Recover TON Address</h2>
|
||||
<button id="generateBtn">Generate</button>
|
||||
<button id="recoverBtn">Recover</button>
|
||||
<br /><br />
|
||||
|
||||
<label for="inputKey">Enter TON/FLO/BTC private key:</label>
|
||||
<input type="text" id="inputKey" />
|
||||
|
||||
<pre id="output"></pre>
|
||||
</div>
|
||||
|
||||
<!-- Balance and History -->
|
||||
<div id="explorerSection">
|
||||
<h2>Transactions</h2>
|
||||
<input
|
||||
id="addressInput"
|
||||
placeholder="Enter TON Address (EQ... or UQ...)"
|
||||
/>
|
||||
<button id="loadWallet">Search</button>
|
||||
|
||||
<div id="balances">
|
||||
<h3>Balances</h3>
|
||||
<p><b>TON:</b> <span id="tonBalance">-</span></p>
|
||||
<p><b>USDT:</b> <span id="usdtBalance">-</span></p>
|
||||
</div>
|
||||
|
||||
<div id="transactions" style="display: none">
|
||||
<h3>Transaction History</h3>
|
||||
<p id="status">Status: idle</p>
|
||||
<div id="txList"></div>
|
||||
<div id="pagination">
|
||||
<button id="prevBtn" disabled>⬅ Previous</button>
|
||||
<button id="nextBtn" disabled>Next ➡</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Send TON Section -->
|
||||
<div id="sendSection">
|
||||
<h2>Send TON (Testnet)</h2>
|
||||
|
||||
<div>
|
||||
<label for="sendPrivKey"><b>Private Key (Hex):</b></label
|
||||
><br />
|
||||
<input id="sendPrivKey" placeholder="Enter private key hex " />
|
||||
</div>
|
||||
|
||||
<!-- Balance Display -->
|
||||
<div id="senderBalanceDisplay" style="display: none">
|
||||
<p><b>Balance:</b> <span id="senderBalance">-</span> TON</p>
|
||||
<p><b> Address:</b> <span id="senderAddress">-</span></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="sendToAddress"><b>Recipient Address:</b></label
|
||||
><br />
|
||||
<input id="sendToAddress" placeholder="EQ... or UQ..." />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="sendAmount"><b>Amount (TON):</b></label
|
||||
><br />
|
||||
<input id="sendAmount" type="number" step="0.001" placeholder="0.1" />
|
||||
</div>
|
||||
|
||||
<button id="sendTON">Send TON</button>
|
||||
|
||||
<div id="sendLog"></div>
|
||||
</div>
|
||||
|
||||
<script src="lib.toncoin.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/tweetnacl/1.0.3/nacl.min.js"></script>
|
||||
<script src="https://unpkg.com/tonweb/dist/tonweb.js"></script>
|
||||
<script src="tonCrypto.js"></script>
|
||||
<script src="tonBlockchainAPI.js"></script>
|
||||
|
||||
<script>
|
||||
const output = document.getElementById("output");
|
||||
|
||||
document.getElementById("generateBtn").onclick = async () => {
|
||||
try {
|
||||
const res = await tonCrypto.generateMultiChain();
|
||||
output.textContent = JSON.stringify(res, null, 2);
|
||||
} catch (e) {
|
||||
output.textContent = "Error: " + e.message;
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById("recoverBtn").onclick = async () => {
|
||||
const inp = document.getElementById("inputKey").value.trim();
|
||||
if (!inp) {
|
||||
output.textContent = "Please enter a private key (TON/BTC/FLO)";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await tonCrypto.recoverFromInput(inp);
|
||||
output.textContent = JSON.stringify(res, null, 2);
|
||||
} catch (e) {
|
||||
output.textContent = "Error: " + e.message;
|
||||
}
|
||||
};
|
||||
|
||||
let currentAddress = "";
|
||||
|
||||
let transactions = [];
|
||||
let beforeLt = null;
|
||||
let allFetched = false;
|
||||
let currentPage = 0;
|
||||
const pageSize = 10;
|
||||
|
||||
document
|
||||
.getElementById("loadWallet")
|
||||
.addEventListener("click", async () => {
|
||||
const addr = document.getElementById("addressInput").value.trim();
|
||||
if (!addr) return alert("Enter a valid TON address");
|
||||
await loadWallet(addr);
|
||||
});
|
||||
|
||||
async function loadWallet(address) {
|
||||
document.getElementById("tonBalance").innerText = "Loading...";
|
||||
document.getElementById("usdtBalance").innerText = "Loading...";
|
||||
|
||||
currentAddress = address;
|
||||
|
||||
beforeLt = null;
|
||||
allFetched = false;
|
||||
transactions = [];
|
||||
currentPage = 0;
|
||||
document.getElementById("transactions").style.display = "block";
|
||||
document.getElementById("txList").innerHTML =
|
||||
"<p>Loading transactions...</p>";
|
||||
|
||||
await Promise.all([
|
||||
getTonBalance(address),
|
||||
getUsdtBalance(address),
|
||||
fetchTransactions(address),
|
||||
]);
|
||||
|
||||
await renderPage();
|
||||
}
|
||||
|
||||
// Get TON balance
|
||||
async function getTonBalance(address) {
|
||||
try {
|
||||
const balance = await tonBlockchainAPI.getTonBalance(address);
|
||||
document.getElementById("tonBalance").innerText = balance.toFixed(6);
|
||||
} catch (err) {
|
||||
console.error("TON balance error:", err);
|
||||
document.getElementById("tonBalance").innerText = "0.000000";
|
||||
}
|
||||
}
|
||||
|
||||
// Get USDT balance
|
||||
async function getUsdtBalance(ownerAddress) {
|
||||
try {
|
||||
const balance = await tonBlockchainAPI.getUsdtBalance(ownerAddress);
|
||||
document.getElementById("usdtBalance").innerText = balance.toFixed(6);
|
||||
} catch (err) {
|
||||
console.error("USDT balance error:", err);
|
||||
document.getElementById("usdtBalance").innerText = "0.000000";
|
||||
}
|
||||
}
|
||||
|
||||
async function convertTob64(rawAddr) {
|
||||
return await tonBlockchainAPI.convertTob64(rawAddr);
|
||||
}
|
||||
|
||||
// Fetch transaction history
|
||||
async function fetchTransactions(address, append = false) {
|
||||
if (allFetched) return;
|
||||
|
||||
document.getElementById("status").textContent =
|
||||
"Fetching transactions...";
|
||||
|
||||
try {
|
||||
const result = await tonBlockchainAPI.fetchTransactions(address, {
|
||||
limit: 100,
|
||||
beforeLt: beforeLt,
|
||||
});
|
||||
|
||||
const newTxs = result.transactions || [];
|
||||
if (newTxs.length === 0) {
|
||||
allFetched = true;
|
||||
document.getElementById("status").textContent =
|
||||
"✅ All transactions fetched.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (append) transactions = transactions.concat(newTxs);
|
||||
else transactions = newTxs;
|
||||
|
||||
beforeLt = result.nextBeforeLt;
|
||||
allFetched = !result.hasMore;
|
||||
|
||||
document.getElementById(
|
||||
"status"
|
||||
).textContent = `Fetched total: ${transactions.length}`;
|
||||
} catch (error) {
|
||||
console.error("Error fetching transactions:", error);
|
||||
document.getElementById("status").textContent =
|
||||
"❌ Error fetching transactions";
|
||||
}
|
||||
}
|
||||
|
||||
async function renderPage() {
|
||||
const start = currentPage * pageSize;
|
||||
const end = start + pageSize;
|
||||
const pageTxs = transactions.slice(start, end);
|
||||
const txList = document.getElementById("txList");
|
||||
txList.innerHTML = "";
|
||||
|
||||
if (pageTxs.length === 0) {
|
||||
txList.innerHTML = "<p>No transactions found.</p>";
|
||||
document.getElementById("nextBtn").disabled = true;
|
||||
document.getElementById("prevBtn").disabled = currentPage === 0;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const tx of pageTxs) {
|
||||
const time = new Date(tx.utime * 1000).toLocaleString();
|
||||
const hash = tx.hash;
|
||||
const lt = tx.lt;
|
||||
const gas = tx.compute_phase?.gas_fees / 1e9 || 0;
|
||||
const success = tx.success ? "✅ Success" : "❌ Failed";
|
||||
|
||||
// --- IN MESSAGE ---
|
||||
if (tx.in_msg && tx.in_msg.source) {
|
||||
const inValue = (parseFloat(tx.in_msg.value || 0) / 1e9).toFixed(6);
|
||||
const fromRaw = tx.in_msg.source.address || "N/A";
|
||||
const toRaw = tx.in_msg.destination?.address || "N/A";
|
||||
const [from, to] = await Promise.all([
|
||||
convertTob64(fromRaw),
|
||||
convertTob64(toRaw),
|
||||
]);
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.className = "tx";
|
||||
div.innerHTML = `
|
||||
<b>${success}</b> — <b>${inValue} TON (IN)</b><br>
|
||||
<small>🕒 ${time}</small>
|
||||
<small>🔗 Hash: ${hash}</small>
|
||||
<small>🔢 LT: ${lt}</small>
|
||||
<small>📤 From: ${from}</small>
|
||||
<small>📥 To: ${to}</small>
|
||||
<small>⚙️ Gas Used: ${gas}</small>
|
||||
<small>💬 Operation: ${tx.in_msg.decoded_op_name || "—"}</small>
|
||||
`;
|
||||
txList.appendChild(div);
|
||||
}
|
||||
|
||||
// --- OUT MESSAGES ---
|
||||
if (tx.out_msgs && tx.out_msgs.length > 0) {
|
||||
for (const out of tx.out_msgs) {
|
||||
const outValue = (parseFloat(out.value || 0) / 1e9).toFixed(6);
|
||||
const fromRaw = out.source?.address || "N/A";
|
||||
const toRaw = out.destination?.address || "N/A";
|
||||
const [from, to] = await Promise.all([
|
||||
convertTob64(fromRaw),
|
||||
convertTob64(toRaw),
|
||||
]);
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.className = "tx";
|
||||
div.innerHTML = `
|
||||
<b>${success}</b> — <b>${outValue} TON (OUT)</b><br>
|
||||
<small>🕒 ${time}</small>
|
||||
<small>🔗 Hash: ${hash}</small>
|
||||
<small>🔢 LT: ${lt}</small>
|
||||
<small>📤 From: ${from}</small>
|
||||
<small>📥 To: ${to}</small>
|
||||
<small>⚙️ Gas Used: ${gas}</small>
|
||||
<small>💬 Operation: ${out.decoded_op_name || "—"}</small>
|
||||
`;
|
||||
txList.appendChild(div);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("prevBtn").disabled = currentPage === 0;
|
||||
document.getElementById("nextBtn").disabled =
|
||||
end >= transactions.length && allFetched;
|
||||
}
|
||||
|
||||
document.getElementById("nextBtn").addEventListener("click", async () => {
|
||||
const nextStart = (currentPage + 1) * pageSize;
|
||||
if (nextStart >= transactions.length && !allFetched) {
|
||||
await fetchTransactions(currentAddress, true);
|
||||
}
|
||||
if ((currentPage + 1) * pageSize < transactions.length) {
|
||||
currentPage++;
|
||||
await renderPage();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("prevBtn").addEventListener("click", async () => {
|
||||
if (currentPage > 0) {
|
||||
currentPage--;
|
||||
await renderPage();
|
||||
}
|
||||
});
|
||||
|
||||
// Send TON functionality
|
||||
|
||||
const sendLog = (msg, isHtml = false) => {
|
||||
console.log(msg);
|
||||
const logDiv = document.getElementById("sendLog");
|
||||
|
||||
if (isHtml) {
|
||||
const newLine = document.createElement("div");
|
||||
newLine.innerHTML = msg;
|
||||
logDiv.appendChild(newLine);
|
||||
} else {
|
||||
logDiv.textContent += msg + "\n";
|
||||
}
|
||||
};
|
||||
|
||||
const clearSendLog = () => {
|
||||
document.getElementById("sendLog").textContent = "";
|
||||
};
|
||||
|
||||
async function getSenderWallet(privHex) {
|
||||
return await tonBlockchainAPI.getSenderWallet(privHex);
|
||||
}
|
||||
|
||||
// Auto-check balance when private key is entered
|
||||
async function updateSenderBalance() {
|
||||
const privHex = document.getElementById("sendPrivKey").value.trim();
|
||||
const balanceDisplay = document.getElementById("senderBalanceDisplay");
|
||||
|
||||
if (!privHex || (privHex.length !== 64 && privHex.length !== 128)) {
|
||||
balanceDisplay.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { address } = await getSenderWallet(privHex);
|
||||
const addr = address.toString(true, true, true);
|
||||
|
||||
const balance = await tonBlockchainAPI.getTestnetBalance(addr);
|
||||
|
||||
document.getElementById("senderBalance").textContent =
|
||||
balance.toFixed(6);
|
||||
document.getElementById("senderAddress").textContent = addr;
|
||||
balanceDisplay.style.display = "block";
|
||||
} catch (err) {
|
||||
balanceDisplay.style.display = "none";
|
||||
console.error("Balance check error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
document
|
||||
.getElementById("sendPrivKey")
|
||||
.addEventListener("input", updateSenderBalance);
|
||||
|
||||
document.getElementById("sendTON").addEventListener("click", async () => {
|
||||
const privHex = document.getElementById("sendPrivKey").value.trim();
|
||||
const toAddress = document.getElementById("sendToAddress").value.trim();
|
||||
const amount = document.getElementById("sendAmount").value.trim();
|
||||
|
||||
clearSendLog();
|
||||
|
||||
if (!privHex || !toAddress || !amount) {
|
||||
sendLog("⚠️ Please fill all fields first!");
|
||||
return;
|
||||
}
|
||||
|
||||
sendLog(" Processing transaction...");
|
||||
|
||||
try {
|
||||
sendLog(` Sending ${amount} TON...`);
|
||||
|
||||
const { wallet, seqno, senderAddr } =
|
||||
await tonBlockchainAPI.sendTonTransaction(
|
||||
privHex,
|
||||
toAddress,
|
||||
amount
|
||||
);
|
||||
|
||||
sendLog(" Transaction Successful!");
|
||||
sendLog(` Sent: ${amount} TON`);
|
||||
sendLog(`To: ${toAddress}`);
|
||||
|
||||
sendLog("✅ Transaction sent. Waiting for confirmation...");
|
||||
|
||||
const result = await tonBlockchainAPI.waitForTransactionConfirmation(
|
||||
wallet,
|
||||
seqno,
|
||||
senderAddr
|
||||
);
|
||||
|
||||
sendLog("✅ Transaction confirmed!");
|
||||
sendLog(`📦 Transaction Hash: ${result.urlHash}`);
|
||||
sendLog(
|
||||
`🔗 View on explorer: <a href="${result.explorerUrl}" target="_blank" style="color: #007bff; text-decoration: underline;">Click here to view transaction</a>`,
|
||||
true
|
||||
);
|
||||
|
||||
setTimeout(updateSenderBalance, 2000);
|
||||
document.getElementById("sendAmount").value = "";
|
||||
} catch (err) {
|
||||
sendLog(`Send error: ${err.message || err}`);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
9993
lib.toncoin.js
Normal file
9993
lib.toncoin.js
Normal file
File diff suppressed because it is too large
Load Diff
330
tonBlockchainAPI.js
Normal file
330
tonBlockchainAPI.js
Normal file
@ -0,0 +1,330 @@
|
||||
(function (EXPORTS) {
|
||||
"use strict";
|
||||
const tonBlockchainAPI = EXPORTS;
|
||||
|
||||
const API = "https://toncenter.com/api/v2";
|
||||
const API_KEY =
|
||||
"62bbf0ea18f197520db44c23d961a4213f373c4c08bf5cb818b722b85192ca63";
|
||||
const USDT_MASTER = "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs";
|
||||
|
||||
const addrCache = new Map();
|
||||
|
||||
// TonWeb initialization
|
||||
let tonweb;
|
||||
if (typeof TonWeb !== "undefined") {
|
||||
tonweb = new TonWeb(
|
||||
new TonWeb.HttpProvider("https://testnet.toncenter.com/api/v2/jsonRPC"),
|
||||
{
|
||||
headers: {
|
||||
"X-API-Key":
|
||||
"f100216d884fe3d57c9fbbdd8bcea727f387e7d86e4105bd4b15e63979c2fc74",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get TON balance for the given address
|
||||
* @param {string} address - The TON address to check
|
||||
* @returns {Promise} Promise object that resolves with balance in TON
|
||||
*/
|
||||
tonBlockchainAPI.getTonBalance = function (address) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(`${API}/getAddressInformation?address=${address}`, {
|
||||
headers: { "X-API-Key": API_KEY },
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok)
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
const balance = (data?.result?.balance || 0) / 1e9;
|
||||
resolve(balance);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("TON balance error:", error);
|
||||
resolve(0);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get USDT jetton balance for the given address
|
||||
* @param {string} ownerAddress - The TON address to check for USDT balance
|
||||
* @returns {Promise} Promise object that resolves with USDT balance
|
||||
*/
|
||||
tonBlockchainAPI.getUsdtBalance = function (ownerAddress) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log("Getting USDT balance for:", ownerAddress);
|
||||
|
||||
fetch(`https://tonapi.io/v2/accounts/${ownerAddress}/jettons`)
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error(`TonAPI error: ${response.status}`);
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
console.log("TonAPI jettons response:", data);
|
||||
|
||||
const usdtJetton = data.balances?.find(
|
||||
(jetton) =>
|
||||
jetton.jetton?.address === USDT_MASTER ||
|
||||
jetton.jetton?.symbol === "USDT" ||
|
||||
jetton.jetton?.name?.includes("Tether")
|
||||
);
|
||||
|
||||
if (usdtJetton) {
|
||||
const balance = parseInt(usdtJetton.balance) / 1e6;
|
||||
console.log("USDT balance found:", balance);
|
||||
resolve(balance);
|
||||
} else {
|
||||
console.log("No USDT balance found");
|
||||
resolve(0);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("USDT balance error:", error);
|
||||
resolve(0);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert raw address to b64 format
|
||||
* @param {string} rawAddr - The raw address to convert
|
||||
* @returns {Promise} Promise object that resolves with user-friendly address
|
||||
*/
|
||||
tonBlockchainAPI.convertTob64 = function (rawAddr) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!rawAddr || !rawAddr.includes(":")) {
|
||||
resolve(rawAddr);
|
||||
return;
|
||||
}
|
||||
if (addrCache.has(rawAddr)) {
|
||||
resolve(addrCache.get(rawAddr));
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(
|
||||
`https://toncenter.com/api/v2/detectAddress?address=${encodeURIComponent(
|
||||
rawAddr
|
||||
)}`,
|
||||
{
|
||||
headers: { "X-API-Key": API_KEY },
|
||||
}
|
||||
)
|
||||
.then((response) => {
|
||||
if (!response.ok)
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
const friendly =
|
||||
data?.result?.bounceable?.b64url ||
|
||||
data?.result?.non_bounceable?.b64url ||
|
||||
rawAddr;
|
||||
addrCache.set(rawAddr, friendly);
|
||||
resolve(friendly);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn("Address conversion failed:", error);
|
||||
resolve(rawAddr);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch transaction history for an address
|
||||
* @param {string} address - The TON address to check
|
||||
* @param {Object} options - Optional parameters
|
||||
* @param {number} options.limit - Number of transactions to retrieve (default: 100)
|
||||
* @param {string} options.beforeLt - Last transaction LT for pagination
|
||||
* @returns {Promise} Promise object that resolves with transaction data
|
||||
*/
|
||||
tonBlockchainAPI.fetchTransactions = function (address, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const limit = options.limit || 100;
|
||||
const beforeLt = options.beforeLt || null;
|
||||
|
||||
const url = `https://tonapi.io/v2/blockchain/accounts/${address}/transactions?limit=${limit}${
|
||||
beforeLt ? "&before_lt=" + beforeLt : ""
|
||||
}`;
|
||||
|
||||
console.log(`Fetching transactions for: ${address}`);
|
||||
|
||||
fetch(url)
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error(`API Error ${response.status}`);
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
const transactions = data.transactions || [];
|
||||
resolve({
|
||||
transactions,
|
||||
hasMore: transactions.length === limit,
|
||||
nextBeforeLt:
|
||||
transactions.length > 0
|
||||
? transactions[transactions.length - 1].lt
|
||||
: null,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching transactions:", error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get testnet balance for an address
|
||||
* @param {string} address - The TON address to check on testnet
|
||||
* @returns {Promise} Promise object that resolves with balance in TON
|
||||
*/
|
||||
tonBlockchainAPI.getTestnetBalance = function (address) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(
|
||||
`https://testnet.toncenter.com/api/v2/getAddressBalance?address=${address}`
|
||||
)
|
||||
.then((response) => {
|
||||
if (!response.ok)
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
const balance = parseFloat(data.result) / 1e9;
|
||||
resolve(balance);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Balance check error:", error);
|
||||
resolve(0);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create wallet from private key
|
||||
* @param {string} privHex - Private key in hexadecimal format
|
||||
* @returns {Promise} Promise object that resolves with wallet, address, and keyPair
|
||||
*/
|
||||
tonBlockchainAPI.getSenderWallet = function (privHex) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!tonweb) {
|
||||
reject(new Error("TonWeb not initialized"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const seed = TonWeb.utils.hexToBytes(privHex.slice(0, 64));
|
||||
const keyPair = TonWeb.utils.keyPairFromSeed(seed.slice(0, 32));
|
||||
|
||||
// v4R2 wallet
|
||||
const WalletClass = tonweb.wallet.all.v4R2;
|
||||
const wallet = new WalletClass(tonweb.provider, {
|
||||
publicKey: keyPair.publicKey,
|
||||
});
|
||||
|
||||
wallet
|
||||
.getAddress()
|
||||
.then((address) => {
|
||||
resolve({ wallet, address, keyPair });
|
||||
})
|
||||
.catch(reject);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Send TON transaction on testnet
|
||||
* @param {string} privHex - Private key in hexadecimal format
|
||||
* @param {string} toAddress - Recipient's TON address
|
||||
* @param {string|number} amount - Amount to send in TON
|
||||
* @returns {Promise} Promise object that resolves with wallet, seqno, and sender address
|
||||
*/
|
||||
tonBlockchainAPI.sendTonTransaction = function (privHex, toAddress, amount) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const { wallet, address, keyPair } =
|
||||
await tonBlockchainAPI.getSenderWallet(privHex);
|
||||
const seqno = await wallet.methods.seqno().call();
|
||||
const senderAddr = address.toString(true, true, true);
|
||||
|
||||
await wallet.methods
|
||||
.transfer({
|
||||
secretKey: keyPair.secretKey,
|
||||
toAddress: toAddress,
|
||||
amount: TonWeb.utils.toNano(amount),
|
||||
seqno: seqno || 0,
|
||||
payload: null,
|
||||
sendMode: 3,
|
||||
})
|
||||
.send();
|
||||
|
||||
resolve({ wallet, seqno, senderAddr });
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Wait for transaction confirmation and get hash
|
||||
* @param {Object} wallet - The TON wallet object
|
||||
* @param {number} originalSeqno - The original sequence number before transaction
|
||||
* @param {string} senderAddr - The sender's address
|
||||
* @returns {Promise} Promise object that resolves with transaction hash and explorer URL
|
||||
*/
|
||||
tonBlockchainAPI.waitForTransactionConfirmation = function (
|
||||
wallet,
|
||||
originalSeqno,
|
||||
senderAddr
|
||||
) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
let seqAfter = originalSeqno;
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
seqAfter = await wallet.methods.seqno().call();
|
||||
if (Number(seqAfter) > Number(originalSeqno)) break;
|
||||
}
|
||||
|
||||
if (seqAfter === originalSeqno) {
|
||||
reject(
|
||||
new Error(
|
||||
"Seqno not increased — transaction might not be confirmed yet."
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait and fetch transaction hash
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
const txRes = await fetch(
|
||||
`https://testnet.toncenter.com/api/v2/getTransactions?address=${senderAddr}&limit=5`
|
||||
);
|
||||
const txData = await txRes.json();
|
||||
const txs = txData.result || [];
|
||||
|
||||
if (txs.length === 0) {
|
||||
reject(new Error("No transactions found."));
|
||||
return;
|
||||
}
|
||||
|
||||
const latestTx = txs[0];
|
||||
const hash = latestTx.transaction_id?.hash || "Unknown";
|
||||
const urlHash = hash.replace(/\+/g, "-").replace(/\//g, "_");
|
||||
|
||||
resolve({
|
||||
urlHash,
|
||||
explorerUrl: `https://testnet.tonviewer.com/transaction/${urlHash}`,
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
})(
|
||||
"object" === typeof module ? module.exports : (window.tonBlockchainAPI = {})
|
||||
);
|
||||
204
tonCrypto.js
Normal file
204
tonCrypto.js
Normal file
@ -0,0 +1,204 @@
|
||||
(function (EXPORTS) {
|
||||
"use strict";
|
||||
const tonCrypto = EXPORTS;
|
||||
|
||||
const nacl = window.nacl;
|
||||
const TonWeb = window.TonWeb;
|
||||
|
||||
// Helpers
|
||||
const generateNewID = (tonCrypto.generateNewID = function () {
|
||||
var key = new Bitcoin.ECKey(false);
|
||||
key.setCompressed(true);
|
||||
return {
|
||||
floID: key.getBitcoinAddress(),
|
||||
pubKey: key.getPubKeyHex(),
|
||||
privKey: key.getBitcoinWalletImportFormat(),
|
||||
};
|
||||
});
|
||||
|
||||
Object.defineProperties(tonCrypto, {
|
||||
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));
|
||||
},
|
||||
},
|
||||
});
|
||||
function hexToBytes(hex) {
|
||||
if (hex.startsWith("0x")) hex = hex.slice(2);
|
||||
return new Uint8Array(hex.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
|
||||
}
|
||||
function bytesToHex(bytes) {
|
||||
return Array.from(bytes)
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
function sha256Hex(hexString) {
|
||||
return Crypto.SHA256(Crypto.util.hexToBytes(hexString));
|
||||
}
|
||||
|
||||
// ---- Multi-chain (FLO, BTC, TON) ----
|
||||
tonCrypto.generateMultiChain = async function (inputWif) {
|
||||
const origBitjsPub = bitjs.pub;
|
||||
const origBitjsPriv = bitjs.priv;
|
||||
const origBitjsCompressed = bitjs.compressed;
|
||||
const origCoinJsCompressed = coinjs.compressed;
|
||||
|
||||
bitjs.compressed = true;
|
||||
coinjs.compressed = true;
|
||||
|
||||
const versions = {
|
||||
BTC: { pub: 0x00, priv: 0x80 },
|
||||
FLO: { pub: 0x23, priv: 0xa3 },
|
||||
};
|
||||
|
||||
let privKeyHex;
|
||||
let compressed = true;
|
||||
|
||||
if (typeof inputWif === "string" && inputWif.length > 0) {
|
||||
const hexOnly = /^[0-9a-fA-F]+$/.test(inputWif.trim());
|
||||
|
||||
if (hexOnly && (inputWif.length === 64 || inputWif.length === 128)) {
|
||||
// Raw hex private key input
|
||||
if (inputWif.length === 128) {
|
||||
privKeyHex = inputWif.substring(0, 64);
|
||||
} else {
|
||||
privKeyHex = inputWif;
|
||||
}
|
||||
compressed = true;
|
||||
} else {
|
||||
// WIF format input
|
||||
try {
|
||||
const decode = Bitcoin.Base58.decode(inputWif);
|
||||
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;
|
||||
} else {
|
||||
compressed = false;
|
||||
}
|
||||
|
||||
privKeyHex = Crypto.util.bytesToHex(key);
|
||||
} catch (e) {
|
||||
console.warn("Invalid WIF format, treating as seed:", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
||||
bitjs.compressed = compressed;
|
||||
coinjs.compressed = compressed;
|
||||
|
||||
// Generate public key
|
||||
const pubKey = bitjs.newPubkey(privKeyHex);
|
||||
|
||||
const result = {
|
||||
BTC: { address: "", privateKey: "" },
|
||||
FLO: { address: "", privateKey: "" },
|
||||
};
|
||||
|
||||
// For BTC
|
||||
bitjs.pub = versions.BTC.pub;
|
||||
bitjs.priv = versions.BTC.priv;
|
||||
result.BTC.address = coinjs.bech32Address(pubKey).address;
|
||||
result.BTC.privateKey = bitjs.privkey2wif(privKeyHex);
|
||||
|
||||
// For FLO
|
||||
bitjs.pub = versions.FLO.pub;
|
||||
bitjs.priv = versions.FLO.priv;
|
||||
result.FLO.address = bitjs.pubkey2address(pubKey);
|
||||
result.FLO.privateKey = bitjs.privkey2wif(privKeyHex);
|
||||
|
||||
bitjs.pub = origBitjsPub;
|
||||
bitjs.priv = origBitjsPriv;
|
||||
bitjs.compressed = origBitjsCompressed;
|
||||
coinjs.compressed = origCoinJsCompressed;
|
||||
// For TON
|
||||
let tonSeed;
|
||||
if (privKeyHex.length === 64) {
|
||||
tonSeed = Crypto.util.hexToBytes(privKeyHex);
|
||||
} else {
|
||||
const padded = privKeyHex.padEnd(64, "0").substring(0, 64);
|
||||
tonSeed = Crypto.util.hexToBytes(padded);
|
||||
}
|
||||
const kp = nacl.sign.keyPair.fromSeed(new Uint8Array(tonSeed));
|
||||
|
||||
let tonAddr;
|
||||
try {
|
||||
let WalletClass = null;
|
||||
let tonweb = null;
|
||||
|
||||
tonweb = new TonWeb();
|
||||
|
||||
if (TonWeb.Wallets.all.v4R2) {
|
||||
WalletClass = TonWeb.Wallets.all.v4R2;
|
||||
console.log("Using TonWeb.Wallets.all.v4R2");
|
||||
}
|
||||
|
||||
if (WalletClass && tonweb) {
|
||||
const wallet = new WalletClass(tonweb.provider, {
|
||||
publicKey: kp.publicKey,
|
||||
});
|
||||
const realAddr = await wallet.getAddress();
|
||||
tonAddr = realAddr.toString(true, true, false);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("TonWeb error, using fallback:", e);
|
||||
}
|
||||
|
||||
result.TON = {
|
||||
address: tonAddr,
|
||||
privateKey: bytesToHex(kp.secretKey),
|
||||
};
|
||||
return result;
|
||||
};
|
||||
|
||||
// ---- Recover ----
|
||||
tonCrypto.recoverFromInput = async function (input) {
|
||||
const trimmed = input.trim();
|
||||
return await tonCrypto.generateMultiChain(trimmed);
|
||||
};
|
||||
})("object" === typeof module ? module.exports : (window.tonCrypto = {}));
|
||||
Loading…
Reference in New Issue
Block a user