Workflow updating files of polkadotwallet
This commit is contained in:
parent
d7070a9dea
commit
bf6d0e50e6
2
polkadotwallet/README.md
Normal file
2
polkadotwallet/README.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# polkadotwallet
|
||||||
|
FLO / BTC linked Polkadot web wallet from RanchiMall
|
||||||
2451
polkadotwallet/index.html
Normal file
2451
polkadotwallet/index.html
Normal file
File diff suppressed because it is too large
Load Diff
11473
polkadotwallet/lib.polkadot.js
Normal file
11473
polkadotwallet/lib.polkadot.js
Normal file
File diff suppressed because it is too large
Load Diff
2
polkadotwallet/polkadot-api-bundle.js
Normal file
2
polkadotwallet/polkadot-api-bundle.js
Normal file
File diff suppressed because one or more lines are too long
536
polkadotwallet/polkadotBlockchainAPI.js
Normal file
536
polkadotwallet/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
polkadotwallet/polkadotCrypto.js
Normal file
322
polkadotwallet/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
polkadotwallet/polkadotSearchDB.js
Normal file
136
polkadotwallet/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
polkadotwallet/polkadot_favicon.svg
Normal file
14
polkadotwallet/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 |
3688
polkadotwallet/style.css
Normal file
3688
polkadotwallet/style.css
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user