323 lines
9.6 KiB
JavaScript
323 lines
9.6 KiB
JavaScript
(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 = {}));
|