stellarwallet/stellarCrypto.js

263 lines
8.8 KiB
JavaScript

(function (EXPORTS) {
"use strict";
const stellarCrypto = EXPORTS;
// Helper functions
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('');
}
// Generate a new random key
function generateNewID() {
var key = new Bitcoin.ECKey(false);
key.setCompressed(true);
return {
floID: key.getBitcoinAddress(),
pubKey: key.getPubKeyHex(),
privKey: key.getBitcoinWalletImportFormat(),
};
}
// --- Multi-chain Generator (BTC, FLO, XLM) ---
stellarCrypto.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 Stellar secret key (starts with 'S' and is 56 chars)
if (trimmedInput.startsWith('S') && trimmedInput.length === 56) {
try {
// Decode Stellar secret key (Base32)
const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
const decoded = [];
let bits = 0;
let value = 0;
for (let i = 0; i < trimmedInput.length; i++) {
const char = trimmedInput[i];
const charValue = BASE32_ALPHABET.indexOf(char);
if (charValue === -1) throw new Error('Invalid Base32 character');
value = (value << 5) | charValue;
bits += 5;
while (bits >= 8) {
decoded.push((value >>> (bits - 8)) & 0xFF);
bits -= 8;
}
}
const decodedBytes = new Uint8Array(decoded);
// Extract seed (skip version byte 0x90, take 32 bytes, ignore checksum)
const seed = decodedBytes.slice(1, 33);
privKeyHex = bytesToHex(seed);
} catch (e) {
console.warn("Invalid Stellar secret key:", e);
// Fall through to generate new key
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);
}
} else 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);
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.warn("Invalid WIF, generating new key:", e);
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);
}
}
} 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: {}, XLM: {} };
// 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);
// XLM (Stellar)
try {
const privBytes = hexToBytes(privKeyHex.substring(0, 64));
const seed = new Uint8Array(privBytes.slice(0, 32));
// Generate Ed25519 keypair from seed
const keyPair = nacl.sign.keyPair.fromSeed(seed);
const pubKey = keyPair.publicKey;
// Stellar address encoding: version byte (0x30 for public key 'G') + public key + CRC16-XModem checksum
const versionByte = 0x30; // Results in 'G' prefix for public keys
const payload = new Uint8Array([versionByte, ...pubKey]);
// Calculate CRC16-XModem checksum
function crc16XModem(data) {
let crc = 0x0000;
for (let i = 0; i < data.length; i++) {
crc ^= data[i] << 8;
for (let j = 0; j < 8; j++) {
if (crc & 0x8000) {
crc = (crc << 1) ^ 0x1021;
} else {
crc = crc << 1;
}
}
}
return crc & 0xFFFF;
}
const checksum = crc16XModem(payload);
// Checksum is stored in little-endian format
const checksumBytes = new Uint8Array([checksum & 0xFF, (checksum >> 8) & 0xFF]);
const addressBytes = new Uint8Array([...payload, ...checksumBytes]);
// Base32 encode the address (RFC 4648)
const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
let bits = 0;
let value = 0;
let output = '';
for (let i = 0; i < addressBytes.length; i++) {
value = (value << 8) | addressBytes[i];
bits += 8;
while (bits >= 5) {
output += BASE32_ALPHABET[(value >>> (bits - 5)) & 31];
bits -= 5;
}
}
if (bits > 0) {
output += BASE32_ALPHABET[(value << (5 - bits)) & 31];
}
const xlmAddress = output;
// Stellar secret key format: version byte (0x90 for secret key 'S') + seed + CRC16
const secretVersionByte = 0x90; // Results in 'S' prefix for secret keys
const secretPayload = new Uint8Array([secretVersionByte, ...seed]);
const secretChecksum = crc16XModem(secretPayload);
const secretChecksumBytes = new Uint8Array([secretChecksum & 0xFF, (secretChecksum >> 8) & 0xFF]);
const secretKeyBytes = new Uint8Array([...secretPayload, ...secretChecksumBytes]);
// Base32 encode the secret key
bits = 0;
value = 0;
let secretOutput = '';
for (let i = 0; i < secretKeyBytes.length; i++) {
value = (value << 8) | secretKeyBytes[i];
bits += 8;
while (bits >= 5) {
secretOutput += BASE32_ALPHABET[(value >>> (bits - 5)) & 31];
bits -= 5;
}
}
if (bits > 0) {
secretOutput += BASE32_ALPHABET[(value << (5 - bits)) & 31];
}
const xlmPrivateKey = secretOutput;
result.XLM.address = xlmAddress;
result.XLM.privateKey = xlmPrivateKey;
} catch (error) {
console.error("Error generating XLM address:", error);
result.XLM.address = "Error generating address";
result.XLM.privateKey = privKeyHex;
}
bitjs.pub = origBitjsPub;
bitjs.priv = origBitjsPriv;
bitjs.compressed = origBitjsCompressed;
coinjs.compressed = origCoinJsCompressed;
return result;
};
// Sign Stellar Transaction
stellarCrypto.signXlm = async function (txBytes, xlmPrivateKey) {
const privKeyOnly = xlmPrivateKey.substring(0, 64);
const privBytes = hexToBytes(privKeyOnly);
const seed = new Uint8Array(privBytes.slice(0, 32));
const keypair = nacl.sign.keyPair.fromSeed(seed);
let txData;
if (typeof txBytes === 'string') {
txData = new Uint8Array(atob(txBytes).split('').map(c => c.charCodeAt(0)));
} else {
txData = new Uint8Array(txBytes);
}
const signature = nacl.sign.detached(txData, keypair.secretKey);
return signature;
};
})("object" === typeof module ? module.exports : (window.stellarCrypto = {}));