feat: Implement initial Stellar Wallet application with multi-blockchain address generation and recovery.

This commit is contained in:
void-57 2025-12-12 18:26:57 +05:30
parent b868a791dc
commit 4bb4d8cec7
5 changed files with 16912 additions and 0 deletions

1900
index.html Normal file

File diff suppressed because it is too large Load Diff

11473
lib.stellar.js Normal file

File diff suppressed because it is too large Load Diff

263
stellarCrypto.js Normal file
View File

@ -0,0 +1,263 @@
(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 = {}));

4
stellar_favicon.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 236.36 200">
<path fill="#000000" d="M203,26.16l-28.46,14.5-137.43,70a82.49,82.49,0,0,1-.7-10.69A81.87,81.87,0,0,1,158.2,28.6l16.29-8.3,2.43-1.24A100,100,0,0,0,18.18,100q0,3.82.29,7.61a18.19,18.19,0,0,1-9.88,17.58L0,129.57V150l25.29-12.89,0,0,8.19-4.18,8.07-4.11v0L186.43,55l16.28-8.29,33.65-17.15V9.14Z"/>
<path fill="#000000" d="M236.36,50,49.78,145,33.5,153.31,0,170.38v20.41l33.27-16.95,28.46-14.5L199.3,89.24A83.45,83.45,0,0,1,200,100,81.87,81.87,0,0,1,78.09,171.36l-1,.53-17.66,9A100,100,0,0,0,218.18,100c0-2.57-.1-5.14-.29-7.68a18.2,18.2,0,0,1,9.87-17.58l8.6-4.38Z"/>
</svg>

After

Width:  |  Height:  |  Size: 639 B

3272
style.css Normal file

File diff suppressed because it is too large Load Diff