feat: add initial Polkadot wallet app with multi-blockchain address generation, recovery, sending, and transaction history for DOT, BTC, and FLO
This commit is contained in:
parent
67fb3f43c3
commit
6619f3eb2d
2451
index.html
Normal file
2451
index.html
Normal file
File diff suppressed because it is too large
Load Diff
11473
lib.polkadot.js
Normal file
11473
lib.polkadot.js
Normal file
File diff suppressed because it is too large
Load Diff
2
polkadot-api-bundle.js
Normal file
2
polkadot-api-bundle.js
Normal file
File diff suppressed because one or more lines are too long
536
polkadotBlockchainAPI.js
Normal file
536
polkadotBlockchainAPI.js
Normal file
@ -0,0 +1,536 @@
|
|||||||
|
// API for Polkadot AssetHub (Subscan)
|
||||||
|
|
||||||
|
const polkadotAPI = (function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const SUBSCAN_API = "https://assethub-polkadot.api.subscan.io";
|
||||||
|
const NETWORK = "assethub-polkadot";
|
||||||
|
|
||||||
|
function normalizeAddress(address) {
|
||||||
|
if (!address) return address;
|
||||||
|
|
||||||
|
if (typeof polkadotCrypto !== "undefined" && polkadotCrypto.hexToSS58) {
|
||||||
|
return polkadotCrypto.hexToSS58(address, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: return as-is
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBalance(address) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${SUBSCAN_API}/api/scan/account/tokens`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-API-Key": "239a9db0f7174ad6a07ee6006dbb29a7", // Add API key if needed
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
address: address,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.code === 0 && data.data) {
|
||||||
|
// Find DOT balance
|
||||||
|
const dotBalance = data.data.native?.find(
|
||||||
|
(token) => token.symbol === "DOT"
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
balance: dotBalance
|
||||||
|
? parseFloat(dotBalance.balance) / Math.pow(10, dotBalance.decimals)
|
||||||
|
: 0,
|
||||||
|
address: address,
|
||||||
|
decimals: dotBalance?.decimals || 10,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(data.message || "Failed to fetch balance");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching balance:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTransactions(address, page = 0, limit = 20) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${SUBSCAN_API}/api/v2/scan/transfers`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-API-Key": "239a9db0f7174ad6a07ee6006dbb29a7",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
address: address,
|
||||||
|
row: limit,
|
||||||
|
page: page,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.code === 0 && data.data) {
|
||||||
|
const transactions = data.data.transfers || [];
|
||||||
|
|
||||||
|
return transactions.map((tx) => ({
|
||||||
|
id: tx.hash,
|
||||||
|
hash: tx.hash,
|
||||||
|
from: normalizeAddress(tx.from),
|
||||||
|
to: normalizeAddress(tx.to),
|
||||||
|
amount: parseFloat(tx.amount || 0),
|
||||||
|
amountDot: parseFloat(tx.amount || 0),
|
||||||
|
fee: parseFloat(tx.fee || 0) / Math.pow(10, 10),
|
||||||
|
feeDot: parseFloat(tx.fee || 0) / Math.pow(10, 10),
|
||||||
|
block: tx.block_num,
|
||||||
|
timestamp: tx.block_timestamp,
|
||||||
|
success: tx.success,
|
||||||
|
type: normalizeAddress(tx.from) === address ? "sent" : "received",
|
||||||
|
module: tx.module,
|
||||||
|
asset_symbol: tx.asset_symbol || "DOT",
|
||||||
|
extrinsicIndex: tx.extrinsic_index,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(data.message || "Failed to fetch transactions");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching transactions:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTransaction(hash) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${SUBSCAN_API}/api/scan/extrinsic`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-API-Key": "239a9db0f7174ad6a07ee6006dbb29a7", // Add API key if needed
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
hash: hash,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.code === 0 && data.data) {
|
||||||
|
const tx = data.data;
|
||||||
|
|
||||||
|
// Extract sender address
|
||||||
|
let from = tx.account_id || tx.account?.address || "";
|
||||||
|
|
||||||
|
// Extract destination and amount based on transaction type
|
||||||
|
let to = "";
|
||||||
|
let amount = 0;
|
||||||
|
|
||||||
|
// Check if it's a regular transfer with transfer object
|
||||||
|
if (tx.transfer) {
|
||||||
|
to = tx.transfer.to || tx.transfer.destination || "";
|
||||||
|
// The transfer.amount is already in DOT (not planck), so use it directly
|
||||||
|
amount = parseFloat(tx.transfer.amount) || 0;
|
||||||
|
}
|
||||||
|
// Check if it's a balance transfer without transfer object (data in params)
|
||||||
|
else if (
|
||||||
|
tx.params &&
|
||||||
|
Array.isArray(tx.params) &&
|
||||||
|
(tx.call_module === "balances" || tx.module === "balances")
|
||||||
|
) {
|
||||||
|
// Find dest parameter
|
||||||
|
const destParam = tx.params.find((p) => p.name === "dest");
|
||||||
|
if (destParam && destParam.value) {
|
||||||
|
// Try to get SS58 address first, fallback to hex Id
|
||||||
|
to = destParam.value.address || destParam.value.Id || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find value parameter
|
||||||
|
const valueParam = tx.params.find((p) => p.name === "value");
|
||||||
|
if (valueParam && valueParam.value) {
|
||||||
|
// Convert from planck to DOT (1 DOT = 10^10 planck)
|
||||||
|
amount = parseFloat(valueParam.value) / Math.pow(10, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check if it's an XCM transfer
|
||||||
|
else if (tx.params && Array.isArray(tx.params)) {
|
||||||
|
// Find beneficiary parameter
|
||||||
|
const beneficiaryParam = tx.params.find(
|
||||||
|
(p) => p.name === "beneficiary"
|
||||||
|
);
|
||||||
|
if (beneficiaryParam && beneficiaryParam.value) {
|
||||||
|
try {
|
||||||
|
// Extract AccountId32 from nested structure
|
||||||
|
const v3 = beneficiaryParam.value.V3 || beneficiaryParam.value.V4;
|
||||||
|
|
||||||
|
if (v3 && v3.interior) {
|
||||||
|
// Handle X1 as object or array
|
||||||
|
let accountId32Data = null;
|
||||||
|
|
||||||
|
if (v3.interior.X1) {
|
||||||
|
if (v3.interior.X1.AccountId32) {
|
||||||
|
accountId32Data = v3.interior.X1.AccountId32;
|
||||||
|
} else if (
|
||||||
|
Array.isArray(v3.interior.X1) &&
|
||||||
|
v3.interior.X1[0]?.AccountId32
|
||||||
|
) {
|
||||||
|
accountId32Data = v3.interior.X1[0].AccountId32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accountId32Data && accountId32Data.id) {
|
||||||
|
to = accountId32Data.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to extract beneficiary:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find assets/amount parameter
|
||||||
|
const assetsParam = tx.params.find(
|
||||||
|
(p) => p.name === "assets" || p.name === "value"
|
||||||
|
);
|
||||||
|
if (assetsParam && assetsParam.value) {
|
||||||
|
try {
|
||||||
|
const v3 = assetsParam.value.V3 || assetsParam.value.V4;
|
||||||
|
|
||||||
|
if (v3 && Array.isArray(v3) && v3.length > 0) {
|
||||||
|
const asset = v3[0];
|
||||||
|
const fungible = asset.fun?.Fungible || asset.amount?.Fungible;
|
||||||
|
|
||||||
|
if (fungible) {
|
||||||
|
amount = parseFloat(fungible) / Math.pow(10, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to extract amount:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: If still no data, check events for Transfer
|
||||||
|
if (!to && !amount && tx.event && Array.isArray(tx.event)) {
|
||||||
|
const transferEvent = tx.event.find(
|
||||||
|
(e) => e.module_id === "balances" && e.event_id === "Transfer"
|
||||||
|
);
|
||||||
|
if (transferEvent && transferEvent.params) {
|
||||||
|
try {
|
||||||
|
const eventParams = JSON.parse(transferEvent.params);
|
||||||
|
|
||||||
|
// Find to and amount from event params
|
||||||
|
const toParam = eventParams.find((p) => p.name === "to");
|
||||||
|
const amountParam = eventParams.find((p) => p.name === "amount");
|
||||||
|
|
||||||
|
if (toParam && toParam.value) {
|
||||||
|
to = toParam.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amountParam && amountParam.value) {
|
||||||
|
amount = parseFloat(amountParam.value) / Math.pow(10, 10);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse event params:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize addresses (keep SS58 as-is, hex addresses remain valid for AssetHub)
|
||||||
|
from = normalizeAddress(from);
|
||||||
|
to = normalizeAddress(to);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: tx.extrinsic_hash,
|
||||||
|
hash: tx.extrinsic_hash,
|
||||||
|
from: from,
|
||||||
|
to: to,
|
||||||
|
amount: amount,
|
||||||
|
amountDot: amount,
|
||||||
|
fee: parseFloat(tx.fee || 0) / Math.pow(10, 10),
|
||||||
|
feeDot: parseFloat(tx.fee || 0) / Math.pow(10, 10),
|
||||||
|
block: tx.block_num,
|
||||||
|
blockNum: tx.block_num,
|
||||||
|
timestamp: tx.block_timestamp,
|
||||||
|
success: tx.success,
|
||||||
|
module: tx.call_module,
|
||||||
|
method: tx.call_module_function,
|
||||||
|
signature: tx.signature,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(data.message || "Transaction not found");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching transaction:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build and sign transaction using direct crypto (no full API needed)
|
||||||
|
async function buildAndSignTransaction(txParams) {
|
||||||
|
const { sourceAddress, destinationAddress, amount, privateKeyHex, memo } =
|
||||||
|
txParams;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Wait for crypto to be ready
|
||||||
|
const { cryptoWaitReady, sr25519PairFromSeed, sr25519Sign } =
|
||||||
|
window.polkadotUtilCrypto || {};
|
||||||
|
if (!cryptoWaitReady) {
|
||||||
|
throw new Error(
|
||||||
|
"Polkadot crypto utilities not loaded. Please refresh the page."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await cryptoWaitReady();
|
||||||
|
|
||||||
|
// Convert hex private key to seed (first 32 bytes)
|
||||||
|
const privKeyOnly = privateKeyHex.substring(0, 64);
|
||||||
|
const { hexToU8a, u8aToHex } = window.polkadotUtil || {};
|
||||||
|
if (!hexToU8a) {
|
||||||
|
throw new Error(
|
||||||
|
"Polkadot utilities not loaded. Please refresh the page."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const seed = hexToU8a("0x" + privKeyOnly).slice(0, 32);
|
||||||
|
|
||||||
|
// Create keypair from seed using Sr25519
|
||||||
|
const keypair = sr25519PairFromSeed(seed);
|
||||||
|
|
||||||
|
// Get address from public key
|
||||||
|
const { encodeAddress } = window.polkadotUtilCrypto;
|
||||||
|
const address = encodeAddress(keypair.publicKey, 0); // 0 = Polkadot prefix
|
||||||
|
|
||||||
|
// Convert amount to planck (1 DOT = 10^10 planck)
|
||||||
|
const amountInPlanck = Math.floor(parseFloat(amount) * Math.pow(10, 10));
|
||||||
|
|
||||||
|
// Estimated fee
|
||||||
|
const estimatedFee = 0.0165;
|
||||||
|
|
||||||
|
// Return transaction data for RPC submission
|
||||||
|
return {
|
||||||
|
keypair: keypair,
|
||||||
|
destinationAddress: destinationAddress,
|
||||||
|
amountInPlanck: amountInPlanck,
|
||||||
|
fee: estimatedFee,
|
||||||
|
feeDot: estimatedFee,
|
||||||
|
sourceAddress: sourceAddress,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error building transaction:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit transaction using Polkadot.js API
|
||||||
|
async function submitTransaction(txData) {
|
||||||
|
try {
|
||||||
|
const { keypair, destinationAddress, amountInPlanck, sourceAddress } =
|
||||||
|
txData;
|
||||||
|
|
||||||
|
// Check if API is available
|
||||||
|
if (
|
||||||
|
!window.polkadotApi ||
|
||||||
|
!window.polkadotApi.ApiPromise ||
|
||||||
|
!window.polkadotApi.WsProvider
|
||||||
|
) {
|
||||||
|
throw new Error("Polkadot API not loaded! Please refresh the page.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ApiPromise, WsProvider } = window.polkadotApi;
|
||||||
|
const rpcUrl = "wss://polkadot-asset-hub-rpc.polkadot.io";
|
||||||
|
|
||||||
|
// Create API instance
|
||||||
|
const provider = new WsProvider(rpcUrl);
|
||||||
|
const api = await ApiPromise.create({ provider });
|
||||||
|
|
||||||
|
// Get account nonce
|
||||||
|
const nonce = await api.rpc.system.accountNextIndex(sourceAddress);
|
||||||
|
|
||||||
|
const transfer = api.tx.balances.transferAllowDeath(
|
||||||
|
destinationAddress,
|
||||||
|
amountInPlanck
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a Keyring and add our keypair
|
||||||
|
// The Polkadot API needs a proper KeyringPair interface
|
||||||
|
// Our keypair from sr25519PairFromSeed has the right structure, but we need to add the sign method
|
||||||
|
const { u8aToHex } = window.polkadotUtil;
|
||||||
|
const signerPair = {
|
||||||
|
address: keypair.address,
|
||||||
|
addressRaw: keypair.publicKey,
|
||||||
|
publicKey: keypair.publicKey,
|
||||||
|
sign: (data) => {
|
||||||
|
const { sr25519Sign } = window.polkadotUtilCrypto;
|
||||||
|
const signature = sr25519Sign(data, keypair);
|
||||||
|
// Return signature with proper format for Sr25519: { sr25519: Uint8Array }
|
||||||
|
return { sr25519: signature };
|
||||||
|
},
|
||||||
|
type: "sr25519",
|
||||||
|
unlock: () => {}, // Required but unused for our case
|
||||||
|
lock: () => {}, // Required but unused for our case
|
||||||
|
isLocked: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let unsub;
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
if (unsub) unsub();
|
||||||
|
api.disconnect();
|
||||||
|
reject(new Error("Transaction timeout"));
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
transfer
|
||||||
|
.signAndSend(signerPair, { nonce }, (result) => {
|
||||||
|
if (result.status.isFinalized) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
|
||||||
|
// Check for errors
|
||||||
|
const failed = result.events.find(({ event }) =>
|
||||||
|
api.events.system.ExtrinsicFailed.is(event)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (failed) {
|
||||||
|
const [dispatchError] = failed.event.data;
|
||||||
|
let errorMessage = "Transaction failed";
|
||||||
|
|
||||||
|
if (dispatchError.isModule) {
|
||||||
|
try {
|
||||||
|
const decoded = api.registry.findMetaError(
|
||||||
|
dispatchError.asModule
|
||||||
|
);
|
||||||
|
errorMessage = `${decoded.section}.${
|
||||||
|
decoded.name
|
||||||
|
}: ${decoded.docs.join(" ")}`;
|
||||||
|
} catch (e) {
|
||||||
|
errorMessage = `Module error: ${dispatchError.asModule.toHuman()}`;
|
||||||
|
}
|
||||||
|
} else if (dispatchError.isToken) {
|
||||||
|
errorMessage = `Token error: ${dispatchError.asToken.toString()}`;
|
||||||
|
} else if (dispatchError.isArithmetic) {
|
||||||
|
errorMessage = `Arithmetic error: ${dispatchError.asArithmetic.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`❌ Transaction failed: ${errorMessage}`);
|
||||||
|
if (unsub) unsub();
|
||||||
|
api.disconnect();
|
||||||
|
reject(new Error(errorMessage));
|
||||||
|
} else {
|
||||||
|
const txHash = transfer.hash.toHex();
|
||||||
|
console.log(`✅ Transaction successful! Hash: ${txHash}`);
|
||||||
|
|
||||||
|
if (unsub) unsub();
|
||||||
|
api.disconnect();
|
||||||
|
resolve({
|
||||||
|
hash: txHash,
|
||||||
|
success: true,
|
||||||
|
block: result.status.asFinalized.toHex(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((unsubscribe) => {
|
||||||
|
unsub = unsubscribe;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
console.error(`❌ Signing/sending error: ${error.message}`);
|
||||||
|
api.disconnect();
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Error submitting transaction:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estimate transaction fee using Polkadot API paymentInfo
|
||||||
|
async function estimateFee(sourceAddress, destinationAddress, amount) {
|
||||||
|
try {
|
||||||
|
// Check if API is available
|
||||||
|
if (
|
||||||
|
!window.polkadotApi ||
|
||||||
|
!window.polkadotApi.ApiPromise ||
|
||||||
|
!window.polkadotApi.WsProvider
|
||||||
|
) {
|
||||||
|
console.warn("Polkadot API not loaded, using default fee estimate");
|
||||||
|
return {
|
||||||
|
fee: 0.0165,
|
||||||
|
feeDot: 0.0165,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ApiPromise, WsProvider } = window.polkadotApi;
|
||||||
|
const rpcUrl = "wss://polkadot-asset-hub-rpc.polkadot.io";
|
||||||
|
|
||||||
|
// Create API instance
|
||||||
|
const provider = new WsProvider(rpcUrl);
|
||||||
|
const api = await ApiPromise.create({ provider });
|
||||||
|
|
||||||
|
// Convert amount to planck
|
||||||
|
const amountInPlanck = Math.floor(parseFloat(amount) * Math.pow(10, 10));
|
||||||
|
|
||||||
|
// Create the transfer transaction
|
||||||
|
const transfer = api.tx.balances.transferAllowDeath(
|
||||||
|
destinationAddress,
|
||||||
|
amountInPlanck
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get payment info (accurate fee estimation)
|
||||||
|
const paymentInfo = await transfer.paymentInfo(sourceAddress);
|
||||||
|
|
||||||
|
// Disconnect after getting fee
|
||||||
|
await api.disconnect();
|
||||||
|
|
||||||
|
// Convert fee from planck to DOT
|
||||||
|
const fee =
|
||||||
|
parseFloat(paymentInfo.partialFee.toString()) / Math.pow(10, 10);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fee: fee,
|
||||||
|
feeDot: fee,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error estimating fee:", error);
|
||||||
|
// Return typical DOT transfer fee if estimation fails
|
||||||
|
return {
|
||||||
|
fee: 0.0165,
|
||||||
|
feeDot: 0.0165,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if account is active (has balance)
|
||||||
|
async function checkAccountActive(address) {
|
||||||
|
try {
|
||||||
|
const balanceData = await getBalance(address);
|
||||||
|
return {
|
||||||
|
isActive: balanceData.balance > 0,
|
||||||
|
balance: balanceData.balance,
|
||||||
|
minimumRequired: 0.01, // Minimum to activate new account
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking account status:", error);
|
||||||
|
// If we can't check, assume account needs activation
|
||||||
|
return {
|
||||||
|
isActive: false,
|
||||||
|
balance: 0,
|
||||||
|
minimumRequired: 0.01,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API
|
||||||
|
return {
|
||||||
|
getBalance,
|
||||||
|
getTransactions,
|
||||||
|
getTransaction,
|
||||||
|
buildAndSignTransaction,
|
||||||
|
submitTransaction,
|
||||||
|
estimateFee,
|
||||||
|
checkAccountActive,
|
||||||
|
SUBSCAN_API,
|
||||||
|
NETWORK,
|
||||||
|
};
|
||||||
|
})();
|
||||||
322
polkadotCrypto.js
Normal file
322
polkadotCrypto.js
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
(function (EXPORTS) {
|
||||||
|
"use strict";
|
||||||
|
const polkadotCrypto = EXPORTS;
|
||||||
|
|
||||||
|
function hexToBytes(hex) {
|
||||||
|
const bytes = [];
|
||||||
|
for (let i = 0; i < hex.length; i += 2) {
|
||||||
|
bytes.push(parseInt(hex.substr(i, 2), 16));
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bytesToHex(bytes) {
|
||||||
|
return Array.from(bytes)
|
||||||
|
.map((b) => b.toString(16).padStart(2, "0"))
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateNewID() {
|
||||||
|
var key = new Bitcoin.ECKey(false);
|
||||||
|
key.setCompressed(true);
|
||||||
|
return {
|
||||||
|
floID: key.getBitcoinAddress(),
|
||||||
|
pubKey: key.getPubKeyHex(),
|
||||||
|
privKey: key.getBitcoinWalletImportFormat(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const BASE58_ALPHABET =
|
||||||
|
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||||
|
|
||||||
|
function base58Encode(bytes) {
|
||||||
|
const digits = [0];
|
||||||
|
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
let carry = bytes[i];
|
||||||
|
for (let j = 0; j < digits.length; j++) {
|
||||||
|
carry += digits[j] << 8;
|
||||||
|
digits[j] = carry % 58;
|
||||||
|
carry = (carry / 58) | 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (carry > 0) {
|
||||||
|
digits.push(carry % 58);
|
||||||
|
carry = (carry / 58) | 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add leading zeros
|
||||||
|
for (let i = 0; i < bytes.length && bytes[i] === 0; i++) {
|
||||||
|
digits.push(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to string
|
||||||
|
return digits
|
||||||
|
.reverse()
|
||||||
|
.map((d) => BASE58_ALPHABET[d])
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function base58Decode(str) {
|
||||||
|
const bytes = [0];
|
||||||
|
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const value = BASE58_ALPHABET.indexOf(str[i]);
|
||||||
|
if (value === -1) {
|
||||||
|
throw new Error(`Invalid Base58 character: ${str[i]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let carry = value;
|
||||||
|
for (let j = 0; j < bytes.length; j++) {
|
||||||
|
carry += bytes[j] * 58;
|
||||||
|
bytes[j] = carry & 0xff;
|
||||||
|
carry >>= 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (carry > 0) {
|
||||||
|
bytes.push(carry & 0xff);
|
||||||
|
carry >>= 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add leading zeros
|
||||||
|
for (let i = 0; i < str.length && str[i] === "1"; i++) {
|
||||||
|
bytes.push(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Uint8Array(bytes.reverse());
|
||||||
|
}
|
||||||
|
|
||||||
|
function blake2bHash(data, outlen = 64) {
|
||||||
|
if (typeof blakejs !== "undefined" && blakejs.blake2b) {
|
||||||
|
return blakejs.blake2b(data, null, outlen);
|
||||||
|
}
|
||||||
|
throw new Error("Blake2b library not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPolkadotAddress(publicKey, ss58Prefix = 0) {
|
||||||
|
// SS58 format: [prefix] + [public_key] + [checksum]
|
||||||
|
const prefix = new Uint8Array([ss58Prefix]);
|
||||||
|
const payload = new Uint8Array([...prefix, ...publicKey]);
|
||||||
|
|
||||||
|
const checksumInput = new Uint8Array([
|
||||||
|
...new TextEncoder().encode("SS58PRE"),
|
||||||
|
...payload,
|
||||||
|
]);
|
||||||
|
const hash = blake2bHash(checksumInput, 64);
|
||||||
|
const checksum = hash.slice(0, 2);
|
||||||
|
|
||||||
|
// Combine all parts
|
||||||
|
const addressBytes = new Uint8Array([...payload, ...checksum]);
|
||||||
|
|
||||||
|
return base58Encode(addressBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Multi-chain Generator (BTC, FLO, DOT) ---
|
||||||
|
polkadotCrypto.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;
|
||||||
|
|
||||||
|
if (typeof inputWif === "string" && inputWif.trim().length > 0) {
|
||||||
|
const trimmedInput = inputWif.trim();
|
||||||
|
const hexOnly = /^[0-9a-fA-F]+$/.test(trimmedInput);
|
||||||
|
|
||||||
|
// Check if it's a Polkadot seed phrase or private key
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Validate WIF checksum
|
||||||
|
if (decode.length < 37) {
|
||||||
|
throw new Error("Invalid WIF key: too short");
|
||||||
|
}
|
||||||
|
|
||||||
|
// WIF format: [version(1)] + [private_key(32)] + [compression_flag(0-1)] + [checksum(4)]
|
||||||
|
const payload = decode.slice(0, decode.length - 4);
|
||||||
|
const providedChecksum = decode.slice(decode.length - 4);
|
||||||
|
|
||||||
|
// Calculate expected checksum using double SHA256
|
||||||
|
const hash1 = Crypto.SHA256(payload, { asBytes: true });
|
||||||
|
const hash2 = Crypto.SHA256(hash1, { asBytes: true });
|
||||||
|
const expectedChecksum = hash2.slice(0, 4);
|
||||||
|
|
||||||
|
// Verify checksum matches
|
||||||
|
let checksumMatch = true;
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
if (providedChecksum[i] !== expectedChecksum[i]) {
|
||||||
|
checksumMatch = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checksumMatch) {
|
||||||
|
const providedHex = providedChecksum
|
||||||
|
.map((b) => b.toString(16).padStart(2, "0"))
|
||||||
|
.join("");
|
||||||
|
const expectedHex = expectedChecksum
|
||||||
|
.map((b) => b.toString(16).padStart(2, "0"))
|
||||||
|
.join("");
|
||||||
|
throw new Error(
|
||||||
|
`Invalid WIF key: checksum mismatch (expected ${expectedHex}, got ${providedHex})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = bytesToHex(key);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Invalid WIF key:", e.message);
|
||||||
|
throw new Error(`Failed to recover from WIF key: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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 = bytesToHex(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Derive addresses for each chain ---
|
||||||
|
const result = { BTC: {}, FLO: {}, DOT: {} };
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// DOT (Polkadot) - Using Sr25519 with Polkadot.js
|
||||||
|
try {
|
||||||
|
const privBytes = hexToBytes(privKeyHex.substring(0, 64));
|
||||||
|
const seed = new Uint8Array(privBytes.slice(0, 32));
|
||||||
|
|
||||||
|
// Wait for Polkadot crypto to be ready
|
||||||
|
await polkadotUtilCrypto.cryptoWaitReady();
|
||||||
|
|
||||||
|
// Create keypair from seed using Sr25519 (Schnorrkel)
|
||||||
|
const keyPair = polkadotUtilCrypto.sr25519PairFromSeed(seed);
|
||||||
|
|
||||||
|
// Encode address in SS58 format with Polkadot prefix (0)
|
||||||
|
const dotAddress = polkadotUtilCrypto.encodeAddress(keyPair.publicKey, 0);
|
||||||
|
|
||||||
|
// Store private key as hex
|
||||||
|
const dotPrivateKey = bytesToHex(seed);
|
||||||
|
|
||||||
|
result.DOT.address = dotAddress;
|
||||||
|
result.DOT.privateKey = dotPrivateKey;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error generating DOT address:", error);
|
||||||
|
result.DOT.address = "Error generating address";
|
||||||
|
result.DOT.privateKey = privKeyHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
bitjs.pub = origBitjsPub;
|
||||||
|
bitjs.priv = origBitjsPriv;
|
||||||
|
bitjs.compressed = origBitjsCompressed;
|
||||||
|
coinjs.compressed = origCoinJsCompressed;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sign Polkadot Transaction using Sr25519
|
||||||
|
polkadotCrypto.signDot = async function (txBytes, dotPrivateKey) {
|
||||||
|
const privKeyOnly = dotPrivateKey.substring(0, 64);
|
||||||
|
const privBytes = hexToBytes(privKeyOnly);
|
||||||
|
const seed = new Uint8Array(privBytes.slice(0, 32));
|
||||||
|
|
||||||
|
// Wait for Polkadot crypto to be ready
|
||||||
|
await polkadotUtilCrypto.cryptoWaitReady();
|
||||||
|
|
||||||
|
// Create keypair from seed using Sr25519
|
||||||
|
const keypair = polkadotUtilCrypto.sr25519PairFromSeed(seed);
|
||||||
|
|
||||||
|
let txData;
|
||||||
|
if (typeof txBytes === "string") {
|
||||||
|
txData = new Uint8Array(
|
||||||
|
atob(txBytes)
|
||||||
|
.split("")
|
||||||
|
.map((c) => c.charCodeAt(0))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
txData = new Uint8Array(txBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign using Sr25519
|
||||||
|
const signature = polkadotUtilCrypto.sr25519Sign(txData, keypair);
|
||||||
|
|
||||||
|
return signature;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export helper function for converting hex addresses to SS58
|
||||||
|
polkadotCrypto.hexToSS58 = function (hexAddress, prefix = 0) {
|
||||||
|
try {
|
||||||
|
if (!hexAddress) return hexAddress;
|
||||||
|
|
||||||
|
// Remove 0x prefix if present
|
||||||
|
const cleanHex = hexAddress.startsWith("0x")
|
||||||
|
? hexAddress.slice(2)
|
||||||
|
: hexAddress;
|
||||||
|
|
||||||
|
// If it's already SS58 format (not hex), return as-is
|
||||||
|
if (!/^[0-9a-fA-F]+$/.test(cleanHex)) {
|
||||||
|
return hexAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert hex to bytes
|
||||||
|
const bytes = [];
|
||||||
|
for (let i = 0; i < cleanHex.length; i += 2) {
|
||||||
|
bytes.push(parseInt(cleanHex.substr(i, 2), 16));
|
||||||
|
}
|
||||||
|
const publicKey = new Uint8Array(bytes);
|
||||||
|
|
||||||
|
// Only convert if it's exactly 32 bytes (valid public key)
|
||||||
|
if (publicKey.length === 32) {
|
||||||
|
return createPolkadotAddress(publicKey, prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return original if not a valid 32-byte key
|
||||||
|
return hexAddress;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to convert hex to SS58:", error);
|
||||||
|
return hexAddress;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})("object" === typeof module ? module.exports : (window.polkadotCrypto = {}));
|
||||||
136
polkadotSearchDB.js
Normal file
136
polkadotSearchDB.js
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
// Search database using local storage
|
||||||
|
|
||||||
|
class PolkadotSearchDB {
|
||||||
|
constructor() {
|
||||||
|
this.dbName = "PolkadotWalletDB";
|
||||||
|
this.storeName = "recentSearches";
|
||||||
|
this.maxSearches = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSearch(address, balance, sourceInfo = null) {
|
||||||
|
try {
|
||||||
|
const searches = this.getSearches();
|
||||||
|
|
||||||
|
// Check if address already exists
|
||||||
|
const existingIndex = searches.findIndex((s) => s.address === address);
|
||||||
|
const existing = existingIndex !== -1 ? searches[existingIndex] : null;
|
||||||
|
|
||||||
|
const searchData = {
|
||||||
|
address: address,
|
||||||
|
balance: balance || 0,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
btcAddress: sourceInfo?.btcAddress || existing?.btcAddress || null,
|
||||||
|
floAddress: sourceInfo?.floAddress || existing?.floAddress || null,
|
||||||
|
isFromPrivateKey: !!(
|
||||||
|
sourceInfo?.btcAddress ||
|
||||||
|
sourceInfo?.floAddress ||
|
||||||
|
existing?.btcAddress ||
|
||||||
|
existing?.floAddress
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
searches[existingIndex] = searchData;
|
||||||
|
} else {
|
||||||
|
// Add new search at the beginning
|
||||||
|
searches.unshift(searchData);
|
||||||
|
|
||||||
|
// Keep only the most recent searches
|
||||||
|
if (searches.length > this.maxSearches) {
|
||||||
|
searches.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(this.storeName, JSON.stringify(searches));
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error saving search:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSearches() {
|
||||||
|
try {
|
||||||
|
const data = localStorage.getItem(this.storeName);
|
||||||
|
return data ? JSON.parse(data) : [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting searches:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSearch(address) {
|
||||||
|
try {
|
||||||
|
const searches = this.getSearches();
|
||||||
|
return searches.find((s) => s.address === address) || null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting search:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSearch(address) {
|
||||||
|
try {
|
||||||
|
const searches = this.getSearches();
|
||||||
|
const filtered = searches.filter((s) => s.address !== address);
|
||||||
|
localStorage.setItem(this.storeName, JSON.stringify(filtered));
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting search:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAll() {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(this.storeName);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error clearing searches:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRecentSearches(limit = null) {
|
||||||
|
try {
|
||||||
|
let searches = this.getSearches();
|
||||||
|
|
||||||
|
// Sort by timestamp descending (newest first)
|
||||||
|
searches.sort((a, b) => b.timestamp - a.timestamp);
|
||||||
|
|
||||||
|
// Apply limit if specified
|
||||||
|
if (limit && limit > 0) {
|
||||||
|
searches = searches.slice(0, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return searches;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting recent searches:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBalance(address, newBalance) {
|
||||||
|
try {
|
||||||
|
const searches = this.getSearches();
|
||||||
|
const index = searches.findIndex((s) => s.address === address);
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
searches[index].balance = newBalance;
|
||||||
|
searches[index].timestamp = Date.now();
|
||||||
|
searches[index].date = new Date().toISOString();
|
||||||
|
localStorage.setItem(this.storeName, JSON.stringify(searches));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating balance:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a global instance if SearchedAddressDB is referenced anywhere
|
||||||
|
const SearchedAddressDB = PolkadotSearchDB;
|
||||||
14
polkadot_favicon.svg
Normal file
14
polkadot_favicon.svg
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 1326.1 1410.3" style="enable-background:new 0 0 1326.1 1410.3;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#E6007A;}
|
||||||
|
</style>
|
||||||
|
<ellipse class="st0" cx="663" cy="147.9" rx="254.3" ry="147.9"/>
|
||||||
|
<ellipse class="st0" cx="663" cy="1262.3" rx="254.3" ry="147.9"/>
|
||||||
|
<ellipse transform="matrix(0.5 -0.866 0.866 0.5 -279.1512 369.5916)" class="st0" cx="180.5" cy="426.5" rx="254.3" ry="148"/>
|
||||||
|
<ellipse transform="matrix(0.5 -0.866 0.866 0.5 -279.1552 1483.9517)" class="st0" cx="1145.6" cy="983.7" rx="254.3" ry="147.9"/>
|
||||||
|
<ellipse transform="matrix(0.866 -0.5 0.5 0.866 -467.6798 222.044)" class="st0" cx="180.5" cy="983.7" rx="148" ry="254.3"/>
|
||||||
|
<ellipse transform="matrix(0.866 -0.5 0.5 0.866 -59.8007 629.9254)" class="st0" cx="1145.6" cy="426.6" rx="147.9" ry="254.3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
Loading…
Reference in New Issue
Block a user