Merge pull request #1 from void-57/main
RanchiMall Stellar Wallet - Complete Feature Set
This commit is contained in:
commit
386dfdc848
1981
index.html
Normal file
1981
index.html
Normal file
File diff suppressed because it is too large
Load Diff
11473
lib.stellar.js
Normal file
11473
lib.stellar.js
Normal file
File diff suppressed because it is too large
Load Diff
496
stellarBlockchainAPI.js
Normal file
496
stellarBlockchainAPI.js
Normal file
@ -0,0 +1,496 @@
|
||||
(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();
|
||||
const fee = feeStats.max_fee.mode || (StellarSdk.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
|
||||
const transaction = new StellarSdk.Transaction(transactionXDR, StellarSdk.Networks.PUBLIC);
|
||||
|
||||
// Submit to network
|
||||
const result = await server.submitTransaction(transaction);
|
||||
|
||||
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);
|
||||
263
stellarCrypto.js
Normal file
263
stellarCrypto.js
Normal 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 = {}));
|
||||
120
stellarSearchDB.js
Normal file
120
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
stellar_favicon.svg
Normal file
4
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 |
Loading…
Reference in New Issue
Block a user