Workflow updating files of stellarwallet
This commit is contained in:
parent
bc162dc331
commit
e7da9c24c8
1
stellarwallet/README.md
Normal file
1
stellarwallet/README.md
Normal file
@ -0,0 +1 @@
|
||||
# stellarwallet
|
||||
1986
stellarwallet/index.html
Normal file
1986
stellarwallet/index.html
Normal file
File diff suppressed because it is too large
Load Diff
11473
stellarwallet/lib.stellar.js
Normal file
11473
stellarwallet/lib.stellar.js
Normal file
File diff suppressed because it is too large
Load Diff
509
stellarwallet/stellarBlockchainAPI.js
Normal file
509
stellarwallet/stellarBlockchainAPI.js
Normal file
@ -0,0 +1,509 @@
|
||||
(function(GLOBAL) {
|
||||
'use strict';
|
||||
|
||||
// Stellar Horizon API endpoints
|
||||
const HORIZON_URL = 'https://horizon.stellar.org'; // Mainnet
|
||||
|
||||
const stellarAPI = {};
|
||||
let StellarSdk = null;
|
||||
let server = null;
|
||||
|
||||
// Initialize Stellar SDK when available
|
||||
stellarAPI.init = function() {
|
||||
if (typeof window !== 'undefined') {
|
||||
const sdkCandidate = window.StellarSdk || window['stellar-sdk'] || window.StellarBase;
|
||||
|
||||
if (sdkCandidate) {
|
||||
let ServerClass = null;
|
||||
if (sdkCandidate.Server) {
|
||||
ServerClass = sdkCandidate.Server;
|
||||
StellarSdk = sdkCandidate;
|
||||
} else if (sdkCandidate.Horizon && sdkCandidate.Horizon.Server) {
|
||||
ServerClass = sdkCandidate.Horizon.Server;
|
||||
StellarSdk = sdkCandidate; // Store the full SDK object
|
||||
}
|
||||
|
||||
if (ServerClass) {
|
||||
try {
|
||||
server = new ServerClass(HORIZON_URL);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating Server instance:', error);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
console.error('❌ Server class not found in StellarSdk');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
console.error('❌ StellarSdk not found on window');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
console.warn('⚠️ Window object not available');
|
||||
return false;
|
||||
};
|
||||
|
||||
stellarAPI.forceInit = function() {
|
||||
return stellarAPI.init();
|
||||
};
|
||||
|
||||
// Get account balance and info
|
||||
stellarAPI.getBalance = async function(address) {
|
||||
try {
|
||||
const response = await fetch(`${HORIZON_URL}/accounts/${address}`);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
throw new Error('Account not found. The account may not be funded yet.');
|
||||
}
|
||||
throw new Error(`Failed to fetch balance: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Find native XLM balance
|
||||
const nativeBalance = data.balances.find(b => b.asset_type === 'native');
|
||||
|
||||
return {
|
||||
address: data.account_id,
|
||||
balance: nativeBalance ? parseFloat(nativeBalance.balance) : 0,
|
||||
balanceXlm: nativeBalance ? parseFloat(nativeBalance.balance) : 0,
|
||||
sequence: data.sequence,
|
||||
subentryCount: data.subentry_count,
|
||||
numSponsoring: data.num_sponsoring || 0,
|
||||
numSponsored: data.num_sponsored || 0,
|
||||
balances: data.balances, // All balances including assets
|
||||
signers: data.signers,
|
||||
flags: data.flags,
|
||||
thresholds: data.thresholds,
|
||||
lastModifiedLedger: data.last_modified_ledger,
|
||||
// Minimum balance calculation: (2 + subentry_count) * 0.5 XLM
|
||||
minBalance: (2 + data.subentry_count) * 0.5
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Get transaction history with pagination
|
||||
stellarAPI.getTransactions = async function(address, options = {}) {
|
||||
const limit = options.limit || 10;
|
||||
const cursor = options.cursor || options.next || null;
|
||||
const order = options.order || 'desc'; // desc = newest first
|
||||
|
||||
let url = `${HORIZON_URL}/accounts/${address}/transactions?limit=${limit}&order=${order}`;
|
||||
|
||||
if (cursor) {
|
||||
url += `&cursor=${cursor}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch transactions: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Format transactions
|
||||
const transactions = await Promise.all((data._embedded.records || []).map(async tx => {
|
||||
// Get operations for this transaction to determine type and details
|
||||
const opsUrl = `${HORIZON_URL}/transactions/${tx.hash}/operations`;
|
||||
|
||||
let operations = [];
|
||||
try {
|
||||
const opsResponse = await fetch(opsUrl);
|
||||
if (opsResponse.ok) {
|
||||
const opsData = await opsResponse.json();
|
||||
operations = opsData._embedded.records || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch operations for transaction:', tx.hash, error);
|
||||
}
|
||||
|
||||
// Find payment operations
|
||||
const paymentOp = operations.find(op =>
|
||||
op.type === 'payment' || op.type === 'create_account'
|
||||
);
|
||||
|
||||
let type = 'other';
|
||||
let amount = 0;
|
||||
let amountXlm = 0;
|
||||
let receiver = null;
|
||||
let sender = tx.source_account;
|
||||
|
||||
if (paymentOp) {
|
||||
if (paymentOp.type === 'payment') {
|
||||
type = paymentOp.from === address ? 'sent' : 'received';
|
||||
amount = parseFloat(paymentOp.amount || 0);
|
||||
amountXlm = amount;
|
||||
receiver = paymentOp.to;
|
||||
sender = paymentOp.from;
|
||||
} else if (paymentOp.type === 'create_account') {
|
||||
type = paymentOp.funder === address ? 'sent' : 'received';
|
||||
amount = parseFloat(paymentOp.starting_balance || 0);
|
||||
amountXlm = amount;
|
||||
receiver = paymentOp.account;
|
||||
sender = paymentOp.funder;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse timestamp
|
||||
const timestamp = new Date(tx.created_at).getTime() / 1000;
|
||||
|
||||
return {
|
||||
id: tx.id,
|
||||
hash: tx.hash,
|
||||
ledger: tx.ledger,
|
||||
createdAt: tx.created_at,
|
||||
sourceAccount: tx.source_account,
|
||||
fee: parseInt(tx.fee_charged || tx.max_fee),
|
||||
feeXlm: parseInt(tx.fee_charged || tx.max_fee) / 10000000,
|
||||
operationCount: tx.operation_count,
|
||||
successful: tx.successful,
|
||||
// Payment details
|
||||
type: type,
|
||||
sender: sender,
|
||||
receiver: receiver,
|
||||
amount: amount,
|
||||
amountXlm: amountXlm,
|
||||
memo: tx.memo || null,
|
||||
memoType: tx.memo_type || null,
|
||||
// Compatibility fields
|
||||
roundTime: timestamp,
|
||||
confirmedRound: tx.ledger
|
||||
};
|
||||
}));
|
||||
|
||||
return {
|
||||
transactions,
|
||||
nextToken: data._embedded.records.length > 0
|
||||
? data._embedded.records[data._embedded.records.length - 1].paging_token
|
||||
: null,
|
||||
hasMore: data._embedded.records.length === limit,
|
||||
cursor: data._embedded.records.length > 0
|
||||
? data._embedded.records[data._embedded.records.length - 1].paging_token
|
||||
: null
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Get transaction parameters (needed for sending)
|
||||
stellarAPI.getTransactionParams = async function(sourceAddress) {
|
||||
try {
|
||||
// Get latest ledger info for fee stats
|
||||
const response = await fetch(`${HORIZON_URL}/fee_stats`);
|
||||
const feeStats = await response.json();
|
||||
|
||||
// Base fee in stroops (0.00001 XLM = 100 stroops)
|
||||
const baseFee = feeStats.last_ledger_base_fee || '100';
|
||||
|
||||
return {
|
||||
fee: parseInt(baseFee),
|
||||
baseFee: baseFee,
|
||||
networkPassphrase: StellarSdk ? StellarSdk.Networks.PUBLIC : 'Public Global Stellar Network ; September 2015',
|
||||
genesisId: 'stellar-mainnet',
|
||||
genesisHash: 'stellar-mainnet'
|
||||
};
|
||||
} catch (error) {
|
||||
// Fallback to default fee
|
||||
return {
|
||||
fee: 100,
|
||||
baseFee: '100',
|
||||
networkPassphrase: StellarSdk ? StellarSdk.Networks.PUBLIC : 'Public Global Stellar Network ; September 2015',
|
||||
genesisId: 'stellar-mainnet',
|
||||
genesisHash: 'stellar-mainnet'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Build and sign transaction using Stellar SDK
|
||||
stellarAPI.buildAndSignTransaction = async function(params) {
|
||||
const { sourceAddress, destinationAddress, amount, secretKey, memo } = params;
|
||||
|
||||
if (!StellarSdk || !server) {
|
||||
throw new Error('Stellar SDK not initialized. Please refresh the page.');
|
||||
}
|
||||
|
||||
try {
|
||||
// Load source account
|
||||
const sourceAccount = await server.loadAccount(sourceAddress);
|
||||
|
||||
// Check if destination account exists
|
||||
let destinationExists = true;
|
||||
try {
|
||||
await server.loadAccount(destinationAddress);
|
||||
} catch (error) {
|
||||
if (error.response && error.response.status === 404) {
|
||||
destinationExists = false;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get fee stats
|
||||
const feeStats = await server.feeStats();
|
||||
|
||||
// fee_charged.mode is typically 100 stroops (0.00001 XLM)
|
||||
const fee = feeStats.fee_charged?.mode || feeStats.last_ledger_base_fee || '100';
|
||||
|
||||
// Build transaction
|
||||
let transaction = new StellarSdk.TransactionBuilder(sourceAccount, {
|
||||
fee: fee,
|
||||
networkPassphrase: StellarSdk.Networks.PUBLIC
|
||||
});
|
||||
|
||||
// Add operation based on whether destination exists
|
||||
if (destinationExists) {
|
||||
// Payment operation
|
||||
transaction = transaction.addOperation(
|
||||
StellarSdk.Operation.payment({
|
||||
destination: destinationAddress,
|
||||
asset: StellarSdk.Asset.native(),
|
||||
amount: amount.toString()
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// Create account operation (requires minimum 1 XLM)
|
||||
if (parseFloat(amount) < 1) {
|
||||
throw new Error('Creating a new account requires a minimum of 1 XLM');
|
||||
}
|
||||
transaction = transaction.addOperation(
|
||||
StellarSdk.Operation.createAccount({
|
||||
destination: destinationAddress,
|
||||
startingBalance: amount.toString()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Add memo if provided
|
||||
if (memo) {
|
||||
transaction = transaction.addMemo(StellarSdk.Memo.text(memo));
|
||||
}
|
||||
|
||||
// Set timeout and build
|
||||
transaction = transaction.setTimeout(30).build();
|
||||
|
||||
// Sign transaction
|
||||
const keypair = StellarSdk.Keypair.fromSecret(secretKey);
|
||||
transaction.sign(keypair);
|
||||
|
||||
return {
|
||||
transaction: transaction,
|
||||
xdr: transaction.toEnvelope().toXDR('base64'),
|
||||
hash: transaction.hash().toString('hex'),
|
||||
destinationExists: destinationExists,
|
||||
fee: parseInt(fee)
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error building transaction:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Submit signed transaction
|
||||
stellarAPI.submitTransaction = async function(transactionXDR) {
|
||||
if (!StellarSdk || !server) {
|
||||
throw new Error('Stellar SDK not initialized. Please refresh the page.');
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse the XDR back to a transaction using TransactionBuilder
|
||||
const transaction = StellarSdk.TransactionBuilder.fromXDR(transactionXDR, StellarSdk.Networks.PUBLIC);
|
||||
|
||||
console.log('Submitting transaction to Stellar network...');
|
||||
|
||||
// Submit to network
|
||||
const result = await server.submitTransaction(transaction);
|
||||
|
||||
console.log('✅ Transaction submitted successfully!');
|
||||
console.log('Transaction Details:', {
|
||||
hash: result.hash,
|
||||
ledger: result.ledger,
|
||||
successful: result.successful,
|
||||
envelope_xdr: result.envelope_xdr,
|
||||
result_xdr: result.result_xdr
|
||||
});
|
||||
|
||||
return {
|
||||
hash: result.hash,
|
||||
ledger: result.ledger,
|
||||
successful: result.successful,
|
||||
txId: result.hash
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Error submitting transaction:', error);
|
||||
|
||||
// Parse Stellar error
|
||||
if (error.response && error.response.data) {
|
||||
const errorData = error.response.data;
|
||||
let errorMsg = errorData.title || 'Transaction failed';
|
||||
|
||||
if (errorData.extras && errorData.extras.result_codes) {
|
||||
const codes = errorData.extras.result_codes;
|
||||
errorMsg += ': ' + (codes.transaction || codes.operations?.join(', ') || 'Unknown error');
|
||||
}
|
||||
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Get single transaction by hash
|
||||
stellarAPI.getTransaction = async function(txHash) {
|
||||
try {
|
||||
const response = await fetch(`${HORIZON_URL}/transactions/${txHash}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Transaction not found: ${response.status}`);
|
||||
}
|
||||
|
||||
const tx = await response.json();
|
||||
|
||||
// Get operations for this transaction
|
||||
const opsUrl = `${HORIZON_URL}/transactions/${tx.hash}/operations`;
|
||||
|
||||
let operations = [];
|
||||
try {
|
||||
const opsResponse = await fetch(opsUrl);
|
||||
if (opsResponse.ok) {
|
||||
const opsData = await opsResponse.json();
|
||||
operations = opsData._embedded.records || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch operations for transaction:', tx.hash, error);
|
||||
}
|
||||
|
||||
// Find payment operations
|
||||
const paymentOp = operations.find(op =>
|
||||
op.type === 'payment' || op.type === 'create_account'
|
||||
);
|
||||
|
||||
let type = 'other';
|
||||
let amount = 0;
|
||||
let amountXlm = 0;
|
||||
let receiver = null;
|
||||
let sender = tx.source_account;
|
||||
|
||||
if (paymentOp) {
|
||||
if (paymentOp.type === 'payment') {
|
||||
type = 'payment';
|
||||
amount = parseFloat(paymentOp.amount || 0);
|
||||
amountXlm = amount;
|
||||
receiver = paymentOp.to;
|
||||
sender = paymentOp.from;
|
||||
} else if (paymentOp.type === 'create_account') {
|
||||
type = 'create_account';
|
||||
amount = parseFloat(paymentOp.starting_balance || 0);
|
||||
amountXlm = amount;
|
||||
receiver = paymentOp.account;
|
||||
sender = paymentOp.funder;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse timestamp
|
||||
const timestamp = new Date(tx.created_at).getTime() / 1000;
|
||||
|
||||
return {
|
||||
id: tx.id,
|
||||
hash: tx.hash,
|
||||
ledger: tx.ledger,
|
||||
createdAt: tx.created_at,
|
||||
sourceAccount: tx.source_account,
|
||||
fee: parseInt(tx.fee_charged || tx.max_fee),
|
||||
feeXlm: parseInt(tx.fee_charged || tx.max_fee) / 10000000,
|
||||
operationCount: tx.operation_count,
|
||||
successful: tx.successful,
|
||||
// Payment details
|
||||
type: type,
|
||||
sender: sender,
|
||||
receiver: receiver,
|
||||
amount: amount,
|
||||
amountXlm: amountXlm,
|
||||
memo: tx.memo || null,
|
||||
memoType: tx.memo_type || null,
|
||||
operations: operations,
|
||||
// Compatibility fields
|
||||
roundTime: timestamp,
|
||||
confirmedRound: tx.ledger
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Format XLM amount for display
|
||||
stellarAPI.formatXLM = function(amount) {
|
||||
return parseFloat(amount).toFixed(7);
|
||||
};
|
||||
|
||||
// Parse XLM to stroops (1 XLM = 10,000,000 stroops)
|
||||
stellarAPI.parseXLM = function(xlm) {
|
||||
return Math.floor(parseFloat(xlm) * 10000000);
|
||||
};
|
||||
|
||||
// Validate Stellar address
|
||||
stellarAPI.isValidAddress = function(address) {
|
||||
// Stellar addresses start with 'G' and are 56 characters long
|
||||
if (!address || typeof address !== 'string') return false;
|
||||
if (address.length !== 56) return false;
|
||||
if (!address.startsWith('G')) return false;
|
||||
|
||||
// Check if it's valid Base32
|
||||
const BASE32_REGEX = /^[A-Z2-7]+$/;
|
||||
return BASE32_REGEX.test(address);
|
||||
};
|
||||
|
||||
// Validate Stellar secret key
|
||||
stellarAPI.isValidSecret = function(secret) {
|
||||
// Stellar secret keys start with 'S' and are 56 characters long
|
||||
if (!secret || typeof secret !== 'string') return false;
|
||||
if (secret.length !== 56) return false;
|
||||
if (!secret.startsWith('S')) return false;
|
||||
|
||||
// Check if it's valid Base32
|
||||
const BASE32_REGEX = /^[A-Z2-7]+$/;
|
||||
return BASE32_REGEX.test(secret);
|
||||
};
|
||||
|
||||
// Check initialization status
|
||||
stellarAPI.isInitialized = function() {
|
||||
return StellarSdk !== null && server !== null;
|
||||
};
|
||||
|
||||
GLOBAL.stellarAPI = stellarAPI;
|
||||
GLOBAL.xlmAPI = stellarAPI; // Alias for compatibility
|
||||
|
||||
// Auto-initialize when SDK is available with retry logic
|
||||
if (typeof window !== 'undefined') {
|
||||
let initAttempts = 0;
|
||||
const maxAttempts = 5;
|
||||
|
||||
function tryInit() {
|
||||
initAttempts++;
|
||||
|
||||
const success = stellarAPI.init();
|
||||
|
||||
if (success) {
|
||||
} else if (initAttempts < maxAttempts) {
|
||||
const delay = initAttempts * 200;
|
||||
setTimeout(tryInit, delay);
|
||||
} else {
|
||||
console.error('❌ Failed to initialize Stellar SDK after', maxAttempts, 'attempts');
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('load', function() {
|
||||
setTimeout(tryInit, 100);
|
||||
});
|
||||
}
|
||||
|
||||
})(typeof window !== 'undefined' ? window : global);
|
||||
303
stellarwallet/stellarCrypto.js
Normal file
303
stellarwallet/stellarCrypto.js
Normal file
@ -0,0 +1,303 @@
|
||||
(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(),
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate CRC16-XModem checksum (shared function)
|
||||
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;
|
||||
}
|
||||
|
||||
// --- 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);
|
||||
|
||||
// Validate checksum
|
||||
if (decodedBytes.length < 35) {
|
||||
throw new Error('Invalid Stellar secret key: too short');
|
||||
}
|
||||
|
||||
// Extract components: [version(1)] + [seed(32)] + [checksum(2)]
|
||||
const payload = decodedBytes.slice(0, 33); // version + seed
|
||||
const providedChecksum = (decodedBytes[34] << 8) | decodedBytes[33]; // little-endian
|
||||
|
||||
// Calculate expected checksum
|
||||
const expectedChecksum = crc16XModem(payload);
|
||||
|
||||
// Verify checksum matches
|
||||
if (providedChecksum !== expectedChecksum) {
|
||||
throw new Error(`Invalid Stellar secret key: checksum mismatch (expected ${expectedChecksum.toString(16)}, got ${providedChecksum.toString(16)})`);
|
||||
}
|
||||
|
||||
// Verify version byte
|
||||
if (decodedBytes[0] !== 0x90) {
|
||||
throw new Error(`Invalid Stellar secret key: wrong version byte (expected 0x90, got 0x${decodedBytes[0].toString(16)})`);
|
||||
}
|
||||
|
||||
// Extract seed (skip version byte, take 32 bytes)
|
||||
const seed = decodedBytes.slice(1, 33);
|
||||
privKeyHex = bytesToHex(seed);
|
||||
} catch (e) {
|
||||
console.error("Invalid Stellar secret key:", e.message);
|
||||
throw new Error(`Failed to recover Stellar secret key: ${e.message}`);
|
||||
}
|
||||
} 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);
|
||||
|
||||
// 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: {}, 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]);
|
||||
|
||||
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 = {}));
|
||||
120
stellarwallet/stellarSearchDB.js
Normal file
120
stellarwallet/stellarSearchDB.js
Normal file
@ -0,0 +1,120 @@
|
||||
class SearchedAddressDB {
|
||||
constructor() {
|
||||
this.dbName = "StellarWalletDB";
|
||||
this.version = 1;
|
||||
this.storeName = "searchedAddresses";
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
async init() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, this.version);
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
resolve();
|
||||
};
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
if (!db.objectStoreNames.contains(this.storeName)) {
|
||||
const store = db.createObjectStore(this.storeName, {
|
||||
keyPath: "id",
|
||||
autoIncrement: true
|
||||
});
|
||||
store.createIndex("timestamp", "timestamp", { unique: false });
|
||||
store.createIndex("xlmAddress", "xlmAddress", { unique: false });
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async saveSearchedAddress(
|
||||
xlmAddress,
|
||||
balance,
|
||||
timestamp = Date.now(),
|
||||
sourceInfo = null
|
||||
) {
|
||||
if (!this.db) await this.init();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], "readwrite");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const index = store.index("xlmAddress");
|
||||
|
||||
// Check if address already exists
|
||||
const getRequest = index.getAll(xlmAddress);
|
||||
getRequest.onsuccess = () => {
|
||||
const existingRecords = getRequest.result;
|
||||
|
||||
if (existingRecords.length > 0) {
|
||||
// Address exists, update the existing record
|
||||
const existingRecord = existingRecords[0];
|
||||
const updatedData = {
|
||||
...existingRecord,
|
||||
balance,
|
||||
timestamp,
|
||||
formattedBalance: `${balance.toFixed(7)} XLM`,
|
||||
};
|
||||
|
||||
const putRequest = store.put(updatedData);
|
||||
putRequest.onsuccess = () => resolve();
|
||||
putRequest.onerror = () => reject(putRequest.error);
|
||||
} else {
|
||||
// Address doesn't exist, create new record
|
||||
const data = {
|
||||
xlmAddress,
|
||||
btcAddress: sourceInfo?.btcAddress || null,
|
||||
floAddress: sourceInfo?.floAddress || null,
|
||||
balance,
|
||||
timestamp,
|
||||
formattedBalance: `${balance.toFixed(7)} XLM`,
|
||||
isFromPrivateKey: !!(sourceInfo?.btcAddress || sourceInfo?.floAddress),
|
||||
};
|
||||
|
||||
const addRequest = store.add(data);
|
||||
addRequest.onsuccess = () => resolve();
|
||||
addRequest.onerror = () => reject(addRequest.error);
|
||||
}
|
||||
};
|
||||
getRequest.onerror = () => reject(getRequest.error);
|
||||
});
|
||||
}
|
||||
|
||||
async getSearchedAddresses() {
|
||||
if (!this.db) await this.init();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], "readonly");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const index = store.index("timestamp");
|
||||
const request = index.getAll();
|
||||
request.onsuccess = () => {
|
||||
const results = request.result.sort(
|
||||
(a, b) => b.timestamp - a.timestamp
|
||||
);
|
||||
resolve(results.slice(0, 10));
|
||||
};
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteSearchedAddress(id) {
|
||||
if (!this.db) await this.init();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], "readwrite");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.delete(id);
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async clearAllSearchedAddresses() {
|
||||
if (!this.db) await this.init();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], "readwrite");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.clear();
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
}
|
||||
4
stellarwallet/stellar_favicon.svg
Normal file
4
stellarwallet/stellar_favicon.svg
Normal 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 |
3381
stellarwallet/style.css
Normal file
3381
stellarwallet/style.css
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user