polkadotwallet/polkadotCrypto.js

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 = {}));