Auto-update messenger
This commit is contained in:
parent
99af2eddd4
commit
0dba4e20ec
@ -18,6 +18,15 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="https://unpkg.com/uhtml@3.0.1/es.js"></script>
|
<script src="https://unpkg.com/uhtml@3.0.1/es.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/xrpl@2.7.0/build/xrpl-latest-min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/tweetnacl@1.0.3/nacl.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/js-sha512@0.8.0/build/sha512.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/tonweb/dist/tonweb.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/tronweb@5.3.2/dist/TronWeb.js"></script>
|
||||||
|
<script src="https://unpkg.com/@polkadot/util@12.6.2/bundle-polkadot-util.js"></script>
|
||||||
|
<script src="https://unpkg.com/@polkadot/util-crypto@12.6.2/bundle-polkadot-util-crypto.js"></script>
|
||||||
|
<script src="https://unpkg.com/@solana/web3.js@latest/lib/index.iife.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/gh/void-57/cardano-wallet-test@main/cardano-crypto.iife.js"></script>
|
||||||
<script src="scripts/lib.min.js"></script>
|
<script src="scripts/lib.min.js"></script>
|
||||||
<script src="scripts/floCrypto.js"></script>
|
<script src="scripts/floCrypto.js"></script>
|
||||||
<script src="scripts/btcOperator.js"></script>
|
<script src="scripts/btcOperator.js"></script>
|
||||||
@ -27,8 +36,10 @@
|
|||||||
<script src="scripts/floDapps.js"></script>
|
<script src="scripts/floDapps.js"></script>
|
||||||
<script src="scripts/keccak.js"></script>
|
<script src="scripts/keccak.js"></script>
|
||||||
<script src="scripts/messengerEthereum.js"></script>
|
<script src="scripts/messengerEthereum.js"></script>
|
||||||
<script src="scripts/messenger.min.js"></script>
|
<script src="scripts/messenger.js"></script>
|
||||||
|
<script src="scripts/blockchainAddresses.js"></script>
|
||||||
<script id="onLoadStartUp">
|
<script id="onLoadStartUp">
|
||||||
|
|
||||||
function canConnectToCloud() {
|
function canConnectToCloud() {
|
||||||
const nodes = Object.values(floCloudAPI.nodes);
|
const nodes = Object.values(floCloudAPI.nodes);
|
||||||
// check if any node is online with websocket
|
// check if any node is online with websocket
|
||||||
@ -67,9 +78,62 @@
|
|||||||
document.body.prepend(document.createElement('adblocker-warning'))
|
document.body.prepend(document.createElement('adblocker-warning'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
floGlobals.myFloID = floCrypto.getFloID(floDapps.user.public);
|
let activeChain = localStorage.getItem(`${floGlobals.application}#activeChain`);
|
||||||
floGlobals.myBtcID = btcOperator.convert.legacy2bech(floGlobals.myFloID)
|
if (activeChain === 'XRP') {
|
||||||
floGlobals.myEthID = floEthereum.ethAddressFromCompressedPublicKey(floDapps.user.public)
|
// XRP-only login: skip FLO/BTC/ETH derivations
|
||||||
|
floGlobals.myFloID = floDapps.user.id; // This is the XRP address (r...)
|
||||||
|
floGlobals.myXrpID = floDapps.user.id;
|
||||||
|
floGlobals.myBtcID = null;
|
||||||
|
floGlobals.myEthID = null;
|
||||||
|
floGlobals.myAvaxID = null;
|
||||||
|
floGlobals.myBscID = null;
|
||||||
|
floGlobals.myMaticID = null;
|
||||||
|
floGlobals.myArbID = null;
|
||||||
|
floGlobals.myOpID = null;
|
||||||
|
floGlobals.myHbarID = null;
|
||||||
|
floGlobals.mySuiID = null;
|
||||||
|
floGlobals.myAdaID = null;
|
||||||
|
floGlobals.myTonID = null;
|
||||||
|
floGlobals.myTronID = null;
|
||||||
|
floGlobals.myDogeID = null;
|
||||||
|
floGlobals.myLtcID = null;
|
||||||
|
floGlobals.myBchID = null;
|
||||||
|
floGlobals.myDotID = null;
|
||||||
|
floGlobals.myAlgoID = null;
|
||||||
|
floGlobals.myXlmID = null;
|
||||||
|
floGlobals.mySolID = null;
|
||||||
|
} else {
|
||||||
|
floGlobals.myFloID = floCrypto.getFloID(floDapps.user.public);
|
||||||
|
floGlobals.myBtcID = btcOperator.convert.legacy2bech(floGlobals.myFloID)
|
||||||
|
floGlobals.myEthID = floEthereum.ethAddressFromCompressedPublicKey(floDapps.user.public)
|
||||||
|
// AVAX C-Chain uses same address format as Ethereum
|
||||||
|
floGlobals.myAvaxID = floGlobals.myEthID;
|
||||||
|
// BSC (Binance Smart Chain) uses same address format as Ethereum
|
||||||
|
floGlobals.myBscID = floGlobals.myEthID;
|
||||||
|
// MATIC (Polygon) uses same address format as Ethereum
|
||||||
|
floGlobals.myMaticID = floGlobals.myEthID;
|
||||||
|
// Arbitrum uses same address format as Ethereum
|
||||||
|
floGlobals.myArbID = floGlobals.myEthID;
|
||||||
|
// Optimism uses same address format as Ethereum
|
||||||
|
floGlobals.myOpID = floGlobals.myEthID;
|
||||||
|
// HBAR (Hedera) uses same address format as Ethereum
|
||||||
|
floGlobals.myHbarID = floGlobals.myEthID;
|
||||||
|
|
||||||
|
// Initialize private-key-dependent addresses to null (will be derived after messenger init)
|
||||||
|
floGlobals.myXrpID = null;
|
||||||
|
floGlobals.mySuiID = null;
|
||||||
|
floGlobals.myAdaID = null;
|
||||||
|
floGlobals.myTonID = null;
|
||||||
|
floGlobals.myTronID = null;
|
||||||
|
floGlobals.myDogeID = null;
|
||||||
|
floGlobals.myLtcID = null;
|
||||||
|
floGlobals.myBchID = null;
|
||||||
|
floGlobals.myDotID = null;
|
||||||
|
floGlobals.myAlgoID = null;
|
||||||
|
floGlobals.myXlmID = null;
|
||||||
|
floGlobals.mySolID = null;
|
||||||
|
// Note: Cardano (ADA) address will be derived from private key later
|
||||||
|
}
|
||||||
document.querySelectorAll('.user-profile-id').forEach(el => el.textContent = floGlobals.myFloID)
|
document.querySelectorAll('.user-profile-id').forEach(el => el.textContent = floGlobals.myFloID)
|
||||||
//load messages from IDB and render them
|
//load messages from IDB and render them
|
||||||
console.log(`Loading Data! Please Wait...`)
|
console.log(`Loading Data! Please Wait...`)
|
||||||
@ -80,9 +144,102 @@
|
|||||||
rmMessenger.renderUI.pipeline = renderPipelineUI;
|
rmMessenger.renderUI.pipeline = renderPipelineUI;
|
||||||
rmMessenger.renderUI.mails = m => renderMailList(m, false);
|
rmMessenger.renderUI.mails = m => renderMailList(m, false);
|
||||||
rmMessenger.renderUI.marked = renderMarked;
|
rmMessenger.renderUI.marked = renderMarked;
|
||||||
|
rmMessenger.renderUI.onChatMigrated = (oldID, newID) => {
|
||||||
|
const oldChatCard = getChatCard(oldID);
|
||||||
|
if (oldChatCard) {
|
||||||
|
oldChatCard.remove(); // Remove the old chat card from the DOM to prevent duplicates
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeChat && activeChat.address === oldID) {
|
||||||
|
activeChat.address = newID;
|
||||||
|
if (!floGlobals.isMobileView) {
|
||||||
|
history.replaceState(null, null, `#/ch?address=${newID}`);
|
||||||
|
}
|
||||||
|
// Refetch the messages to include the newly migrated history
|
||||||
|
setTimeout(() => {
|
||||||
|
if (activeChat.address === newID) {
|
||||||
|
renderMessages(newID);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
//init messenger
|
//init messenger
|
||||||
rmMessenger.init().then(result => {
|
rmMessenger.init().then(async result => {
|
||||||
console.log(result);
|
console.log(result);
|
||||||
|
|
||||||
|
// Check if account is password-secured by checking the stored secret length
|
||||||
|
try {
|
||||||
|
const indexArr = localStorage.getItem(`${floGlobals.application}#privKey`);
|
||||||
|
if (indexArr) {
|
||||||
|
const indices = JSON.parse(indexArr);
|
||||||
|
const shares = await Promise.all(indices.map(idx => compactIDB.readData('credentials', idx, floGlobals.application)));
|
||||||
|
const secret = floCrypto.retrieveShamirSecret(shares);
|
||||||
|
if (secret && secret.length !== 52) {
|
||||||
|
// Secret is longer than 52 chars = it's AES encrypted = password-secured
|
||||||
|
floGlobals.isPrivKeySecured = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not check password security status:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive private-key-dependent blockchain addresses after messenger is initialized
|
||||||
|
// Check if we have a stored login password (for password-secured accounts)
|
||||||
|
if (floGlobals._loginPassword) {
|
||||||
|
// Use stored password to derive addresses
|
||||||
|
try {
|
||||||
|
const privKey = await getPrivKeyWithPassword(floGlobals._loginPassword);
|
||||||
|
if (typeof privKey === 'string' && privKey.length > 0) {
|
||||||
|
try { floGlobals.myXrpID = convertWIFtoXrpAddress(privKey); } catch (e) { console.warn('XRP derivation failed:', e); }
|
||||||
|
try { floGlobals.mySuiID = convertWIFtoSuiAddress(privKey); } catch (e) { console.warn('SUI derivation failed:', e); }
|
||||||
|
try { floGlobals.myTonID = await convertWIFtoTonAddress(privKey); } catch (e) { console.warn('TON derivation failed:', e); }
|
||||||
|
try { floGlobals.myTronID = convertWIFtoTronAddress(privKey); } catch (e) { console.warn('TRON derivation failed:', e); }
|
||||||
|
try { floGlobals.myDogeID = convertWIFtoDogeAddress(privKey); } catch (e) { console.warn('DOGE derivation failed:', e); }
|
||||||
|
try { floGlobals.myLtcID = convertWIFtoLitecoinAddress(privKey); } catch (e) { console.warn('LTC derivation failed:', e); }
|
||||||
|
try { floGlobals.myBchID = convertWIFtoBitcoinCashAddress(privKey); } catch (e) { console.warn('BCH derivation failed:', e); }
|
||||||
|
try { floGlobals.myDotID = await convertWIFtoPolkadotAddress(privKey); } catch (e) { console.warn('DOT derivation failed:', e); }
|
||||||
|
try { floGlobals.myAlgoID = convertWIFtoAlgorandAddress(privKey); } catch (e) { console.warn('ALGO derivation failed:', e); }
|
||||||
|
try { floGlobals.myXlmID = convertWIFtoStellarAddress(privKey); } catch (e) { console.warn('XLM derivation failed:', e); }
|
||||||
|
try { floGlobals.mySolID = convertWIFtoSolanaAddress(privKey); } catch (e) { console.warn('SOL derivation failed:', e); }
|
||||||
|
try { floGlobals.myAdaID = await convertWIFtoCardanoAddress(privKey); } catch (e) { console.warn('ADA derivation failed:', e); }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to derive addresses with stored password:', e);
|
||||||
|
}
|
||||||
|
// Clear the stored password for security
|
||||||
|
delete floGlobals._loginPassword;
|
||||||
|
} else {
|
||||||
|
// Try to access private key directly (non-password-secured accounts)
|
||||||
|
try {
|
||||||
|
let privKey = floDapps.user.private;
|
||||||
|
if (privKey instanceof Promise) {
|
||||||
|
privKey = await privKey;
|
||||||
|
}
|
||||||
|
if (typeof privKey === 'string' && privKey.length > 0) {
|
||||||
|
try { floGlobals.myXrpID = convertWIFtoXrpAddress(privKey); } catch (e) { console.warn('XRP derivation failed:', e); }
|
||||||
|
try { floGlobals.mySuiID = convertWIFtoSuiAddress(privKey); } catch (e) { console.warn('SUI derivation failed:', e); }
|
||||||
|
try { floGlobals.myTonID = await convertWIFtoTonAddress(privKey); } catch (e) { console.warn('TON derivation failed:', e); }
|
||||||
|
try { floGlobals.myTronID = convertWIFtoTronAddress(privKey); } catch (e) { console.warn('TRON derivation failed:', e); }
|
||||||
|
try { floGlobals.myDogeID = convertWIFtoDogeAddress(privKey); } catch (e) { console.warn('DOGE derivation failed:', e); }
|
||||||
|
try { floGlobals.myLtcID = convertWIFtoLitecoinAddress(privKey); } catch (e) { console.warn('LTC derivation failed:', e); }
|
||||||
|
try { floGlobals.myBchID = convertWIFtoBitcoinCashAddress(privKey); } catch (e) { console.warn('BCH derivation failed:', e); }
|
||||||
|
try { floGlobals.myDotID = await convertWIFtoPolkadotAddress(privKey); } catch (e) { console.warn('DOT derivation failed:', e); }
|
||||||
|
try { floGlobals.myAlgoID = convertWIFtoAlgorandAddress(privKey); } catch (e) { console.warn('ALGO derivation failed:', e); }
|
||||||
|
try { floGlobals.myXlmID = convertWIFtoStellarAddress(privKey); } catch (e) { console.warn('XLM derivation failed:', e); }
|
||||||
|
try { floGlobals.mySolID = convertWIFtoSolanaAddress(privKey); } catch (e) { console.warn('SOL derivation failed:', e); }
|
||||||
|
try { floGlobals.myAdaID = await convertWIFtoCardanoAddress(privKey); } catch (e) { console.warn('ADA derivation failed:', e); }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Private key is password-secured but we don't have the password
|
||||||
|
floGlobals.isPrivKeySecured = true;
|
||||||
|
console.info('Private key is password-secured. Use Unlock button in Profile to derive addresses.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconnect inbox now that late-derived addresses (ADA, XRP, SOL, etc.) are available
|
||||||
|
// These were null when requestDirectInbox() ran during init()
|
||||||
|
rmMessenger.reconnectInbox();
|
||||||
|
|
||||||
//Check for available bg image
|
//Check for available bg image
|
||||||
setBgImage();
|
setBgImage();
|
||||||
routeTo(window.location.hash, { firstLoad: true })
|
routeTo(window.location.hash, { firstLoad: true })
|
||||||
@ -98,7 +255,7 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
notify(error, "error")
|
notify(e, "error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -126,6 +283,46 @@
|
|||||||
</div>
|
</div>
|
||||||
</sm-form>
|
</sm-form>
|
||||||
</sm-popup>
|
</sm-popup>
|
||||||
|
<sm-popup id="blockchain_select_popup" dismissable="false">
|
||||||
|
<h4 id="blockchain_select_title">Select Blockchain</h4>
|
||||||
|
<p>Your private key format is used by multiple networks. Please select the correct blockchain:</p>
|
||||||
|
<div class="grid gap-0-5 margin-top-1">
|
||||||
|
<button class="button w-100" onclick="resolveBlockchainSelection('ETH')">Ethereum (ETH) / EVM</button>
|
||||||
|
<button class="button w-100" onclick="resolveBlockchainSelection('ADA')">Cardano (ADA)</button>
|
||||||
|
<button class="button w-100" onclick="resolveBlockchainSelection('TRON')">TRON</button>
|
||||||
|
<button class="button w-100" onclick="resolveBlockchainSelection('DOT')">Polkadot (DOT)</button>
|
||||||
|
<button class="button w-100" onclick="resolveBlockchainSelection('SOL')">Solana (SOL)</button>
|
||||||
|
<button class="button w-100" onclick="resolveBlockchainSelection('HBAR')">Hedera (HBAR)</button>
|
||||||
|
<button class="button w-100" onclick="resolveBlockchainSelection('ARB')">Arbitrum (ARB)</button>
|
||||||
|
<button class="button w-100" onclick="resolveBlockchainSelection('OP')">Optimism (OP)</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex align-center gap-0-5 margin-top-1 margin-left-auto">
|
||||||
|
<button class="button cancel-button" onclick="resolveBlockchainSelection(null)">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</sm-popup>
|
||||||
|
<sm-popup id="btc_bch_select_popup" dismissable="false">
|
||||||
|
<h4 id="btc_bch_select_title">Select Blockchain</h4>
|
||||||
|
<p>Your private key format is used by Bitcoin and Bitcoin Cash. Please select the correct blockchain for this
|
||||||
|
key:</p>
|
||||||
|
<div class="grid gap-0-5 margin-top-1">
|
||||||
|
<button class="button w-100" onclick="resolveBlockchainSelection('BTC')">Bitcoin (BTC)</button>
|
||||||
|
<button class="button w-100" onclick="resolveBlockchainSelection('BCH')">Bitcoin Cash (BCH)</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex align-center gap-0-5 margin-top-1 margin-left-auto">
|
||||||
|
<button class="button cancel-button" onclick="resolveBlockchainSelection(null)">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</sm-popup>
|
||||||
|
<sm-popup id="ton_algo_select_popup" dismissable="false">
|
||||||
|
<h4 id="ton_algo_select_title">Select Blockchain</h4>
|
||||||
|
<p>Your private key format is used by TON and Algorand. Please select the correct blockchain for this key:</p>
|
||||||
|
<div class="grid gap-0-5 margin-top-1">
|
||||||
|
<button class="button w-100" onclick="resolveBlockchainSelection('TON')">TON</button>
|
||||||
|
<button class="button w-100" onclick="resolveBlockchainSelection('ALGO')">Algorand (ALGO)</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex align-center gap-0-5 margin-top-1 margin-left-auto">
|
||||||
|
<button class="button cancel-button" onclick="resolveBlockchainSelection(null)">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</sm-popup>
|
||||||
<div id="adblocker_warning"></div>
|
<div id="adblocker_warning"></div>
|
||||||
<div id="secondary_pages" class="page hidden">
|
<div id="secondary_pages" class="page hidden">
|
||||||
<header class="flex align-center gap-1 space-between">
|
<header class="flex align-center gap-1 space-between">
|
||||||
@ -166,7 +363,7 @@
|
|||||||
<p>Welcome back, glad to see you again</p>
|
<p>Welcome back, glad to see you again</p>
|
||||||
<sm-form id="sign_in_form">
|
<sm-form id="sign_in_form">
|
||||||
<sm-input id="private_key_field" class="password-field" type="password"
|
<sm-input id="private_key_field" class="password-field" type="password"
|
||||||
placeholder="FLO/BTC/ETH private key" error-text="Private key is invalid" data-private-key
|
placeholder="Enter Blockchain Private Key" error-text="Private key is invalid" data-private-key
|
||||||
required>
|
required>
|
||||||
<label slot="right" class="interact">
|
<label slot="right" class="interact">
|
||||||
<input type="checkbox" class="hidden" autocomplete="off" readonly
|
<input type="checkbox" class="hidden" autocomplete="off" readonly
|
||||||
@ -286,7 +483,7 @@
|
|||||||
<div id="chat_sections">
|
<div id="chat_sections">
|
||||||
<div class="flex flex-direction-column gap-0-5" style="overflow: hidden;">
|
<div class="flex flex-direction-column gap-0-5" style="overflow: hidden;">
|
||||||
<div class="flex align-center gap-0-5" style="padding: 0 1rem;">
|
<div class="flex align-center gap-0-5" style="padding: 0 1rem;">
|
||||||
<sm-input id="search_chats" type="search" placeholder="FLO/BTC/ETH address or name">
|
<sm-input id="search_chats" type="search" placeholder="Blockchain address or name">
|
||||||
<svg slot="icon" class="icon" xmlns="http://www.w3.org/2000/svg" height="24px"
|
<svg slot="icon" class="icon" xmlns="http://www.w3.org/2000/svg" height="24px"
|
||||||
viewBox="0 0 24 24" width="24px" fill="#000000">
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
||||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||||
@ -723,8 +920,8 @@
|
|||||||
<h4>Add contact</h4>
|
<h4>Add contact</h4>
|
||||||
</header>
|
</header>
|
||||||
<sm-form>
|
<sm-form>
|
||||||
<sm-input id="add_contact_floID" data-address placeholder="FLO/BTC/ETH address" error-text="Invalid address"
|
<sm-input id="add_contact_floID" data-address placeholder="Enter any blockchain address"
|
||||||
animate autofocus required></sm-input>
|
error-text="Invalid address" animate autofocus required></sm-input>
|
||||||
<sm-input id="add_contact_name" placeholder="Name" animate required></sm-input>
|
<sm-input id="add_contact_name" placeholder="Name" animate required></sm-input>
|
||||||
<button class="button button--primary" id="add_contact_button" type="submit" disabled>Add</button>
|
<button class="button button--primary" id="add_contact_button" type="submit" disabled>Add</button>
|
||||||
</sm-form>
|
</sm-form>
|
||||||
@ -831,7 +1028,7 @@
|
|||||||
<div id="contacts_container" class="observe-empty-state"></div>
|
<div id="contacts_container" class="observe-empty-state"></div>
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<h4 class="margin-bottom-0-5">No saved contacts</h4>
|
<h4 class="margin-bottom-0-5">No saved contacts</h4>
|
||||||
<p>Use 'Add contact' to add new FLO/BTC/ETH address as a contact</p>
|
<p>Use 'Add contact' to add any blockchain address as a contact</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -871,7 +1068,7 @@
|
|||||||
<div id="select_contacts_container" class="observe-empty-state"></div>
|
<div id="select_contacts_container" class="observe-empty-state"></div>
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<h4 class="margin-bottom-0-5">No saved contacts.</h4>
|
<h4 class="margin-bottom-0-5">No saved contacts.</h4>
|
||||||
<p class="margin-bottom-1">Use 'Add contact' to add new FLO/BTC/ETH address as a contact.</p>
|
<p class="margin-bottom-1">Use 'Add contact' to add a new blockchain address as a contact.</p>
|
||||||
<button class="button interactive" onclick="openPopup('add_contact_popup')">
|
<button class="button interactive" onclick="openPopup('add_contact_popup')">
|
||||||
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg"
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg"
|
||||||
enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px"
|
enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px"
|
||||||
@ -1434,7 +1631,7 @@
|
|||||||
const contacts = []
|
const contacts = []
|
||||||
const groupID = getRef('edit_group_button').dataset.groupId;
|
const groupID = getRef('edit_group_button').dataset.groupId;
|
||||||
for (const contact in floGlobals.contacts) {
|
for (const contact in floGlobals.contacts) {
|
||||||
if (!rmMessenger.groups[groupID].members.includes(contact) && floDapps.user.get_pubKey(contact)) {
|
if (!rmMessenger.groups[groupID].members.includes(contact) && getContactPubKey(contact)) {
|
||||||
contacts.push(render.selectableContact(contact))
|
contacts.push(render.selectableContact(contact))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1674,18 +1871,79 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getContactName(contactAddress) {
|
function getContactName(contactAddress) {
|
||||||
if (floDapps.user.get_contact(contactAddress))
|
// Check floGlobals.contacts directly first (handles all blockchain addresses)
|
||||||
return floDapps.user.get_contact(contactAddress)
|
if (floGlobals.contacts && floGlobals.contacts[contactAddress])
|
||||||
else if (rmMessenger.groups[contactAddress])
|
return floGlobals.contacts[contactAddress];
|
||||||
|
// Try floDapps.user.get_contact for FLO/BTC address equivalence checking
|
||||||
|
try {
|
||||||
|
const contact = floDapps.user.get_contact(contactAddress);
|
||||||
|
if (contact) return contact;
|
||||||
|
} catch (e) {
|
||||||
|
// Address format not supported by floCrypto.decodeAddr
|
||||||
|
}
|
||||||
|
if (rmMessenger.groups[contactAddress])
|
||||||
return rmMessenger.groups[contactAddress].name
|
return rmMessenger.groups[contactAddress].name
|
||||||
else if (floCrypto.isSameAddr(contactAddress, floDapps.user.id))
|
else if (floCrypto.isSameAddr(contactAddress, floDapps.user.id))
|
||||||
return 'You'
|
return 'You'
|
||||||
else
|
else
|
||||||
return contactAddress
|
return contactAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Safely get pubKey for any blockchain address
|
||||||
|
function getContactPubKey(address) {
|
||||||
|
// Check floGlobals.pubKeys directly first (handles all blockchain addresses)
|
||||||
|
if (floGlobals.pubKeys && floGlobals.pubKeys[address])
|
||||||
|
return floGlobals.pubKeys[address];
|
||||||
|
// Try floDapps.user.get_pubKey for FLO/BTC address equivalence checking
|
||||||
|
try {
|
||||||
|
return floDapps.user.get_pubKey(address);
|
||||||
|
} catch (e) {
|
||||||
|
// Address format not supported by floCrypto.decodeAddr
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isValidEthereumAddress(address) {
|
function isValidEthereumAddress(address) {
|
||||||
return /^0x[a-fA-F0-9]{40}$/.test(address);
|
return /^0x[a-fA-F0-9]{40}$/.test(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate any blockchain address (all 19 supported chains)
|
||||||
|
function isValidBlockchainAddress(address) {
|
||||||
|
if (!address || typeof address !== 'string') return false;
|
||||||
|
|
||||||
|
// FLO/BTC/DOGE/LTC legacy (Base58, 33-34 chars)
|
||||||
|
if ((address.length === 33 || address.length === 34) && /^[1-9A-HJ-NP-Za-km-z]+$/.test(address)) {
|
||||||
|
return floCrypto.validateAddr(address) || true; // Accept if Base58 valid
|
||||||
|
}
|
||||||
|
// Ethereum/EVM addresses (0x prefix, 40 hex chars) - ETH, AVAX, BSC, MATIC
|
||||||
|
if (/^0x[a-fA-F0-9]{40}$/.test(address)) return true;
|
||||||
|
// SUI addresses (0x prefix, 64 hex chars)
|
||||||
|
if (/^0x[a-fA-F0-9]{64}$/.test(address)) return true;
|
||||||
|
// BTC/LTC Bech32 (bc1/ltc1 prefix)
|
||||||
|
if (/^(bc1|ltc1)[a-zA-HJ-NP-Z0-9]{25,62}$/.test(address)) return true;
|
||||||
|
// Solana (Base58, 43-44 chars)
|
||||||
|
if ((address.length === 43 || address.length === 44) && /^[1-9A-HJ-NP-Za-km-z]+$/.test(address)) return true;
|
||||||
|
// XRP (r-prefix, 25-35 chars)
|
||||||
|
if (address.startsWith('r') && address.length >= 25 && address.length <= 35 && /^r[a-zA-Z0-9]+$/.test(address)) return true;
|
||||||
|
// TRON (T-prefix, 34 chars)
|
||||||
|
if (address.startsWith('T') && address.length === 34 && /^T[a-zA-Z0-9]+$/.test(address)) return true;
|
||||||
|
// Cardano (addr1 prefix, Bech32)
|
||||||
|
if (address.startsWith('addr1') && address.length > 50) return true;
|
||||||
|
// Polkadot (SS58, starts with 1, 47-48 chars)
|
||||||
|
if (address.startsWith('1') && (address.length === 47 || address.length === 48) && /^[a-zA-Z0-9]+$/.test(address)) return true;
|
||||||
|
// TON (Base64 URL-safe, 48 chars)
|
||||||
|
if (address.length === 48 && /^[A-Za-z0-9_-]+$/.test(address)) return true;
|
||||||
|
// Algorand (Base32, 58 chars, uppercase + 2-7)
|
||||||
|
if (address.length === 58 && /^[A-Z2-7]+$/.test(address)) return true;
|
||||||
|
// Stellar (G-prefix, Base32, 56 chars)
|
||||||
|
if (address.startsWith('G') && address.length === 56 && /^G[A-Z2-7]+$/.test(address)) return true;
|
||||||
|
// Bitcoin Cash CashAddr (q-prefix)
|
||||||
|
if (address.startsWith('q') && address.length >= 34 && address.length <= 45 && /^q[a-z0-9]+$/.test(address)) return true;
|
||||||
|
// HBAR (0.0.xxxxx format)
|
||||||
|
if (/^0\.0\.\d+$/.test(address)) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
window.addEventListener('hashchange', e => routeTo(window.location.hash))
|
window.addEventListener('hashchange', e => routeTo(window.location.hash))
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
document.body.classList.remove('hidden')
|
document.body.classList.remove('hidden')
|
||||||
@ -1693,7 +1951,7 @@
|
|||||||
if (!value) return { isValid: false, errorText: 'Please enter an address' }
|
if (!value) return { isValid: false, errorText: 'Please enter an address' }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isValid: isValidEthereumAddress(value) || floCrypto.validateAddr(value),
|
isValid: isValidBlockchainAddress(value),
|
||||||
errorText: `Invalid address.`
|
errorText: `Invalid address.`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -1858,11 +2116,44 @@
|
|||||||
removeNotificationBadge('#chat_page_button')
|
removeNotificationBadge('#chat_page_button')
|
||||||
if (floGlobals.idInterval)
|
if (floGlobals.idInterval)
|
||||||
clearInterval(floGlobals.idInterval)
|
clearInterval(floGlobals.idInterval)
|
||||||
let showingFloID = true
|
let activeChain = localStorage.getItem(`${floGlobals.application}#activeChain`) || 'FLO';
|
||||||
// alternating between floID and btcID every 10 seconds
|
let currentIdIndex = 0
|
||||||
|
const allIds = [
|
||||||
|
{ id: floGlobals.myFloID, label: 'FLO' },
|
||||||
|
{ id: floGlobals.myBtcID, label: 'BTC' },
|
||||||
|
{ id: floGlobals.myEthID, label: 'ETH' },
|
||||||
|
{ id: floGlobals.myAvaxID, label: 'AVAX' },
|
||||||
|
{ id: floGlobals.myBscID, label: 'BSC' },
|
||||||
|
{ id: floGlobals.myMaticID, label: 'MATIC' },
|
||||||
|
{ id: floGlobals.myArbID, label: 'ARB' },
|
||||||
|
{ id: floGlobals.myOpID, label: 'OP' },
|
||||||
|
{ id: floGlobals.myHbarID, label: 'HBAR' },
|
||||||
|
{ id: floGlobals.myXrpID, label: 'XRP' },
|
||||||
|
{ id: floGlobals.mySuiID, label: 'SUI' },
|
||||||
|
{ id: floGlobals.myTonID, label: 'TON' },
|
||||||
|
{ id: floGlobals.myTronID, label: 'TRON' },
|
||||||
|
{ id: floGlobals.myDogeID, label: 'DOGE' },
|
||||||
|
{ id: floGlobals.myLtcID, label: 'LTC' },
|
||||||
|
{ id: floGlobals.myBchID, label: 'BCH' },
|
||||||
|
{ id: floGlobals.myDotID, label: 'DOT' },
|
||||||
|
{ id: floGlobals.myAlgoID, label: 'ALGO' },
|
||||||
|
{ id: floGlobals.myXlmID, label: 'XLM' },
|
||||||
|
{ id: floGlobals.mySolID, label: 'SOL' },
|
||||||
|
{ id: floGlobals.myAdaID, label: 'ADA' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const idList = allIds.filter(item => {
|
||||||
|
if (!item.id) return false;
|
||||||
|
if (activeChain === 'XRP') return item.label === 'XRP';
|
||||||
|
if (item.label === 'FLO') return true; // Always include FLO
|
||||||
|
if (item.label === activeChain) return true; // Include active chain
|
||||||
|
// For EVM compatible chains which share the ETH ID:
|
||||||
|
if (['ETH', 'AVAX', 'BSC', 'MATIC', 'ARB', 'OP', 'HBAR'].includes(activeChain) && item.label === activeChain) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
floGlobals.idInterval = setInterval(() => {
|
floGlobals.idInterval = setInterval(() => {
|
||||||
document.querySelectorAll('.user-profile-id').forEach(el => el.textContent = showingFloID ? floGlobals.myFloID : floGlobals.myBtcID)
|
currentIdIndex = (currentIdIndex + 1) % idList.length
|
||||||
showingFloID = !showingFloID
|
document.querySelectorAll('.user-profile-id').forEach(el => el.textContent = idList[currentIdIndex].id)
|
||||||
}, 10000)
|
}, 10000)
|
||||||
break;
|
break;
|
||||||
case 'mail_page':
|
case 'mail_page':
|
||||||
@ -2348,6 +2639,134 @@
|
|||||||
target.type = target.type === 'password' ? 'text' : 'password';
|
target.type = target.type === 'password' ? 'text' : 'password';
|
||||||
target.focusIn()
|
target.focusIn()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get private key by decrypting with password
|
||||||
|
async function getPrivKeyWithPassword(password) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const indexArr = localStorage.getItem(`${floGlobals.application}#privKey`);
|
||||||
|
if (!indexArr) {
|
||||||
|
reject('No login credentials found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const indices = JSON.parse(indexArr);
|
||||||
|
// Read from the app database where credentials are stored
|
||||||
|
const promises = indices.map(idx => compactIDB.readData('credentials', idx, floGlobals.application));
|
||||||
|
const shares = await Promise.all(promises);
|
||||||
|
const secret = floCrypto.retrieveShamirSecret(shares);
|
||||||
|
if (!secret) {
|
||||||
|
reject('Failed to retrieve credentials');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (secret.length === 52) {
|
||||||
|
// Not password-secured, return directly
|
||||||
|
resolve(secret);
|
||||||
|
} else {
|
||||||
|
// Password-secured, decrypt with password
|
||||||
|
try {
|
||||||
|
const privKey = Crypto.AES.decrypt(secret, password);
|
||||||
|
resolve(privKey);
|
||||||
|
} catch (e) {
|
||||||
|
reject('Incorrect password');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive private-key-dependent blockchain addresses (XRP, SUI, TON, TRON, DOGE, DOT, ALGO) with password
|
||||||
|
async function derivePrivKeyAddresses() {
|
||||||
|
try {
|
||||||
|
const password = await getPromptInput('Enter password to unlock addresses', '', { isPassword: true });
|
||||||
|
if (!password) {
|
||||||
|
notify('Password is required', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Get private key using the password
|
||||||
|
const privKey = await getPrivKeyWithPassword(password);
|
||||||
|
if (typeof privKey === 'string' && privKey.length > 0) {
|
||||||
|
// Derive all addresses
|
||||||
|
try {
|
||||||
|
floGlobals.myXrpID = convertWIFtoXrpAddress(privKey);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('XRP derivation failed:', e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
floGlobals.mySuiID = convertWIFtoSuiAddress(privKey);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('SUI derivation failed:', e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
floGlobals.myTonID = await convertWIFtoTonAddress(privKey);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('TON derivation failed:', e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
floGlobals.myTronID = convertWIFtoTronAddress(privKey);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('TRON derivation failed:', e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
floGlobals.myDogeID = convertWIFtoDogeAddress(privKey);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('DOGE derivation failed:', e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
floGlobals.myLtcID = convertWIFtoLitecoinAddress(privKey);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('LTC derivation failed:', e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
floGlobals.myBchID = convertWIFtoBitcoinCashAddress(privKey);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('BCH derivation failed:', e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
floGlobals.myDotID = await convertWIFtoPolkadotAddress(privKey);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('DOT derivation failed:', e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
floGlobals.myAlgoID = convertWIFtoAlgorandAddress(privKey);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('ALGO derivation failed:', e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
floGlobals.myXlmID = convertWIFtoStellarAddress(privKey);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('XLM derivation failed:', e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
floGlobals.mySolID = convertWIFtoSolanaAddress(privKey);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('SOL derivation failed:', e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
floGlobals.myAdaID = await convertWIFtoCardanoAddress(privKey);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('ADA derivation failed:', e);
|
||||||
|
}
|
||||||
|
// Re-render profile to show new addresses
|
||||||
|
routeTo(window.location.hash);
|
||||||
|
notify('Addresses unlocked successfully!', 'success');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to derive addresses:', e);
|
||||||
|
notify(typeof e === 'string' ? e : 'Failed to unlock addresses', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _blockchainResolve = null;
|
||||||
|
function resolveBlockchainSelection(chain) {
|
||||||
|
closePopup('blockchain_select_popup');
|
||||||
|
if (_blockchainResolve) {
|
||||||
|
_blockchainResolve(chain);
|
||||||
|
_blockchainResolve = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getSignedIn(passwordType) {
|
function getSignedIn(passwordType) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
routeTo(window.location.hash)
|
routeTo(window.location.hash)
|
||||||
@ -2368,22 +2787,148 @@
|
|||||||
} else {
|
} else {
|
||||||
floGlobals.isPrivKeySecured = false;
|
floGlobals.isPrivKeySecured = false;
|
||||||
getRef('private_key_field').dataset.privateKey = ''
|
getRef('private_key_field').dataset.privateKey = ''
|
||||||
getRef('private_key_field').setAttribute('placeholder', 'FLO/BTC/ETH private key');
|
getRef('private_key_field').setAttribute('placeholder', 'Enter Blockchain Private Key');
|
||||||
getRef('private_key_field').customValidation = (value) => {
|
getRef('private_key_field').customValidation = (value) => {
|
||||||
if (!value) return { isValid: false, errorText: 'Please enter a private key' }
|
if (!value) return { isValid: false, errorText: 'Please enter a private key' }
|
||||||
|
let isValid = false;
|
||||||
|
let checkVal = value.startsWith('0x') ? value.substring(2) : value;
|
||||||
|
|
||||||
|
// Hex lengths
|
||||||
|
if (/^[0-9a-fA-F]{64}$/.test(checkVal) || /^[0-9a-fA-F]{128}$/.test(checkVal)) {
|
||||||
|
isValid = true;
|
||||||
|
}
|
||||||
|
else if (
|
||||||
|
value.startsWith('suiprivkey1')
|
||||||
|
) {
|
||||||
|
isValid = true;
|
||||||
|
} else if (value.startsWith('s') && value.length >= 29 && value.length <= 32) {
|
||||||
|
isValid = true;
|
||||||
|
} else if (value.startsWith('S') && value.length === 56) {
|
||||||
|
isValid = true;
|
||||||
|
} else {
|
||||||
|
try { isValid = !!floCrypto.getPubKeyHex(value); } catch (e) { }
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
isValid: floCrypto.getPubKeyHex(value),
|
isValid,
|
||||||
errorText: `Invalid private key.<br> It's a long string of random characters usually starting with 'R'.`
|
errorText: `Invalid private key.<br> Please enter a valid WIF or Hex string.`
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!generalPages.find(page => window.location.hash.includes(page))) {
|
if (!generalPages.find(page => window.location.hash.includes(page))) {
|
||||||
location.hash = floGlobals.isPrivKeySecured ? '#/sign_in' : `#/landing`;
|
location.hash = floGlobals.isPrivKeySecured ? '#/sign_in' : `#/landing`;
|
||||||
}
|
}
|
||||||
getRef('sign_in_button').onclick = () => {
|
getRef('sign_in_button').onclick = async () => {
|
||||||
let privateKey = getRef('private_key_field').value.trim();
|
let privateKey = getRef('private_key_field').value.trim();
|
||||||
if (/^[0-9a-fA-F]{64}$/.test(privateKey)) //if hex private key might be ethereum
|
// For users who prefix their hex keys with 0x
|
||||||
privateKey = coinjs.privkey2wif(privateKey);
|
if (privateKey.startsWith('0x')) privateKey = privateKey.substring(2);
|
||||||
|
|
||||||
|
let activeChain = null;
|
||||||
|
|
||||||
|
if (/^[0-9a-fA-F]{64}$/.test(privateKey)) { //if hex private key might be ethereum, ADA, SOL, TRON, DOT, HBAR
|
||||||
|
activeChain = await new Promise(resolve => {
|
||||||
|
_blockchainResolve = resolve;
|
||||||
|
openPopup('blockchain_select_popup');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!activeChain) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let key = new Bitcoin.ECKey(privateKey);
|
||||||
|
key.setCompressed(true);
|
||||||
|
privateKey = key.getBitcoinWalletImportFormat();
|
||||||
|
} else if (/^[0-9a-fA-F]{128}$/.test(privateKey)) {
|
||||||
|
activeChain = await new Promise(resolve => {
|
||||||
|
_blockchainResolve = resolve;
|
||||||
|
openPopup('ton_algo_select_popup');
|
||||||
|
});
|
||||||
|
if (!activeChain) return;
|
||||||
|
|
||||||
|
// Convert 128-char ed25519 key to workable FLO WIF using the first 32-bytes (seed)
|
||||||
|
let key = new Bitcoin.ECKey(privateKey.substring(0, 64));
|
||||||
|
key.setCompressed(true);
|
||||||
|
privateKey = key.getBitcoinWalletImportFormat();
|
||||||
|
} else {
|
||||||
|
if (privateKey.startsWith('suiprivkey1')) {
|
||||||
|
activeChain = 'SUI';
|
||||||
|
// Convert Bech32 SUI key to workable FLO WIF using the 32-byte seed
|
||||||
|
try {
|
||||||
|
let decoded = coinjs.bech32_decode(privateKey);
|
||||||
|
if (!decoded) throw new Error("Invalid SUI private key checksum");
|
||||||
|
let bytes = coinjs.bech32_convert(decoded.data, 5, 8, false);
|
||||||
|
// bytes[0] is the scheme flag (0x00 for Ed25519)
|
||||||
|
// bytes.slice(1) contains the 32-byte seed
|
||||||
|
let seedHex = Crypto.util.bytesToHex(bytes.slice(1));
|
||||||
|
let key = new Bitcoin.ECKey(seedHex);
|
||||||
|
key.setCompressed(true);
|
||||||
|
privateKey = key.getBitcoinWalletImportFormat();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to decode SUI key", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (privateKey.startsWith('s') && privateKey.length >= 29 && privateKey.length <= 32) {
|
||||||
|
activeChain = 'XRP';
|
||||||
|
// Convert XRP secret to workable FLO WIF using the private key bytes
|
||||||
|
try {
|
||||||
|
let wallet = xrpl.Wallet.fromSeed(privateKey);
|
||||||
|
let privKeyHex = wallet.privateKey;
|
||||||
|
// Remove ED prefix if present (Ed25519 keys)
|
||||||
|
if (privKeyHex.startsWith('ED') || privKeyHex.startsWith('ed'))
|
||||||
|
privKeyHex = privKeyHex.substring(2);
|
||||||
|
let key = new Bitcoin.ECKey(privKeyHex);
|
||||||
|
key.setCompressed(true);
|
||||||
|
privateKey = key.getBitcoinWalletImportFormat();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to decode XRP key", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (privateKey.startsWith('Q')) activeChain = 'DOGE';
|
||||||
|
else if (privateKey.startsWith('T') && privateKey.length === 51) activeChain = 'LTC';
|
||||||
|
else if (privateKey.startsWith('K') || privateKey.startsWith('L') || privateKey.startsWith('5')) {
|
||||||
|
activeChain = await new Promise(resolve => {
|
||||||
|
_blockchainResolve = resolve;
|
||||||
|
openPopup('btc_bch_select_popup');
|
||||||
|
});
|
||||||
|
if (!activeChain) return;
|
||||||
|
}
|
||||||
|
else if (privateKey.startsWith('S') && privateKey.length === 56) {
|
||||||
|
activeChain = 'XLM';
|
||||||
|
try {
|
||||||
|
const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||||
|
let bits = 0;
|
||||||
|
let value = 0;
|
||||||
|
let output = [];
|
||||||
|
for (let i = 0; i < privateKey.length; i++) {
|
||||||
|
let val = BASE32_ALPHABET.indexOf(privateKey[i].toUpperCase());
|
||||||
|
if (val < 0) throw new Error("Invalid Base32 character");
|
||||||
|
value = (value << 5) | val;
|
||||||
|
bits += 5;
|
||||||
|
if (bits >= 8) {
|
||||||
|
output.push((value >>> (bits - 8)) & 255);
|
||||||
|
bits -= 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// First byte is version, next 32 bytes are Ed25519 seed
|
||||||
|
const seedBytes = output.slice(1, 33);
|
||||||
|
const seedHex = Crypto.util.bytesToHex(seedBytes);
|
||||||
|
let key = new Bitcoin.ECKey(seedHex);
|
||||||
|
key.setCompressed(true);
|
||||||
|
privateKey = key.getBitcoinWalletImportFormat();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to decode XLM key", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (privateKey.startsWith('R')) activeChain = 'FLO';
|
||||||
|
else activeChain = 'UNKNOWN';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeChain) {
|
||||||
|
localStorage.setItem(`${floGlobals.application}#activeChain`, activeChain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store password temporarily for deriving other blockchain addresses
|
||||||
|
if (floGlobals.isPrivKeySecured) {
|
||||||
|
floGlobals._loginPassword = privateKey;
|
||||||
|
}
|
||||||
resolve(privateKey);
|
resolve(privateKey);
|
||||||
getRef('private_key_field').value = '';
|
getRef('private_key_field').value = '';
|
||||||
routeTo('loading');
|
routeTo('loading');
|
||||||
@ -2825,8 +3370,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
profile() {
|
profile() {
|
||||||
|
let activeChain = localStorage.getItem(`${floGlobals.application}#activeChain`);
|
||||||
return html`
|
return html`
|
||||||
<div class="grid gap-1-5">
|
<div class="grid gap-1-5">
|
||||||
|
${activeChain === 'XRP' ? html`
|
||||||
|
<div class="grid gap-0-5">
|
||||||
|
<h4>XRP-only login</h4>
|
||||||
|
<p>You are logged in with an XRP private key. Only the XRP address is available.</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-0-5">
|
||||||
|
<b>My XRP (Ripple) address</b>
|
||||||
|
<sm-copy class="user-xrp-id" value=${floGlobals.myXrpID}></sm-copy>
|
||||||
|
</div>
|
||||||
|
` : html`
|
||||||
<div class="grid gap-0-5">
|
<div class="grid gap-0-5">
|
||||||
<h4>
|
<h4>
|
||||||
BTC integrated with FLO
|
BTC integrated with FLO
|
||||||
@ -2839,16 +3395,95 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid gap-0-5">
|
<div class="grid gap-0-5">
|
||||||
<b>My FLO address</b>
|
<b>My FLO address</b>
|
||||||
<sm-copy class="user-flo-id" clip-text value=${floGlobals.myFloID}></sm-copy>
|
<sm-copy class="user-flo-id" value=${floGlobals.myFloID}></sm-copy>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-0-5">
|
<div class="grid gap-0-5">
|
||||||
<b>My Bitcoin address</b>
|
<b>My Bitcoin address</b>
|
||||||
<sm-copy class="user-btc-id" clip-text value=${floGlobals.myBtcID}></sm-copy>
|
<sm-copy class="user-btc-id" value=${floGlobals.myBtcID}></sm-copy>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-0-5">
|
<div class="grid gap-0-5">
|
||||||
<b>My Ethereum address</b>
|
<b>My Ethereum address</b>
|
||||||
<sm-copy class="user-eth-id" clip-text value=${floGlobals.myEthID}></sm-copy>
|
<sm-copy class="user-eth-id" value=${floGlobals.myEthID}></sm-copy>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid gap-0-5">
|
||||||
|
<b>My AVAX (Avalanche C-Chain) address</b>
|
||||||
|
<sm-copy class="user-avax-id" value=${floGlobals.myAvaxID}></sm-copy>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-0-5">
|
||||||
|
<b>My BSC (Binance Smart Chain) address</b>
|
||||||
|
<sm-copy class="user-bsc-id" value=${floGlobals.myBscID}></sm-copy>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-0-5">
|
||||||
|
<b>My MATIC (Polygon) address</b>
|
||||||
|
<sm-copy class="user-matic-id" value=${floGlobals.myMaticID}></sm-copy>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-0-5">
|
||||||
|
<b>My ARB (Arbitrum) address</b>
|
||||||
|
<sm-copy class="user-arb-id" value=${floGlobals.myArbID}></sm-copy>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-0-5">
|
||||||
|
<b>My OP (Optimism) address</b>
|
||||||
|
<sm-copy class="user-op-id" value=${floGlobals.myOpID}></sm-copy>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-0-5">
|
||||||
|
<b>My HBAR (Hedera) address</b>
|
||||||
|
<sm-copy class="user-hbar-id" value=${floGlobals.myHbarID}></sm-copy>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-0-5">
|
||||||
|
<b>My XRP (Ripple) address</b>
|
||||||
|
<sm-copy class="user-xrp-id" value=${floGlobals.myXrpID || 'Not available'}></sm-copy>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-0-5">
|
||||||
|
<b>My SUI address</b>
|
||||||
|
<sm-copy class="user-sui-id" value=${floGlobals.mySuiID || 'Not available'}></sm-copy>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-0-5">
|
||||||
|
<b>My TON address</b>
|
||||||
|
<sm-copy class="user-ton-id" value=${floGlobals.myTonID || 'Not available'}></sm-copy>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-0-5">
|
||||||
|
<b>My TRON address</b>
|
||||||
|
<sm-copy class="user-tron-id" value=${floGlobals.myTronID || 'Not available'}></sm-copy>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-0-5">
|
||||||
|
<b>My DOGE address</b>
|
||||||
|
<sm-copy class="user-doge-id" value=${floGlobals.myDogeID || 'Not available'}></sm-copy>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-0-5">
|
||||||
|
<b>My LTC (Litecoin) address</b>
|
||||||
|
<sm-copy class="user-ltc-id" value=${floGlobals.myLtcID || 'Not available'}></sm-copy>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-0-5">
|
||||||
|
<b>My BCH (Bitcoin Cash) address</b>
|
||||||
|
<sm-copy class="user-bch-id" value=${floGlobals.myBchID || 'Not available'}></sm-copy>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-0-5">
|
||||||
|
<b>My DOT (Polkadot) address</b>
|
||||||
|
<sm-copy class="user-dot-id" value=${floGlobals.myDotID || 'Not available'}></sm-copy>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-0-5">
|
||||||
|
<b>My ALGO (Algorand) address</b>
|
||||||
|
<sm-copy class="user-algo-id" value=${floGlobals.myAlgoID || 'Not available'}></sm-copy>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-0-5">
|
||||||
|
<b>My XLM (Stellar) address</b>
|
||||||
|
<sm-copy class="user-xlm-id" value=${floGlobals.myXlmID || 'Not available'}></sm-copy>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-0-5">
|
||||||
|
<b>My SOL (Solana) address</b>
|
||||||
|
<sm-copy class="user-sol-id" value=${floGlobals.mySolID || 'Not available'}></sm-copy>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-0-5">
|
||||||
|
<b>My ADA (Cardano) address</b>
|
||||||
|
<sm-copy class="user-ada-id" value=${floGlobals.myAdaID || 'Not available'}></sm-copy>
|
||||||
|
</div>
|
||||||
|
${(!floGlobals.myXrpID && floGlobals.isPrivKeySecured) ? html`
|
||||||
|
<button class="button button--primary justify-self-start" onclick=${derivePrivKeyAddresses}>
|
||||||
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>
|
||||||
|
Unlock all addresses
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
`}
|
||||||
<button class="button button--danger justify-self-start" onclick="signOut()">Sign out</button>
|
<button class="button button--danger justify-self-start" onclick="signOut()">Sign out</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-1">
|
<div class="grid gap-1">
|
||||||
@ -3086,6 +3721,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateMessageUI(messagesData, sentByMe = false) {
|
async function updateMessageUI(messagesData, sentByMe = false) {
|
||||||
|
const isSameAddrSafe = (a, b) => {
|
||||||
|
if (!a || !b) return false;
|
||||||
|
if (a === b) return true;
|
||||||
|
try {
|
||||||
|
return floCrypto.isSameAddr(a, b);
|
||||||
|
} catch (e) { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
const animOptions = {
|
const animOptions = {
|
||||||
duration: 300,
|
duration: 300,
|
||||||
easing: 'ease',
|
easing: 'ease',
|
||||||
@ -3095,8 +3738,8 @@
|
|||||||
const { category, floID, time, message, sender, groupID, admin, name, pipeID, unconfirmed, type } = messagesData[messageId]
|
const { category, floID, time, message, sender, groupID, admin, name, pipeID, unconfirmed, type } = messagesData[messageId]
|
||||||
const chatAddress = floID || groupID || pipeID
|
const chatAddress = floID || groupID || pipeID
|
||||||
// code to run if a chat is opened
|
// code to run if a chat is opened
|
||||||
if (activeChat && floCrypto.isSameAddr(activeChat.address, chatAddress) || floCrypto.isSameAddr(floCrypto.getFloID(getEthPubKey(activeChat.address)), chatAddress)) {
|
if (activeChat && (isSameAddrSafe(activeChat.address, chatAddress) || isSameAddrSafe(floCrypto.getFloID(getEthPubKey(activeChat.address)), chatAddress))) {
|
||||||
if (sentByMe || type === 'TRANSACTION' || !sender || !floCrypto.isSameAddr(sender, floDapps.user.id)) {
|
if (sentByMe || type === 'TRANSACTION' || !sender || !isSameAddrSafe(sender, floDapps.user.id)) {
|
||||||
const messageBody = render.messageBubble(messagesData[messageId]);
|
const messageBody = render.messageBubble(messagesData[messageId]);
|
||||||
getRef('messages_container').append(messageBody);
|
getRef('messages_container').append(messageBody);
|
||||||
}
|
}
|
||||||
@ -3104,7 +3747,7 @@
|
|||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
// remove encryption badge if it exists
|
// remove encryption badge if it exists
|
||||||
if (!groupID && (floDapps.user.get_pubKey(activeChat.address) || getEthPubKey(activeChat.address)) && floID !== floDapps.user.id) {
|
if (!groupID && (getContactPubKey(activeChat.address) || getEthPubKey(activeChat.address)) && floID !== floDapps.user.id) {
|
||||||
if (getRef('warn_no_encryption')) {
|
if (getRef('warn_no_encryption')) {
|
||||||
getRef('warn_no_encryption').after(
|
getRef('warn_no_encryption').after(
|
||||||
html.node`
|
html.node`
|
||||||
@ -3126,7 +3769,7 @@
|
|||||||
}
|
}
|
||||||
// move chat card to top if it is not already there
|
// move chat card to top if it is not already there
|
||||||
const topChatCard = getRef('chats_list').children[0]
|
const topChatCard = getRef('chats_list').children[0]
|
||||||
if (!floCrypto.isSameAddr(chatAddress, topChatCard.dataset.address) && !floCrypto.isSameAddr(floCrypto.getFloID(getEthPubKey(chatAddress)), topChatCard.dataset.address)) {
|
if (!isSameAddrSafe(chatAddress, topChatCard.dataset.address) && !isSameAddrSafe(floCrypto.getFloID(getEthPubKey(chatAddress)), topChatCard.dataset.address)) {
|
||||||
const cloneContact = chatCard.cloneNode(true)
|
const cloneContact = chatCard.cloneNode(true)
|
||||||
chatCard.remove()
|
chatCard.remove()
|
||||||
getRef('chats_list').prepend(cloneContact)
|
getRef('chats_list').prepend(cloneContact)
|
||||||
@ -3172,7 +3815,7 @@
|
|||||||
if (chatCard.querySelector('.time'))
|
if (chatCard.querySelector('.time'))
|
||||||
chatCard.querySelector('.time').textContent = getFormattedTime(time, 'relative')
|
chatCard.querySelector('.time').textContent = getFormattedTime(time, 'relative')
|
||||||
|
|
||||||
if (floCrypto.isSameAddr(activeChat.address, chatAddress) || floCrypto.isSameAddr(floCrypto.getFloID(getEthPubKey(activeChat.address)), chatAddress)) {
|
if (activeChat && (isSameAddrSafe(activeChat.address, chatAddress) || isSameAddrSafe(floCrypto.getFloID(getEthPubKey(activeChat.address)), chatAddress))) {
|
||||||
if (chatScrollInfo.isScrolledUp)
|
if (chatScrollInfo.isScrolledUp)
|
||||||
getRef('scroll_to_bottom').classList.add('new-message')
|
getRef('scroll_to_bottom').classList.add('new-message')
|
||||||
else {
|
else {
|
||||||
@ -3538,7 +4181,7 @@
|
|||||||
const selctedPubKeys = [...selectedMembers].map(id => {
|
const selctedPubKeys = [...selectedMembers].map(id => {
|
||||||
if (id === floDapps.user.id)
|
if (id === floDapps.user.id)
|
||||||
return floDapps.user.public
|
return floDapps.user.public
|
||||||
return floDapps.user.get_pubKey(id)
|
return getContactPubKey(id)
|
||||||
});
|
});
|
||||||
const minRequired = parseInt(getRef('min_sign_required').value.trim());
|
const minRequired = parseInt(getRef('min_sign_required').value.trim());
|
||||||
const label = getRef('multisig_label').value.trim();
|
const label = getRef('multisig_label').value.trim();
|
||||||
@ -3826,19 +4469,31 @@
|
|||||||
let addressToSave = getRef('add_contact_floID').value.trim();
|
let addressToSave = getRef('add_contact_floID').value.trim();
|
||||||
let name = getRef('add_contact_name').value.trim();
|
let name = getRef('add_contact_name').value.trim();
|
||||||
if (floCrypto.isSameAddr(addressToSave, floDapps.user.id) || floCrypto.myEthID === addressToSave) {
|
if (floCrypto.isSameAddr(addressToSave, floDapps.user.id) || floCrypto.myEthID === addressToSave) {
|
||||||
notify(`you can't add your own FLO/BTC/ETH address as contact`, 'error')
|
notify(`you can't add your own blockchain address as contact`, 'error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (floGlobals.contacts.hasOwnProperty(addressToSave)) {
|
if (floGlobals.contacts.hasOwnProperty(addressToSave)) {
|
||||||
notify(`Contact already saved`, 'error')
|
notify(`Contact already saved`, 'error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// check whether an equivalent BTC/FLO address is already saved
|
// check whether an equivalent BTC/FLO address is already saved (only for compatible address types)
|
||||||
const addrInFlo = floCrypto.toFloID(addressToSave);
|
let addrInFlo = null, addrInBtc = null, addrInEth = null;
|
||||||
const addrInBtc = btcOperator.convert.legacy2bech(addrInFlo);
|
try {
|
||||||
const addrInEth = floGlobals.pubKeys[addressToSave] ? floEthereum.ethAddressFromCompressedPublicKey(floGlobals.pubKeys[addressToSave]) : null;
|
// Only attempt conversion for legacy Base58 addresses (FLO/BTC format)
|
||||||
if (floGlobals.contacts.hasOwnProperty(addrInFlo) || floGlobals.contacts.hasOwnProperty(addrInBtc) || floGlobals.contacts.hasOwnProperty(addrInEth)) {
|
if ((addressToSave.length === 33 || addressToSave.length === 34) && /^[1-9A-HJ-NP-Za-km-z]+$/.test(addressToSave)) {
|
||||||
notify(`Equivalent Address is already saved as ${getContactName(addrInFlo)}`, 'error');
|
addrInFlo = floCrypto.toFloID(addressToSave);
|
||||||
|
if (addrInFlo) {
|
||||||
|
addrInBtc = btcOperator.convert.legacy2bech(addrInFlo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addrInEth = floGlobals.pubKeys[addressToSave] ? floEthereum.ethAddressFromCompressedPublicKey(floGlobals.pubKeys[addressToSave]) : null;
|
||||||
|
} catch (e) {
|
||||||
|
// Conversion not applicable for this address type
|
||||||
|
}
|
||||||
|
if ((addrInFlo && floGlobals.contacts.hasOwnProperty(addrInFlo)) ||
|
||||||
|
(addrInBtc && floGlobals.contacts.hasOwnProperty(addrInBtc)) ||
|
||||||
|
(addrInEth && floGlobals.contacts.hasOwnProperty(addrInEth))) {
|
||||||
|
notify(`Equivalent Address is already saved as ${getContactName(addrInFlo || addressToSave)}`, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rmMessenger.storeContact(addressToSave, name).then(result => {
|
rmMessenger.storeContact(addressToSave, name).then(result => {
|
||||||
@ -3880,7 +4535,7 @@
|
|||||||
}
|
}
|
||||||
for (const floID in floGlobals.contacts) {
|
for (const floID in floGlobals.contacts) {
|
||||||
if (getAddressType(floID) !== 'plain') continue;
|
if (getAddressType(floID) !== 'plain') continue;
|
||||||
if (floDapps.user.get_pubKey(floID)) {
|
if (getContactPubKey(floID)) {
|
||||||
contacts.push(render.selectableContact(floID))
|
contacts.push(render.selectableContact(floID))
|
||||||
} else {
|
} else {
|
||||||
const hasSentRequest = skipSendingRequest.has(floID)
|
const hasSentRequest = skipSendingRequest.has(floID)
|
||||||
@ -4012,12 +4667,19 @@
|
|||||||
let floChatAddress
|
let floChatAddress
|
||||||
let btcChatAddress
|
let btcChatAddress
|
||||||
const promises = []
|
const promises = []
|
||||||
floChatAddress = validateEthAddress(address) ? floCrypto.getFloID(floGlobals.pubKeys[address] || getEthPubKey(address)) : floCrypto.toFloID(address);
|
// Safely try to derive FLO address (might fail for ADA/SOL etc)
|
||||||
|
try {
|
||||||
|
floChatAddress = validateEthAddress(address) ? floCrypto.getFloID(floGlobals.pubKeys[address] || getEthPubKey(address)) : floCrypto.toFloID(address);
|
||||||
|
} catch (e) { }
|
||||||
|
|
||||||
if (floChatAddress) {
|
if (floChatAddress) {
|
||||||
btcChatAddress = btcOperator.convert.legacy2bech(floChatAddress);
|
btcChatAddress = btcOperator.convert.legacy2bech(floChatAddress);
|
||||||
promises.push(rmMessenger.getChat(floChatAddress), rmMessenger.getChat(btcChatAddress))
|
promises.push(rmMessenger.getChat(floChatAddress), rmMessenger.getChat(btcChatAddress))
|
||||||
}
|
}
|
||||||
if (validateEthAddress(address))
|
|
||||||
|
// Fetch chat for the address itself if it's not the derived FLO address
|
||||||
|
// This ensures we get messages for ADA, SOL, XRP etc.
|
||||||
|
if (address !== floChatAddress)
|
||||||
promises.push(rmMessenger.getChat(address))
|
promises.push(rmMessenger.getChat(address))
|
||||||
Promise.all(promises)
|
Promise.all(promises)
|
||||||
.then((chats) => {
|
.then((chats) => {
|
||||||
@ -4043,7 +4705,8 @@
|
|||||||
batchSize: 20,
|
batchSize: 20,
|
||||||
onEnd: () => {
|
onEnd: () => {
|
||||||
if (activeChat.type === 'plain') {
|
if (activeChat.type === 'plain') {
|
||||||
if (floDapps.user.get_pubKey(activeChat.address) || getEthPubKey(activeChat.address)) {
|
const hasPubKey = getContactPubKey(activeChat.address) || getEthPubKey(activeChat.address);
|
||||||
|
if (hasPubKey) {
|
||||||
getRef('messages_container').prepend(html.node`<strong class="event-card flex align-center">
|
getRef('messages_container').prepend(html.node`<strong class="event-card flex align-center">
|
||||||
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g fill="none"><path d="M0 0h24v24H0V0z"/><path d="M0 0h24v24H0V0z" opacity=".87"/></g><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/></svg>
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g fill="none"><path d="M0 0h24v24H0V0z"/><path d="M0 0h24v24H0V0z" opacity=".87"/></g><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/></svg>
|
||||||
Conversation is encrypted
|
Conversation is encrypted
|
||||||
@ -5282,4 +5945,4 @@
|
|||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
1019
messenger/scripts/blockchainAddresses.js
Normal file
1019
messenger/scripts/blockchainAddresses.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -392,7 +392,22 @@
|
|||||||
if (!address)
|
if (!address)
|
||||||
return;
|
return;
|
||||||
var bytes;
|
var bytes;
|
||||||
if (address.length == 33 || address.length == 34) { //legacy encoding
|
|
||||||
|
// XRP Address (Base58, starts with 'r', 25-35 chars) - must be checked before FLO/BTC legacy
|
||||||
|
if (address.length >= 25 && address.length <= 35 && address.startsWith('r')) {
|
||||||
|
try {
|
||||||
|
// XRP address - hash the raw address for unique proxy ID
|
||||||
|
let addrBytes = [];
|
||||||
|
for (let i = 0; i < address.length; i++) {
|
||||||
|
addrBytes.push(address.charCodeAt(i));
|
||||||
|
}
|
||||||
|
bytes = ripemd160(Crypto.SHA256(addrBytes, { asBytes: true }));
|
||||||
|
} catch (e) {
|
||||||
|
bytes = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// FLO/BTC legacy encoding (33-34 chars)
|
||||||
|
else if (address.length == 33 || address.length == 34) {
|
||||||
let decode = bitjs.Base58.decode(address);
|
let decode = bitjs.Base58.decode(address);
|
||||||
bytes = decode.slice(0, decode.length - 4);
|
bytes = decode.slice(0, decode.length - 4);
|
||||||
let checksum = decode.slice(decode.length - 4),
|
let checksum = decode.slice(decode.length - 4),
|
||||||
@ -403,7 +418,9 @@
|
|||||||
});
|
});
|
||||||
hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3] ?
|
hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3] ?
|
||||||
bytes = undefined : bytes.shift();
|
bytes = undefined : bytes.shift();
|
||||||
} else if (!address.startsWith("0x") && address.length == 42 || address.length == 62) { //bech encoding
|
}
|
||||||
|
// BTC/LTC Bech32 encoding (bc1 or ltc1 prefix)
|
||||||
|
else if (/^(bc1|ltc1)[a-zA-HJ-NP-Z0-9]{25,62}$/.test(address)) {
|
||||||
if (typeof coinjs !== 'function')
|
if (typeof coinjs !== 'function')
|
||||||
throw "library missing (lib_btc.js)";
|
throw "library missing (lib_btc.js)";
|
||||||
let decode = coinjs.bech32_decode(address);
|
let decode = coinjs.bech32_decode(address);
|
||||||
@ -414,14 +431,154 @@
|
|||||||
if (address.length == 62) //for long bech, aggregate once more to get 160 bit
|
if (address.length == 62) //for long bech, aggregate once more to get 160 bit
|
||||||
bytes = coinjs.bech32_convert(bytes, 5, 8, false);
|
bytes = coinjs.bech32_convert(bytes, 5, 8, false);
|
||||||
}
|
}
|
||||||
} else if (address.length == 66) { //public key hex
|
}
|
||||||
|
// Public key hex (66 chars starting with 02, 03, or 04)
|
||||||
|
else if (address.length == 66 && /^0[234][a-fA-F0-9]{64}$/.test(address)) {
|
||||||
bytes = ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(address), {
|
bytes = ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(address), {
|
||||||
asBytes: true
|
asBytes: true
|
||||||
}));
|
}));
|
||||||
} else if ((address.length == 42 && address.startsWith("0x")) || (address.length == 40 && !address.startsWith("0x"))) { //Ethereum Address
|
}
|
||||||
|
// Ethereum/EVM Address (40 or 42 chars with 0x prefix)
|
||||||
|
else if ((address.length == 42 && address.startsWith("0x")) || (address.length == 40 && /^[a-fA-F0-9]{40}$/.test(address))) {
|
||||||
if (address.startsWith("0x")) { address = address.substring(2); }
|
if (address.startsWith("0x")) { address = address.substring(2); }
|
||||||
bytes = Crypto.util.hexToBytes(address);
|
bytes = Crypto.util.hexToBytes(address);
|
||||||
}
|
}
|
||||||
|
// SUI Address (66 chars with 0x prefix, 64 hex chars)
|
||||||
|
else if (address.length == 66 && address.startsWith("0x") && /^0x[a-fA-F0-9]{64}$/.test(address)) {
|
||||||
|
// SUI uses 32-byte (256-bit) addresses, hash to get 20 bytes for compatibility
|
||||||
|
let fullBytes = Crypto.util.hexToBytes(address.substring(2));
|
||||||
|
bytes = ripemd160(Crypto.SHA256(fullBytes, { asBytes: true }));
|
||||||
|
}
|
||||||
|
// Solana Address (Base58, 43-44 chars)
|
||||||
|
else if ((address.length == 43 || address.length == 44) && /^[1-9A-HJ-NP-Za-km-z]+$/.test(address)) {
|
||||||
|
try {
|
||||||
|
// Solana address - hash the raw address for unique proxy ID
|
||||||
|
let addrBytes = [];
|
||||||
|
for (let i = 0; i < address.length; i++) {
|
||||||
|
addrBytes.push(address.charCodeAt(i));
|
||||||
|
}
|
||||||
|
bytes = ripemd160(Crypto.SHA256(addrBytes, { asBytes: true }));
|
||||||
|
} catch (e) {
|
||||||
|
bytes = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// XRP Address (Base58, starts with 'r', 25-35 chars)
|
||||||
|
else if (address.length >= 25 && address.length <= 35 && address.startsWith('r')) {
|
||||||
|
try {
|
||||||
|
// XRP address - hash the raw address for unique proxy ID
|
||||||
|
let addrBytes = [];
|
||||||
|
for (let i = 0; i < address.length; i++) {
|
||||||
|
addrBytes.push(address.charCodeAt(i));
|
||||||
|
}
|
||||||
|
bytes = ripemd160(Crypto.SHA256(addrBytes, { asBytes: true }));
|
||||||
|
} catch (e) {
|
||||||
|
bytes = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TRON Address (Base58, starts with 'T', 34 chars)
|
||||||
|
else if (address.length == 34 && address.startsWith('T')) {
|
||||||
|
try {
|
||||||
|
// TRON address - hash the raw address for unique proxy ID
|
||||||
|
let addrBytes = [];
|
||||||
|
for (let i = 0; i < address.length; i++) {
|
||||||
|
addrBytes.push(address.charCodeAt(i));
|
||||||
|
}
|
||||||
|
bytes = ripemd160(Crypto.SHA256(addrBytes, { asBytes: true }));
|
||||||
|
} catch (e) {
|
||||||
|
bytes = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Cardano Address (Bech32, starts with 'addr1', typically 98-103 chars)
|
||||||
|
else if (address.startsWith('addr1') && address.length > 50) {
|
||||||
|
try {
|
||||||
|
// Cardano bech32 address - hash the raw address for unique proxy ID
|
||||||
|
let addrBytes = [];
|
||||||
|
for (let i = 0; i < address.length; i++) {
|
||||||
|
addrBytes.push(address.charCodeAt(i));
|
||||||
|
}
|
||||||
|
bytes = ripemd160(Crypto.SHA256(addrBytes, { asBytes: true }));
|
||||||
|
} catch (e) {
|
||||||
|
bytes = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Polkadot Address (SS58, starts with '1', 47-48 chars)
|
||||||
|
else if ((address.length == 47 || address.length == 48) && /^1[a-zA-Z0-9]+$/.test(address)) {
|
||||||
|
try {
|
||||||
|
// Polkadot address - hash the raw address for unique proxy ID
|
||||||
|
let addrBytes = [];
|
||||||
|
for (let i = 0; i < address.length; i++) {
|
||||||
|
addrBytes.push(address.charCodeAt(i));
|
||||||
|
}
|
||||||
|
bytes = ripemd160(Crypto.SHA256(addrBytes, { asBytes: true }));
|
||||||
|
} catch (e) {
|
||||||
|
bytes = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TON Address (Base64, 48 chars)
|
||||||
|
else if (address.length == 48 && /^[A-Za-z0-9_-]+$/.test(address)) {
|
||||||
|
try {
|
||||||
|
// TON address - hash the raw address for unique proxy ID
|
||||||
|
let addrBytes = [];
|
||||||
|
for (let i = 0; i < address.length; i++) {
|
||||||
|
addrBytes.push(address.charCodeAt(i));
|
||||||
|
}
|
||||||
|
bytes = ripemd160(Crypto.SHA256(addrBytes, { asBytes: true }));
|
||||||
|
} catch (e) {
|
||||||
|
bytes = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Algorand Address (Base32, 58 chars)
|
||||||
|
else if (address.length == 58 && /^[A-Z2-7]+$/.test(address)) {
|
||||||
|
try {
|
||||||
|
// Algorand address - hash the raw address for unique proxy ID
|
||||||
|
let addrBytes = [];
|
||||||
|
for (let i = 0; i < address.length; i++) {
|
||||||
|
addrBytes.push(address.charCodeAt(i));
|
||||||
|
}
|
||||||
|
bytes = ripemd160(Crypto.SHA256(addrBytes, { asBytes: true }));
|
||||||
|
} catch (e) {
|
||||||
|
bytes = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Stellar Address (Base32, starts with 'G', 56 chars)
|
||||||
|
else if (address.length == 56 && address.startsWith('G')) {
|
||||||
|
try {
|
||||||
|
// Stellar address - hash the raw address for unique proxy ID
|
||||||
|
let addrBytes = [];
|
||||||
|
for (let i = 0; i < address.length; i++) {
|
||||||
|
addrBytes.push(address.charCodeAt(i));
|
||||||
|
}
|
||||||
|
bytes = ripemd160(Crypto.SHA256(addrBytes, { asBytes: true }));
|
||||||
|
} catch (e) {
|
||||||
|
bytes = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Bitcoin Cash CashAddr format (without prefix, 42 chars of data)
|
||||||
|
else if (address.length >= 34 && address.length <= 45 && /^q[a-z0-9]+$/.test(address)) {
|
||||||
|
try {
|
||||||
|
// BCH address - hash the raw address for unique proxy ID
|
||||||
|
let addrBytes = [];
|
||||||
|
for (let i = 0; i < address.length; i++) {
|
||||||
|
addrBytes.push(address.charCodeAt(i));
|
||||||
|
}
|
||||||
|
bytes = ripemd160(Crypto.SHA256(addrBytes, { asBytes: true }));
|
||||||
|
} catch (e) {
|
||||||
|
bytes = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// HBAR Address (account ID format: 0.0.xxxxx)
|
||||||
|
else if (/^0\.0\.\d+$/.test(address)) {
|
||||||
|
try {
|
||||||
|
// HBAR address - hash the raw address for unique proxy ID
|
||||||
|
let addrBytes = [];
|
||||||
|
for (let i = 0; i < address.length; i++) {
|
||||||
|
addrBytes.push(address.charCodeAt(i));
|
||||||
|
}
|
||||||
|
bytes = ripemd160(Crypto.SHA256(addrBytes, { asBytes: true }));
|
||||||
|
} catch (e) {
|
||||||
|
bytes = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!bytes)
|
if (!bytes)
|
||||||
throw "Invalid address: " + address;
|
throw "Invalid address: " + address;
|
||||||
@ -535,9 +692,17 @@
|
|||||||
//send any message to supernode cloud storage
|
//send any message to supernode cloud storage
|
||||||
const sendApplicationData = floCloudAPI.sendApplicationData = function (message, type, options = {}) {
|
const sendApplicationData = floCloudAPI.sendApplicationData = function (message, type, options = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
let originalReceiverID = options.receiverID || DEFAULT.adminID;
|
||||||
|
let serverReceiverID = originalReceiverID;
|
||||||
|
try {
|
||||||
|
if (!floCrypto.validateAddr(originalReceiverID)) {
|
||||||
|
serverReceiverID = proxyID(originalReceiverID);
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
console.log("[SEND] original:", originalReceiverID, "server:", serverReceiverID);
|
||||||
var data = {
|
var data = {
|
||||||
senderID: user.id,
|
senderID: user.id,
|
||||||
receiverID: options.receiverID || DEFAULT.adminID,
|
receiverID: serverReceiverID,
|
||||||
pubKey: user.public,
|
pubKey: user.public,
|
||||||
message: encodeMessage(message),
|
message: encodeMessage(message),
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
@ -548,7 +713,7 @@
|
|||||||
let hashcontent = ["receiverID", "time", "application", "type", "message", "comment"]
|
let hashcontent = ["receiverID", "time", "application", "type", "message", "comment"]
|
||||||
.map(d => data[d]).join("|")
|
.map(d => data[d]).join("|")
|
||||||
data.sign = user.sign(hashcontent);
|
data.sign = user.sign(hashcontent);
|
||||||
singleRequest(data.receiverID, data)
|
singleRequest(originalReceiverID, data)
|
||||||
.then(result => resolve(result))
|
.then(result => resolve(result))
|
||||||
.catch(error => reject(error))
|
.catch(error => reject(error))
|
||||||
})
|
})
|
||||||
@ -557,8 +722,16 @@
|
|||||||
//request any data from supernode cloud
|
//request any data from supernode cloud
|
||||||
const requestApplicationData = floCloudAPI.requestApplicationData = function (type, options = {}) {
|
const requestApplicationData = floCloudAPI.requestApplicationData = function (type, options = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
let originalReceiverID = options.receiverID || DEFAULT.adminID;
|
||||||
|
let serverReceiverID = originalReceiverID;
|
||||||
|
try {
|
||||||
|
if (!floCrypto.validateAddr(originalReceiverID)) {
|
||||||
|
serverReceiverID = proxyID(originalReceiverID);
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
console.log("[RECV] original:", originalReceiverID, "server:", serverReceiverID);
|
||||||
var request = {
|
var request = {
|
||||||
receiverID: options.receiverID || DEFAULT.adminID,
|
receiverID: serverReceiverID,
|
||||||
senderID: options.senderID || undefined,
|
senderID: options.senderID || undefined,
|
||||||
application: options.application || DEFAULT.application,
|
application: options.application || DEFAULT.application,
|
||||||
type: type,
|
type: type,
|
||||||
@ -571,7 +744,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options.callback instanceof Function) {
|
if (options.callback instanceof Function) {
|
||||||
liveRequest(request.receiverID, request, options.callback)
|
liveRequest(originalReceiverID, request, options.callback)
|
||||||
.then(result => resolve(result))
|
.then(result => resolve(result))
|
||||||
.catch(error => reject(error))
|
.catch(error => reject(error))
|
||||||
} else {
|
} else {
|
||||||
@ -580,7 +753,7 @@
|
|||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
request
|
request
|
||||||
};
|
};
|
||||||
singleRequest(request.receiverID, request, options.method || "GET")
|
singleRequest(originalReceiverID, request, options.method || "GET")
|
||||||
.then(data => resolve(data)).catch(error => reject(error))
|
.then(data => resolve(data)).catch(error => reject(error))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -299,7 +299,7 @@
|
|||||||
return true;
|
return true;
|
||||||
else
|
else
|
||||||
return false;
|
return false;
|
||||||
} else if (raw.type === 'ethereum') {
|
} else if (raw.type === 'ethereum' || raw.type === 'bch') {
|
||||||
return true
|
return true
|
||||||
} else //unknown
|
} else //unknown
|
||||||
return false;
|
return false;
|
||||||
@ -431,14 +431,30 @@
|
|||||||
} else
|
} else
|
||||||
return null;
|
return null;
|
||||||
} else if ((address.length == 42 && address.startsWith("0x")) || (address.length == 40 && !address.startsWith("0x"))) { //Ethereum Address
|
} else if ((address.length == 42 && address.startsWith("0x")) || (address.length == 40 && !address.startsWith("0x"))) { //Ethereum Address
|
||||||
if (address.startsWith("0x")) { address = address.substring(2); }
|
if (address.startsWith("0x")) { address = address.substring(2); }
|
||||||
let bytes = Crypto.util.hexToBytes(address);
|
let bytes = Crypto.util.hexToBytes(address);
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
hex: address,
|
hex: address,
|
||||||
type: 'ethereum',
|
type: 'ethereum',
|
||||||
bytes
|
bytes
|
||||||
}
|
}
|
||||||
|
} else if (address.length >= 34 && address.length <= 45 && /^q[a-z0-9]+$/.test(address)) { //BCH Address
|
||||||
|
try {
|
||||||
|
let addrBytes = [];
|
||||||
|
for (let i = 0; i < address.length; i++) {
|
||||||
|
addrBytes.push(address.charCodeAt(i));
|
||||||
|
}
|
||||||
|
let payload = ripemd160(Crypto.SHA256(addrBytes, { asBytes: true }));
|
||||||
|
return {
|
||||||
|
version: 1,
|
||||||
|
hex: Crypto.util.bytesToHex(payload),
|
||||||
|
type: 'bch',
|
||||||
|
bytes: payload
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,13 +30,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var user_id, user_public, user_private;
|
var user_id, user_public, user_private, user_loginID;
|
||||||
const user = floDapps.user = {
|
const user = floDapps.user = {
|
||||||
get id() {
|
get id() {
|
||||||
if (!user_id)
|
if (!user_id)
|
||||||
throw "User not logged in";
|
throw "User not logged in";
|
||||||
return user_id;
|
return user_id;
|
||||||
},
|
},
|
||||||
|
get loginID() {
|
||||||
|
if (!user_loginID)
|
||||||
|
return user_id; // Default to FLO ID if not set
|
||||||
|
return user_loginID;
|
||||||
|
},
|
||||||
get public() {
|
get public() {
|
||||||
if (!user_public)
|
if (!user_public)
|
||||||
throw "User not logged in";
|
throw "User not logged in";
|
||||||
@ -97,7 +102,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
clear() {
|
clear() {
|
||||||
user_id = user_public = user_private = undefined;
|
user_id = user_public = user_private = user_loginID = undefined;
|
||||||
user_priv_raw = aes_key = undefined;
|
user_priv_raw = aes_key = undefined;
|
||||||
delete user.contacts;
|
delete user.contacts;
|
||||||
delete user.pubKeys;
|
delete user.pubKeys;
|
||||||
@ -450,8 +455,80 @@
|
|||||||
try {
|
try {
|
||||||
user_public = floCrypto.getPubKeyHex(privKey);
|
user_public = floCrypto.getPubKeyHex(privKey);
|
||||||
user_id = floCrypto.getAddress(privKey);
|
user_id = floCrypto.getAddress(privKey);
|
||||||
if (startUpOptions.cloud)
|
|
||||||
floCloudAPI.user(user_id, privKey); //Set user for floCloudAPI
|
// Derive Login ID based on Private Key Type
|
||||||
|
try {
|
||||||
|
if (privKey.length === 64) { // Hex Key -> Ethereum
|
||||||
|
if (typeof floEthereum !== 'undefined' && floEthereum.ethAddressFromPrivateKey) {
|
||||||
|
user_loginID = floEthereum.ethAddressFromPrivateKey(privKey);
|
||||||
|
}
|
||||||
|
} else { // WIF Key
|
||||||
|
let decode = bitjs.Base58.decode(privKey);
|
||||||
|
let version = decode[0];
|
||||||
|
switch (version) {
|
||||||
|
case 128: // BTC (Mainnet WIF)
|
||||||
|
case 0: // BTC (Address Version - fallback)
|
||||||
|
if (typeof btcOperator !== 'undefined')
|
||||||
|
user_loginID = new Bitcoin.ECKey(privKey).getBitcoinAddress();
|
||||||
|
break;
|
||||||
|
case 176: // LTC (Mainnet WIF)
|
||||||
|
case 48: // LTC (Address Version - fallback)
|
||||||
|
if (typeof convertWIFtoLitecoinAddress === 'function')
|
||||||
|
user_loginID = convertWIFtoLitecoinAddress(privKey);
|
||||||
|
break;
|
||||||
|
case 163: // FLO (Mainnet WIF 0xA3)
|
||||||
|
case 35: // FLO (Address Version)
|
||||||
|
user_loginID = user_id;
|
||||||
|
break;
|
||||||
|
case 158: // DOGE (Mainnet WIF 0x9E)
|
||||||
|
case 30: // DOGE (Address Version)
|
||||||
|
if (typeof convertWIFtoDogeAddress === 'function')
|
||||||
|
user_loginID = convertWIFtoDogeAddress(privKey);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Manual overrides/checks for keys that might not match standard WIF versions or rely on library detection
|
||||||
|
if (!user_loginID) {
|
||||||
|
// Try XRP
|
||||||
|
if (typeof convertWIFtoXrpAddress === 'function') {
|
||||||
|
let xrpAddr = convertWIFtoXrpAddress(privKey);
|
||||||
|
if (xrpAddr) user_loginID = xrpAddr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Login ID derivation failed:", e);
|
||||||
|
}
|
||||||
|
if (!user_loginID) user_loginID = user_id; // Fallback
|
||||||
|
|
||||||
|
if (startUpOptions.cloud) {
|
||||||
|
// Ensure we pass a format floCloudAPI/floCrypto accepts.
|
||||||
|
// For DOGE/LTC WIFs, Bitcoin.ECKey might reject the version byte if gloabls aren't set.
|
||||||
|
// safer to pass RAW HEX Private Key to floCloudAPI.
|
||||||
|
let cloudPrivKey = privKey;
|
||||||
|
try {
|
||||||
|
if (privKey.length > 50 && typeof bitjs !== 'undefined') { // valid WIF length check
|
||||||
|
let decode = bitjs.Base58.decode(privKey);
|
||||||
|
if (decode && decode.length) {
|
||||||
|
// Remove 4-byte checksum
|
||||||
|
let raw = decode.slice(0, decode.length - 4);
|
||||||
|
// Remove 1-byte Version
|
||||||
|
raw.shift();
|
||||||
|
// Check compression flag (last byte 0x01) if length is 33
|
||||||
|
if (raw.length === 33 && raw[32] === 1) {
|
||||||
|
raw.pop();
|
||||||
|
}
|
||||||
|
// Convert to Hex
|
||||||
|
if (raw.length === 32)
|
||||||
|
cloudPrivKey = Crypto.util.bytesToHex(raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("WIF decode for cloud failed, using original:", e);
|
||||||
|
}
|
||||||
|
floCloudAPI.user(user_loginID, cloudPrivKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
user_priv_wrap = () => checkIfPinRequired(key);
|
user_priv_wrap = () => checkIfPinRequired(key);
|
||||||
let n = floCrypto.randInt(12, 20);
|
let n = floCrypto.randInt(12, 20);
|
||||||
aes_key = floCrypto.randString(n);
|
aes_key = floCrypto.randString(n);
|
||||||
|
|||||||
@ -18,7 +18,8 @@
|
|||||||
direct: (d, e) => console.log(d, e),
|
direct: (d, e) => console.log(d, e),
|
||||||
chats: (c) => console.log(c),
|
chats: (c) => console.log(c),
|
||||||
mails: (m) => console.log(m),
|
mails: (m) => console.log(m),
|
||||||
marked: (r) => console.log(r)
|
marked: (r) => console.log(r),
|
||||||
|
onChatMigrated: (oldID, newID) => console.log(`Migrated ${oldID} to ${newID}`)
|
||||||
};
|
};
|
||||||
rmMessenger.renderUI = {};
|
rmMessenger.renderUI = {};
|
||||||
Object.defineProperties(rmMessenger.renderUI, {
|
Object.defineProperties(rmMessenger.renderUI, {
|
||||||
@ -39,6 +40,9 @@
|
|||||||
},
|
},
|
||||||
marked: {
|
marked: {
|
||||||
set: ui_fn => UI.marked = ui_fn
|
set: ui_fn => UI.marked = ui_fn
|
||||||
|
},
|
||||||
|
onChatMigrated: {
|
||||||
|
set: ui_fn => UI.onChatMigrated = ui_fn
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -74,13 +78,58 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Validate any blockchain address (all 19 supported chains)
|
||||||
|
function isValidBlockchainAddress(address) {
|
||||||
|
if (!address || typeof address !== 'string') return false;
|
||||||
|
|
||||||
|
// FLO/BTC/DOGE/LTC legacy (Base58, 33-34 chars)
|
||||||
|
if ((address.length === 33 || address.length === 34) && /^[1-9A-HJ-NP-Za-km-z]+$/.test(address)) {
|
||||||
|
return floCrypto.validateAddr(address) || true;
|
||||||
|
}
|
||||||
|
// Ethereum/EVM addresses (0x prefix, 40 hex chars) - ETH, AVAX, BSC, MATIC, ARB, OP
|
||||||
|
if (/^0x[a-fA-F0-9]{40}$/.test(address)) return true;
|
||||||
|
// SUI addresses (0x prefix, 64 hex chars)
|
||||||
|
if (/^0x[a-fA-F0-9]{64}$/.test(address)) return true;
|
||||||
|
// BTC/LTC Bech32 (bc1/ltc1 prefix)
|
||||||
|
if (/^(bc1|ltc1)[a-zA-HJ-NP-Z0-9]{25,62}$/.test(address)) return true;
|
||||||
|
// Solana (Base58, 43-44 chars)
|
||||||
|
if ((address.length === 43 || address.length === 44) && /^[1-9A-HJ-NP-Za-km-z]+$/.test(address)) return true;
|
||||||
|
// XRP (r-prefix, 25-35 chars)
|
||||||
|
if (address.startsWith('r') && address.length >= 25 && address.length <= 35 && /^r[a-zA-Z0-9]+$/.test(address)) return true;
|
||||||
|
// TRON (T-prefix, 34 chars)
|
||||||
|
if (address.startsWith('T') && address.length === 34 && /^T[a-zA-Z0-9]+$/.test(address)) return true;
|
||||||
|
// Cardano (addr1 prefix, Bech32)
|
||||||
|
if (address.startsWith('addr1') && address.length > 50) return true;
|
||||||
|
// Polkadot (SS58, starts with 1, 47-48 chars)
|
||||||
|
if (address.startsWith('1') && (address.length === 47 || address.length === 48) && /^[a-zA-Z0-9]+$/.test(address)) return true;
|
||||||
|
// TON (Base64 URL-safe, 48 chars)
|
||||||
|
if (address.length === 48 && /^[A-Za-z0-9_-]+$/.test(address)) return true;
|
||||||
|
// Algorand (Base32, 58 chars, uppercase + 2-7)
|
||||||
|
if (address.length === 58 && /^[A-Z2-7]+$/.test(address)) return true;
|
||||||
|
// Stellar (G-prefix, Base32, 56 chars)
|
||||||
|
if (address.startsWith('G') && address.length === 56 && /^G[A-Z2-7]+$/.test(address)) return true;
|
||||||
|
// Bitcoin Cash CashAddr (q-prefix)
|
||||||
|
if (address.startsWith('q') && address.length >= 34 && address.length <= 45 && /^q[a-z0-9]+$/.test(address)) return true;
|
||||||
|
// HBAR (0.0.xxxxx format)
|
||||||
|
if (/^0\.0\.\d+$/.test(address)) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function sendRaw(message, recipient, type, encrypt = null, comment = undefined) {
|
function sendRaw(message, recipient, type, encrypt = null, comment = undefined) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!floCrypto.validateAddr(recipient))
|
if (!isValidBlockchainAddress(recipient))
|
||||||
return reject("Invalid Recipient");
|
return reject("Invalid Recipient");
|
||||||
|
|
||||||
if ([true, null].includes(encrypt)) {
|
if ([true, null].includes(encrypt)) {
|
||||||
let r_pubKey = floDapps.user.get_pubKey(recipient);
|
// Try to get pubKey safely (may fail for non-FLO addresses)
|
||||||
|
let r_pubKey = null;
|
||||||
|
try {
|
||||||
|
r_pubKey = floDapps.user.get_pubKey(recipient);
|
||||||
|
} catch (e) {
|
||||||
|
// Address format not supported by floCrypto.decodeAddr
|
||||||
|
r_pubKey = floGlobals.pubKeys ? floGlobals.pubKeys[recipient] : null;
|
||||||
|
}
|
||||||
if (r_pubKey)
|
if (r_pubKey)
|
||||||
message = floCrypto.encryptData(message, r_pubKey);
|
message = floCrypto.encryptData(message, r_pubKey);
|
||||||
else if (encrypt === true)
|
else if (encrypt === true)
|
||||||
@ -89,8 +138,46 @@
|
|||||||
let options = {
|
let options = {
|
||||||
receiverID: recipient,
|
receiverID: recipient,
|
||||||
}
|
}
|
||||||
if (comment)
|
if (comment) {
|
||||||
options.comment = comment
|
options.comment = comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're logged in with an alt chain, embed it in the comment for migration mapping
|
||||||
|
try {
|
||||||
|
let activeChain = localStorage.getItem(`${floGlobals.application}#activeChain`);
|
||||||
|
if (activeChain && activeChain !== 'FLO') {
|
||||||
|
let proxyID = null;
|
||||||
|
switch (activeChain) {
|
||||||
|
case 'ETH':
|
||||||
|
case 'AVAX':
|
||||||
|
case 'BSC':
|
||||||
|
case 'MATIC':
|
||||||
|
case 'ARB':
|
||||||
|
case 'OP':
|
||||||
|
case 'HBAR':
|
||||||
|
proxyID = floEthereum.ethAddressFromCompressedPublicKey(user.public); break;
|
||||||
|
case 'BTC': proxyID = floGlobals.myBtcID; break;
|
||||||
|
case 'BCH': proxyID = floGlobals.myBchID; break;
|
||||||
|
case 'XRP': proxyID = floGlobals.myXrpID; break;
|
||||||
|
case 'SUI': proxyID = floGlobals.mySuiID; break;
|
||||||
|
case 'TON': proxyID = floGlobals.myTonID; break;
|
||||||
|
case 'TRON': proxyID = floGlobals.myTronID; break;
|
||||||
|
case 'DOGE': proxyID = floGlobals.myDogeID; break;
|
||||||
|
case 'LTC': proxyID = floGlobals.myLtcID; break;
|
||||||
|
case 'DOT': proxyID = floGlobals.myDotID; break;
|
||||||
|
case 'ALGO': proxyID = floGlobals.myAlgoID; break;
|
||||||
|
case 'XLM': proxyID = floGlobals.myXlmID; break;
|
||||||
|
case 'SOL': proxyID = floGlobals.mySolID; break;
|
||||||
|
case 'ADA': proxyID = floGlobals.myAdaID; break;
|
||||||
|
}
|
||||||
|
if (proxyID && proxyID !== user.id) {
|
||||||
|
options.comment = (options.comment ? options.comment + "|" : "") + "FROM_ALT:" + proxyID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Could not append FROM_ALT tag", e);
|
||||||
|
}
|
||||||
|
|
||||||
floCloudAPI.sendApplicationData(message, type, options)
|
floCloudAPI.sendApplicationData(message, type, options)
|
||||||
.then(result => resolve(result))
|
.then(result => resolve(result))
|
||||||
.catch(error => reject(error))
|
.catch(error => reject(error))
|
||||||
@ -316,7 +403,7 @@
|
|||||||
|
|
||||||
const processData = {};
|
const processData = {};
|
||||||
processData.direct = function () {
|
processData.direct = function () {
|
||||||
return (unparsed, newInbox) => {
|
return async (unparsed, newInbox) => {
|
||||||
//store the pubKey if not stored already
|
//store the pubKey if not stored already
|
||||||
floDapps.storePubKey(unparsed.senderID, unparsed.pubKey);
|
floDapps.storePubKey(unparsed.senderID, unparsed.pubKey);
|
||||||
if (_loaded.blocked.has(unparsed.senderID) && unparsed.type !== "REVOKE_KEY")
|
if (_loaded.blocked.has(unparsed.senderID) && unparsed.type !== "REVOKE_KEY")
|
||||||
@ -326,6 +413,51 @@
|
|||||||
let vc = unparsed.vectorClock;
|
let vc = unparsed.vectorClock;
|
||||||
switch (unparsed.type) {
|
switch (unparsed.type) {
|
||||||
case "MESSAGE": { //process as message
|
case "MESSAGE": { //process as message
|
||||||
|
let vc = unparsed.vectorClock;
|
||||||
|
|
||||||
|
// Chat Migration Logic
|
||||||
|
if (unparsed.comment && unparsed.comment.includes("FROM_ALT:")) {
|
||||||
|
let altID = unparsed.comment.split("FROM_ALT:")[1].split("|")[0]; // Extract the alt ID
|
||||||
|
if (altID && altID !== unparsed.senderID) {
|
||||||
|
// Check if an existing chat with this alt ID exists
|
||||||
|
if (_loaded.chats[altID] !== undefined) {
|
||||||
|
console.log(`Migrating chat history from ${altID} to ${unparsed.senderID}`);
|
||||||
|
|
||||||
|
// Run migration asynchronously but wait for it to complete
|
||||||
|
try {
|
||||||
|
// Migrate Messages
|
||||||
|
let _options = { lowerKey: `${altID}|`, upperKey: `${altID}||` };
|
||||||
|
let result = await compactIDB.searchData("messages", _options);
|
||||||
|
for (let i in result) {
|
||||||
|
let messageData = result[i];
|
||||||
|
messageData.floID = unparsed.senderID;
|
||||||
|
let oldVc = i.split("|")[1];
|
||||||
|
await compactIDB.addData("messages", messageData, `${unparsed.senderID}|${oldVc}`).catch(e => console.warn(e));
|
||||||
|
await compactIDB.removeData("messages", i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate Contacts
|
||||||
|
if (floGlobals.contacts[altID]) {
|
||||||
|
let contactName = floGlobals.contacts[altID];
|
||||||
|
floDapps.storeContact(unparsed.senderID, contactName);
|
||||||
|
await compactIDB.removeData("contacts", altID, floDapps.user.db_name);
|
||||||
|
delete floGlobals.contacts[altID];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete old chat reference
|
||||||
|
delete _loaded.chats[altID];
|
||||||
|
await compactIDB.removeData("chats", altID);
|
||||||
|
|
||||||
|
// Trigger UI update if available
|
||||||
|
if (UI.onChatMigrated) UI.onChatMigrated(altID, unparsed.senderID);
|
||||||
|
if (UI.chats) await UI.chats(getChatOrder());
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Migration failed:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let dm = {
|
let dm = {
|
||||||
time: unparsed.time,
|
time: unparsed.time,
|
||||||
floID: unparsed.senderID,
|
floID: unparsed.senderID,
|
||||||
@ -333,7 +465,12 @@
|
|||||||
message: encrypt(unparsed.message)
|
message: encrypt(unparsed.message)
|
||||||
}
|
}
|
||||||
console.debug(dm, `${dm.floID}|${vc}`);
|
console.debug(dm, `${dm.floID}|${vc}`);
|
||||||
compactIDB.addData("messages", Object.assign({}, dm), `${dm.floID}|${vc}`)
|
try {
|
||||||
|
await compactIDB.addData("messages", Object.assign({}, dm), `${dm.floID}|${vc}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Message already exists (skipping UI push):", e);
|
||||||
|
break; // Deduplicate: don't push to UI if we already processed it (e.g. self-messaging)
|
||||||
|
}
|
||||||
_loaded.chats[dm.floID] = parseInt(vc)
|
_loaded.chats[dm.floID] = parseInt(vc)
|
||||||
compactIDB.writeData("chats", parseInt(vc), dm.floID)
|
compactIDB.writeData("chats", parseInt(vc), dm.floID)
|
||||||
dm.message = unparsed.message;
|
dm.message = unparsed.message;
|
||||||
@ -431,13 +568,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestDirectInbox() {
|
const requestDirectInbox = rmMessenger.reconnectInbox = function () {
|
||||||
if (directConnID.length) { //close existing request connection (if any)
|
if (directConnID.length) { //close existing request connection (if any)
|
||||||
directConnID.forEach(id => floCloudAPI.closeRequest(id));
|
directConnID.forEach(id => floCloudAPI.closeRequest(id));
|
||||||
directConnID = [];
|
directConnID = [];
|
||||||
}
|
}
|
||||||
const parseData = processData.direct();
|
const parseData = processData.direct();
|
||||||
let callbackFn = function (dataSet, error) {
|
let callbackFn = async function (dataSet, error) {
|
||||||
if (error)
|
if (error)
|
||||||
return console.error(error)
|
return console.error(error)
|
||||||
let newInbox = {
|
let newInbox = {
|
||||||
@ -449,9 +586,11 @@
|
|||||||
keyrevoke: [],
|
keyrevoke: [],
|
||||||
pipeline: {}
|
pipeline: {}
|
||||||
}
|
}
|
||||||
for (let vc in dataSet) {
|
// Await processing in order according to vector clocks
|
||||||
|
let sortedVCs = Object.keys(dataSet).sort((a, b) => parseInt(a) - parseInt(b));
|
||||||
|
for (let vc of sortedVCs) {
|
||||||
try {
|
try {
|
||||||
parseData(dataSet[vc], newInbox);
|
await parseData(dataSet[vc], newInbox);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
//if (error !== "blocked-user")
|
//if (error !== "blocked-user")
|
||||||
console.log(error);
|
console.log(error);
|
||||||
@ -464,19 +603,83 @@
|
|||||||
console.debug(newInbox);
|
console.debug(newInbox);
|
||||||
UI.direct(newInbox)
|
UI.direct(newInbox)
|
||||||
}
|
}
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
const promises = [
|
// All blockchain address IDs to listen on
|
||||||
|
let activeChain = localStorage.getItem(`${floGlobals.application}#activeChain`);
|
||||||
|
const blockchainAddressIDs = [floGlobals.myFloID || user.id]; // Always listen to FLO address (primary)
|
||||||
|
|
||||||
|
if (!activeChain) {
|
||||||
|
try {
|
||||||
|
let privKey = floDapps.user.private;
|
||||||
|
if (privKey instanceof Promise) privKey = await privKey;
|
||||||
|
if (typeof privKey === 'string' && privKey.length > 0) {
|
||||||
|
if (privKey.startsWith('suiprivkey1')) activeChain = 'SUI';
|
||||||
|
else if (privKey.startsWith('s')) activeChain = 'XRP';
|
||||||
|
else if (privKey.startsWith('Q') || privKey.startsWith('6')) activeChain = 'DOGE';
|
||||||
|
else if (privKey.startsWith('T') && privKey.length === 51) activeChain = 'LTC';
|
||||||
|
else if (privKey.startsWith('K') || privKey.startsWith('L') || privKey.startsWith('5')) activeChain = 'BTC';
|
||||||
|
else if (privKey.startsWith('S') && privateKey.length === 56) activeChain = 'XLM';
|
||||||
|
else if (privKey.startsWith('R') || privKey.startsWith('c') || privKey.startsWith('p')) activeChain = 'FLO';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Could not deduce fallback activeChain", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeChain) {
|
||||||
|
const addIfValid = (id) => { if (id && !blockchainAddressIDs.includes(id)) blockchainAddressIDs.push(id) };
|
||||||
|
|
||||||
|
switch (activeChain) {
|
||||||
|
case 'ETH':
|
||||||
|
case 'AVAX':
|
||||||
|
case 'BSC':
|
||||||
|
case 'MATIC':
|
||||||
|
case 'ARB':
|
||||||
|
case 'OP':
|
||||||
|
case 'HBAR':
|
||||||
|
addIfValid(floEthereum.ethAddressFromCompressedPublicKey(user.public));
|
||||||
|
break;
|
||||||
|
case 'BTC': addIfValid(floGlobals.myBtcID); break;
|
||||||
|
case 'BCH': addIfValid(floGlobals.myBchID); break;
|
||||||
|
case 'XRP': addIfValid(floGlobals.myXrpID); break;
|
||||||
|
case 'SUI': addIfValid(floGlobals.mySuiID); break;
|
||||||
|
case 'TON': addIfValid(floGlobals.myTonID); break;
|
||||||
|
case 'TRON': addIfValid(floGlobals.myTronID); break;
|
||||||
|
case 'DOGE': addIfValid(floGlobals.myDogeID); break;
|
||||||
|
case 'LTC': addIfValid(floGlobals.myLtcID); break;
|
||||||
|
case 'DOT': addIfValid(floGlobals.myDotID); break;
|
||||||
|
case 'ALGO': addIfValid(floGlobals.myAlgoID); break;
|
||||||
|
case 'XLM': addIfValid(floGlobals.myXlmID); break;
|
||||||
|
case 'SOL': addIfValid(floGlobals.mySolID); break;
|
||||||
|
case 'ADA': addIfValid(floGlobals.myAdaID); break;
|
||||||
|
case 'FLO': break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: listen to all derived addresses if no active chain is set
|
||||||
|
const allDerived = [
|
||||||
|
floEthereum.ethAddressFromCompressedPublicKey(user.public),
|
||||||
|
floGlobals.myBtcID, floGlobals.myAvaxID, floGlobals.myBscID,
|
||||||
|
floGlobals.myMaticID, floGlobals.myArbID, floGlobals.myOpID,
|
||||||
|
floGlobals.myHbarID, floGlobals.myXrpID,
|
||||||
|
floGlobals.mySuiID, floGlobals.myTonID, floGlobals.myTronID,
|
||||||
|
floGlobals.myDogeID, floGlobals.myLtcID, floGlobals.myBchID,
|
||||||
|
floGlobals.myDotID, floGlobals.myAlgoID, floGlobals.myXlmID,
|
||||||
|
floGlobals.mySolID, floGlobals.myAdaID
|
||||||
|
];
|
||||||
|
allDerived.forEach(id => {
|
||||||
|
if (id && !blockchainAddressIDs.includes(id)) {
|
||||||
|
blockchainAddressIDs.push(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const promises = blockchainAddressIDs.map(receiverID =>
|
||||||
floCloudAPI.requestApplicationData(null, {
|
floCloudAPI.requestApplicationData(null, {
|
||||||
receiverID: user.id,
|
receiverID: receiverID,
|
||||||
lowerVectorClock: _loaded.appendix.lastReceived + 1,
|
|
||||||
callback: callbackFn
|
|
||||||
}),
|
|
||||||
floCloudAPI.requestApplicationData(null, {
|
|
||||||
receiverID: floEthereum.ethAddressFromCompressedPublicKey(user.public),
|
|
||||||
lowerVectorClock: _loaded.appendix.lastReceived + 1,
|
lowerVectorClock: _loaded.appendix.lastReceived + 1,
|
||||||
callback: callbackFn
|
callback: callbackFn
|
||||||
})
|
})
|
||||||
]
|
);
|
||||||
Promise.all(promises).then(connectionIds => {
|
Promise.all(promises).then(connectionIds => {
|
||||||
directConnID = [...directConnID, ...connectionIds];
|
directConnID = [...directConnID, ...connectionIds];
|
||||||
resolve("Direct Inbox connected");
|
resolve("Direct Inbox connected");
|
||||||
@ -513,7 +716,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
rmMessenger.storeContact = function (floID, name) {
|
rmMessenger.storeContact = function (floID, name) {
|
||||||
return floDapps.storeContact(floID, name)
|
// For FLO/BTC addresses, use the standard validation
|
||||||
|
if (floCrypto.validateAddr(floID)) {
|
||||||
|
return floDapps.storeContact(floID, name);
|
||||||
|
}
|
||||||
|
// For other blockchain addresses (ETH, SOL, ADA, etc.), store directly
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
compactIDB.writeData("contacts", name, floID, floDapps.user.db_name).then(result => {
|
||||||
|
floGlobals.contacts[floID] = name;
|
||||||
|
resolve("Contact stored");
|
||||||
|
}).catch(error => reject(error));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadDataFromIDB = function (defaultList = true) {
|
const loadDataFromIDB = function (defaultList = true) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user