Auto-update messenger

This commit is contained in:
ranchimall 2026-03-06 20:26:12 +00:00
parent 99af2eddd4
commit 0dba4e20ec
6 changed files with 2246 additions and 85 deletions

View File

@ -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>

File diff suppressed because it is too large Load Diff

View File

@ -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))
} }
}) })

View File

@ -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;
}
} }
} }

View File

@ -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);

View File

@ -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) {