feat: Expand blockchain address support to multiple chains and enhancing UI for address validation and chat migration.

This commit is contained in:
void-57 2026-02-23 01:01:56 +05:30
parent d6fd63dace
commit 80fa542bf9
5 changed files with 679 additions and 72 deletions

View File

@ -36,7 +36,7 @@
<script src="scripts/floDapps.js"></script>
<script src="scripts/keccak.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">
@ -89,7 +89,7 @@
floGlobals.myMaticID = 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;
@ -115,10 +115,29 @@
rmMessenger.renderUI.pipeline = renderPipelineUI;
rmMessenger.renderUI.mails = m => renderMailList(m, false);
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
rmMessenger.init().then(async result => {
console.log(result);
// Check if account is password-secured by checking the stored secret length
try {
const indexArr = localStorage.getItem(`${floGlobals.application}#privKey`);
@ -134,7 +153,7 @@
} 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) {
@ -187,7 +206,11 @@
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
setBgImage();
routeTo(window.location.hash, { firstLoad: true })
@ -231,6 +254,20 @@
</div>
</sm-form>
</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>
</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="secondary_pages" class="page hidden">
<header class="flex align-center gap-1 space-between">
@ -828,8 +865,8 @@
<h4>Add contact</h4>
</header>
<sm-form>
<sm-input id="add_contact_floID" data-address placeholder="FLO/BTC/ETH address" error-text="Invalid address"
animate autofocus required></sm-input>
<sm-input id="add_contact_floID" data-address placeholder="Enter any blockchain address"
error-text="Invalid address" animate autofocus 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>
</sm-form>
@ -936,7 +973,7 @@
<div id="contacts_container" class="observe-empty-state"></div>
<div class="empty-state">
<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>
@ -1539,7 +1576,7 @@
const contacts = []
const groupID = getRef('edit_group_button').dataset.groupId;
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))
}
}
@ -1779,18 +1816,79 @@
}
function getContactName(contactAddress) {
if (floDapps.user.get_contact(contactAddress))
return floDapps.user.get_contact(contactAddress)
else if (rmMessenger.groups[contactAddress])
// Check floGlobals.contacts directly first (handles all blockchain addresses)
if (floGlobals.contacts && floGlobals.contacts[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
else if (floCrypto.isSameAddr(contactAddress, floDapps.user.id))
return 'You'
else
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) {
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("load", () => {
document.body.classList.remove('hidden')
@ -1798,7 +1896,7 @@
if (!value) return { isValid: false, errorText: 'Please enter an address' }
return {
isValid: isValidEthereumAddress(value) || floCrypto.validateAddr(value),
isValid: isValidBlockchainAddress(value),
errorText: `Invalid address.`
}
})
@ -2474,7 +2572,7 @@
target.type = target.type === 'password' ? 'text' : 'password';
target.focusIn()
}
// Get private key by decrypting with password
async function getPrivKeyWithPassword(password) {
return new Promise(async (resolve, reject) => {
@ -2510,7 +2608,7 @@
}
});
}
// Derive private-key-dependent blockchain addresses (XRP, SUI, TON, TRON, DOGE, DOT, ALGO) with password
async function derivePrivKeyAddresses() {
try {
@ -2592,7 +2690,16 @@
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) {
return new Promise((resolve, reject) => {
routeTo(window.location.hash)
@ -2625,10 +2732,37 @@
if (!generalPages.find(page => window.location.hash.includes(page))) {
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();
if (/^[0-9a-fA-F]{64}$/.test(privateKey)) //if hex private key might be ethereum
privateKey = coinjs.privkey2wif(privateKey);
let activeChain = null;
if (/^[0-9a-fA-F]{64}$/.test(privateKey)) { //if hex private key might be ethereum, ADA, SOL, TRON, DOT
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 (privateKey.startsWith('suiprivkey1')) activeChain = 'SUI';
else if (privateKey.startsWith('s')) activeChain = 'XRP';
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') ) activeChain = 'BTC';
else if (privateKey.startsWith('S') && privateKey.length === 56) activeChain = 'XLM';
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;
@ -3405,6 +3539,14 @@
}
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 = {
duration: 300,
easing: 'ease',
@ -3414,8 +3556,8 @@
const { category, floID, time, message, sender, groupID, admin, name, pipeID, unconfirmed, type } = messagesData[messageId]
const chatAddress = floID || groupID || pipeID
// code to run if a chat is opened
if (activeChat && floCrypto.isSameAddr(activeChat.address, chatAddress) || floCrypto.isSameAddr(floCrypto.getFloID(getEthPubKey(activeChat.address)), chatAddress)) {
if (sentByMe || type === 'TRANSACTION' || !sender || !floCrypto.isSameAddr(sender, floDapps.user.id)) {
if (activeChat && (isSameAddrSafe(activeChat.address, chatAddress) || isSameAddrSafe(floCrypto.getFloID(getEthPubKey(activeChat.address)), chatAddress))) {
if (sentByMe || type === 'TRANSACTION' || !sender || !isSameAddrSafe(sender, floDapps.user.id)) {
const messageBody = render.messageBubble(messagesData[messageId]);
getRef('messages_container').append(messageBody);
}
@ -3423,7 +3565,7 @@
scrollToBottom()
}
// 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')) {
getRef('warn_no_encryption').after(
html.node`
@ -3445,7 +3587,7 @@
}
// move chat card to top if it is not already there
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)
chatCard.remove()
getRef('chats_list').prepend(cloneContact)
@ -3491,7 +3633,7 @@
if (chatCard.querySelector('.time'))
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)
getRef('scroll_to_bottom').classList.add('new-message')
else {
@ -3857,7 +3999,7 @@
const selctedPubKeys = [...selectedMembers].map(id => {
if (id === floDapps.user.id)
return floDapps.user.public
return floDapps.user.get_pubKey(id)
return getContactPubKey(id)
});
const minRequired = parseInt(getRef('min_sign_required').value.trim());
const label = getRef('multisig_label').value.trim();
@ -4152,12 +4294,24 @@
notify(`Contact already saved`, 'error')
return
}
// check whether an equivalent BTC/FLO address is already saved
const addrInFlo = floCrypto.toFloID(addressToSave);
const addrInBtc = btcOperator.convert.legacy2bech(addrInFlo);
const addrInEth = floGlobals.pubKeys[addressToSave] ? floEthereum.ethAddressFromCompressedPublicKey(floGlobals.pubKeys[addressToSave]) : null;
if (floGlobals.contacts.hasOwnProperty(addrInFlo) || floGlobals.contacts.hasOwnProperty(addrInBtc) || floGlobals.contacts.hasOwnProperty(addrInEth)) {
notify(`Equivalent Address is already saved as ${getContactName(addrInFlo)}`, 'error');
// check whether an equivalent BTC/FLO address is already saved (only for compatible address types)
let addrInFlo = null, addrInBtc = null, addrInEth = null;
try {
// Only attempt conversion for legacy Base58 addresses (FLO/BTC format)
if ((addressToSave.length === 33 || addressToSave.length === 34) && /^[1-9A-HJ-NP-Za-km-z]+$/.test(addressToSave)) {
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;
}
rmMessenger.storeContact(addressToSave, name).then(result => {
@ -4199,7 +4353,7 @@
}
for (const floID in floGlobals.contacts) {
if (getAddressType(floID) !== 'plain') continue;
if (floDapps.user.get_pubKey(floID)) {
if (getContactPubKey(floID)) {
contacts.push(render.selectableContact(floID))
} else {
const hasSentRequest = skipSendingRequest.has(floID)
@ -4331,12 +4485,19 @@
let floChatAddress
let btcChatAddress
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) {
btcChatAddress = btcOperator.convert.legacy2bech(floChatAddress);
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))
Promise.all(promises)
.then((chats) => {
@ -4362,7 +4523,8 @@
batchSize: 20,
onEnd: () => {
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">
<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
@ -5601,4 +5763,4 @@
</script>
</body>
</html>
</html>

View File

@ -392,7 +392,9 @@
if (!address)
return;
var bytes;
if (address.length == 33 || address.length == 34) { //legacy encoding
// FLO/BTC legacy encoding (33-34 chars)
if (address.length == 33 || address.length == 34) {
let decode = bitjs.Base58.decode(address);
bytes = decode.slice(0, decode.length - 4);
let checksum = decode.slice(decode.length - 4),
@ -403,7 +405,9 @@
});
hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3] ?
bytes = undefined : bytes.shift();
} else if (!address.startsWith("0x") && address.length == 42 || address.length == 62) { //bech encoding
}
// BTC/LTC Bech32 encoding (42 or 62 chars, not starting with 0x)
else if (!address.startsWith("0x") && (address.length == 42 || address.length == 62) && !address.startsWith("addr1")) {
if (typeof coinjs !== 'function')
throw "library missing (lib_btc.js)";
let decode = coinjs.bech32_decode(address);
@ -414,14 +418,154 @@
if (address.length == 62) //for long bech, aggregate once more to get 160 bit
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), {
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); }
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)
throw "Invalid address: " + address;
@ -535,9 +679,17 @@
//send any message to supernode cloud storage
const sendApplicationData = floCloudAPI.sendApplicationData = function (message, type, options = {}) {
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 = {
senderID: user.id,
receiverID: options.receiverID || DEFAULT.adminID,
receiverID: serverReceiverID,
pubKey: user.public,
message: encodeMessage(message),
time: Date.now(),
@ -548,7 +700,7 @@
let hashcontent = ["receiverID", "time", "application", "type", "message", "comment"]
.map(d => data[d]).join("|")
data.sign = user.sign(hashcontent);
singleRequest(data.receiverID, data)
singleRequest(originalReceiverID, data)
.then(result => resolve(result))
.catch(error => reject(error))
})
@ -557,8 +709,16 @@
//request any data from supernode cloud
const requestApplicationData = floCloudAPI.requestApplicationData = function (type, options = {}) {
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 = {
receiverID: options.receiverID || DEFAULT.adminID,
receiverID: serverReceiverID,
senderID: options.senderID || undefined,
application: options.application || DEFAULT.application,
type: type,
@ -571,7 +731,7 @@
}
if (options.callback instanceof Function) {
liveRequest(request.receiverID, request, options.callback)
liveRequest(originalReceiverID, request, options.callback)
.then(result => resolve(result))
.catch(error => reject(error))
} else {
@ -580,7 +740,7 @@
time: Date.now(),
request
};
singleRequest(request.receiverID, request, options.method || "GET")
singleRequest(originalReceiverID, request, options.method || "GET")
.then(data => resolve(data)).catch(error => reject(error))
}
})

View File

@ -431,8 +431,8 @@
} else
return null;
} else if ((address.length == 42 && address.startsWith("0x")) || (address.length == 40 && !address.startsWith("0x"))) { //Ethereum Address
if (address.startsWith("0x")) { address = address.substring(2); }
let bytes = Crypto.util.hexToBytes(address);
if (address.startsWith("0x")) { address = address.substring(2); }
let bytes = Crypto.util.hexToBytes(address);
return {
version: 1,
hex: address,

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 = {
get id() {
if (!user_id)
throw "User not logged in";
return user_id;
},
get loginID() {
if (!user_loginID)
return user_id; // Default to FLO ID if not set
return user_loginID;
},
get public() {
if (!user_public)
throw "User not logged in";
@ -97,7 +102,7 @@
}
},
clear() {
user_id = user_public = user_private = undefined;
user_id = user_public = user_private = user_loginID = undefined;
user_priv_raw = aes_key = undefined;
delete user.contacts;
delete user.pubKeys;
@ -450,8 +455,80 @@
try {
user_public = floCrypto.getPubKeyHex(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);
let n = floCrypto.randInt(12, 20);
aes_key = floCrypto.randString(n);

View File

@ -18,7 +18,8 @@
direct: (d, e) => console.log(d, e),
chats: (c) => console.log(c),
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 = {};
Object.defineProperties(rmMessenger.renderUI, {
@ -39,6 +40,9 @@
},
marked: {
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
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) {
return new Promise((resolve, reject) => {
if (!floCrypto.validateAddr(recipient))
if (!isValidBlockchainAddress(recipient))
return reject("Invalid Recipient");
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)
message = floCrypto.encryptData(message, r_pubKey);
else if (encrypt === true)
@ -89,8 +138,44 @@
let options = {
receiverID: recipient,
}
if (comment)
options.comment = comment
if (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 '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)
.then(result => resolve(result))
.catch(error => reject(error))
@ -316,7 +401,7 @@
const processData = {};
processData.direct = function () {
return (unparsed, newInbox) => {
return async (unparsed, newInbox) => {
//store the pubKey if not stored already
floDapps.storePubKey(unparsed.senderID, unparsed.pubKey);
if (_loaded.blocked.has(unparsed.senderID) && unparsed.type !== "REVOKE_KEY")
@ -326,6 +411,51 @@
let vc = unparsed.vectorClock;
switch (unparsed.type) {
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 = {
time: unparsed.time,
floID: unparsed.senderID,
@ -333,7 +463,12 @@
message: encrypt(unparsed.message)
}
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)
compactIDB.writeData("chats", parseInt(vc), dm.floID)
dm.message = unparsed.message;
@ -431,13 +566,13 @@
}
}
function requestDirectInbox() {
const requestDirectInbox = rmMessenger.reconnectInbox = function () {
if (directConnID.length) { //close existing request connection (if any)
directConnID.forEach(id => floCloudAPI.closeRequest(id));
directConnID = [];
}
const parseData = processData.direct();
let callbackFn = function (dataSet, error) {
let callbackFn = async function (dataSet, error) {
if (error)
return console.error(error)
let newInbox = {
@ -449,9 +584,11 @@
keyrevoke: [],
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 {
parseData(dataSet[vc], newInbox);
await parseData(dataSet[vc], newInbox);
} catch (error) {
//if (error !== "blocked-user")
console.log(error);
@ -464,19 +601,80 @@
console.debug(newInbox);
UI.direct(newInbox)
}
return new Promise((resolve, reject) => {
const promises = [
return new Promise(async (resolve, reject) => {
// All blockchain address IDs to listen on
let activeChain = localStorage.getItem(`${floGlobals.application}#activeChain`);
const blockchainAddressIDs = [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 '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.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, {
receiverID: user.id,
lowerVectorClock: _loaded.appendix.lastReceived + 1,
callback: callbackFn
}),
floCloudAPI.requestApplicationData(null, {
receiverID: floEthereum.ethAddressFromCompressedPublicKey(user.public),
receiverID: receiverID,
lowerVectorClock: _loaded.appendix.lastReceived + 1,
callback: callbackFn
})
]
);
Promise.all(promises).then(connectionIds => {
directConnID = [...directConnID, ...connectionIds];
resolve("Direct Inbox connected");
@ -513,7 +711,17 @@
}
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) {