From 0dba4e20ec299facefec4a4cd18b52e286a767f7 Mon Sep 17 00:00:00 2001 From: ranchimall Date: Fri, 6 Mar 2026 20:26:12 +0000 Subject: [PATCH] Auto-update messenger --- messenger/index.html | 759 +++++++++++++++- messenger/scripts/blockchainAddresses.js | 1019 ++++++++++++++++++++++ messenger/scripts/floCloudAPI.js | 191 +++- messenger/scripts/floCrypto.js | 22 +- messenger/scripts/floDapps.js | 85 +- messenger/scripts/messenger.js | 255 +++++- 6 files changed, 2246 insertions(+), 85 deletions(-) create mode 100644 messenger/scripts/blockchainAddresses.js diff --git a/messenger/index.html b/messenger/index.html index ac0e35a..bfb136f 100644 --- a/messenger/index.html +++ b/messenger/index.html @@ -18,6 +18,15 @@ } + + + + + + + + + @@ -27,8 +36,10 @@ - + + @@ -126,6 +283,46 @@ + +

Select Blockchain

+

Your private key format is used by multiple networks. Please select the correct blockchain:

+
+ + + + + + + + +
+
+ +
+
+ +

Select Blockchain

+

Your private key format is used by Bitcoin and Bitcoin Cash. Please select the correct blockchain for this + key:

+
+ + +
+
+ +
+
+ +

Select Blockchain

+

Your private key format is used by TON and Algorand. Please select the correct blockchain for this key:

+
+ + +
+
+ +
+
@@ -871,7 +1068,7 @@

No saved contacts.

-

Use 'Add contact' to add new FLO/BTC/ETH address as a contact.

+

Use 'Add contact' to add a new blockchain address as a contact.

+ ` : ''} + `}
@@ -3086,6 +3721,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', @@ -3095,8 +3738,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); } @@ -3104,7 +3747,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` @@ -3126,7 +3769,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) @@ -3172,7 +3815,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 { @@ -3538,7 +4181,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(); @@ -3826,19 +4469,31 @@ let addressToSave = getRef('add_contact_floID').value.trim(); let name = getRef('add_contact_name').value.trim(); 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 } if (floGlobals.contacts.hasOwnProperty(addressToSave)) { 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 => { @@ -3880,7 +4535,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) @@ -4012,12 +4667,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) => { @@ -4043,7 +4705,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` Conversation is encrypted @@ -5282,4 +5945,4 @@ - + \ No newline at end of file diff --git a/messenger/scripts/blockchainAddresses.js b/messenger/scripts/blockchainAddresses.js new file mode 100644 index 0000000..d708a91 --- /dev/null +++ b/messenger/scripts/blockchainAddresses.js @@ -0,0 +1,1019 @@ +/** + * Blockchain Address Derivation Utilities + * Derives addresses for multiple blockchains from a single FLO/BTC WIF private key + * + * Supported Blockchains: + * - FLO - via floCrypto (base blockchain) + * - BTC (Bitcoin) - via btcOperator + * - ETH (Ethereum) - via floEthereum + * - AVAX (Avalanche C-Chain) - same as ETH (EVM-compatible) + * - BSC (Binance Smart Chain) - same as ETH (EVM-compatible) + * - MATIC (Polygon) - same as ETH (EVM-compatible) + * - ARB (Arbitrum) - same as ETH (EVM-compatible) + * - OP (Optimism) - same as ETH (EVM-compatible) + * - HBAR (Hedera) - same as ETH (EVM-compatible) + * - XRP (Ripple) - via xrpl library + * - SUI - via nacl + BLAKE2b + * - TON - via nacl + TonWeb + * - TRON - via TronWeb + * - DOGE - via bitjs with version byte 0x1e + * - LTC (Litecoin) - via bitjs with version byte 0x30 + * - BCH (Bitcoin Cash) - via bitjs with version byte 0x00 + CashAddr format + * - DOT (Polkadot) - via @polkadot/util-crypto with SS58 encoding + * - ALGO (Algorand) - via nacl + SHA-512/256 with Base32 encoding + * - XLM (Stellar) - via nacl + CRC16-XModem with Base32 encoding + * - SOL (Solana) - via Ed25519 + Base58 encoding + * - ADA (Cardano) - via cardanoCrypto library (https://cdn.jsdelivr.net/gh/void-57/cardano-wallet-test@main/cardano-crypto.iife.js) + * + * Dependencies : + * - bitjs (for WIF decoding) + * - Crypto.util (for hex/bytes conversion) + * - xrpl (for XRP) + * - nacl/TweetNaCl (for SUI, TON, ALGO, XLM) + * - TonWeb (for TON) + * - TronWeb (for TRON) + * - @polkadot/util-crypto (for DOT) + * - @polkadot/util (for DOT) + * - js-sha512 (for ALGO) + * - @solana/web3.js (for SOL) + * - cardanoCrypto (for ADA) + */ + +// Base58 encoding/decoding for Solana +var bs58 = (function () { + const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + const BASE = ALPHABET.length; + + // Convert a byte array to a Base58 string + function encode(buffer) { + if (buffer.length === 0) return ""; + + // Convert byte array to a BigInt + let intVal = BigInt(0); + for (let i = 0; i < buffer.length; i++) { + intVal = intVal * BigInt(256) + BigInt(buffer[i]); + } + + // Convert BigInt to Base58 string + let result = ""; + while (intVal > 0) { + const remainder = intVal % BigInt(BASE); + intVal = intVal / BigInt(BASE); + result = ALPHABET[Number(remainder)] + result; + } + + // Add '1' for each leading 0 byte in the byte array + for (let i = 0; i < buffer.length && buffer[i] === 0; i++) { + result = ALPHABET[0] + result; + } + + return result; + } + + // Convert a Base58 string to a byte array + function decode(string) { + if (string.length === 0) return new Uint8Array(); + + // Convert Base58 string to BigInt + let intVal = BigInt(0); + for (let i = 0; i < string.length; i++) { + const charIndex = ALPHABET.indexOf(string[i]); + if (charIndex < 0) { + throw new Error("Invalid Base58 character"); + } + intVal = intVal * BigInt(BASE) + BigInt(charIndex); + } + + // Convert BigInt to byte array + const byteArray = []; + while (intVal > 0) { + byteArray.push(Number(intVal % BigInt(256))); + intVal = intVal / BigInt(256); + } + + // Reverse the byte array and add leading zeros + byteArray.reverse(); + for (let i = 0; i < string.length && string[i] === ALPHABET[0]; i++) { + byteArray.unshift(0); + } + + return Uint8Array.from(byteArray); + } + + return { encode, decode }; +})(); + +// BlakeJS - BLAKE2b hashing implementation for SUI +const blakejs = (function () { + const BLAKE2B_IV32 = new Uint32Array([ + 0xf3bcc908, 0x6a09e667, 0x84caa73b, 0xbb67ae85, 0xfe94f82b, 0x3c6ef372, + 0x5f1d36f1, 0xa54ff53a, 0xade682d1, 0x510e527f, 0x2b3e6c1f, 0x9b05688c, + 0xfb41bd6b, 0x1f83d9ab, 0x137e2179, 0x5be0cd19, + ]); + const SIGMA8 = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 14, 10, 4, 8, 9, 15, + 13, 6, 1, 12, 0, 2, 11, 7, 5, 3, 11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, + 7, 1, 9, 4, 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8, 9, 0, 5, + 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13, 2, 12, 6, 10, 0, 11, 8, 3, 4, + 13, 7, 5, 15, 14, 1, 9, 12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, + 11, 13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10, 6, 15, 14, 9, 11, + 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5, 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, + 3, 12, 13, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 14, 10, + 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3, + ]; + const SIGMA82 = new Uint8Array(SIGMA8.map((x) => x * 2)); + const v = new Uint32Array(32); + const m = new Uint32Array(32); + const parameterBlock = new Uint8Array(64); + + function B2B_GET32(arr, i) { + return arr[i] ^ (arr[i + 1] << 8) ^ (arr[i + 2] << 16) ^ (arr[i + 3] << 24); + } + function ADD64AA(v, a, b) { + const o0 = v[a] + v[b]; + let o1 = v[a + 1] + v[b + 1]; + if (o0 >= 0x100000000) o1++; + v[a] = o0; + v[a + 1] = o1; + } + function ADD64AC(v, a, b0, b1) { + let o0 = v[a] + b0; + if (b0 < 0) o0 += 0x100000000; + let o1 = v[a + 1] + b1; + if (o0 >= 0x100000000) o1++; + v[a] = o0; + v[a + 1] = o1; + } + function B2B_G(a, b, c, d, ix, iy) { + const x0 = m[ix], + x1 = m[ix + 1], + y0 = m[iy], + y1 = m[iy + 1]; + ADD64AA(v, a, b); + ADD64AC(v, a, x0, x1); + let xor0 = v[d] ^ v[a], + xor1 = v[d + 1] ^ v[a + 1]; + v[d] = xor1; + v[d + 1] = xor0; + ADD64AA(v, c, d); + xor0 = v[b] ^ v[c]; + xor1 = v[b + 1] ^ v[c + 1]; + v[b] = (xor0 >>> 24) ^ (xor1 << 8); + v[b + 1] = (xor1 >>> 24) ^ (xor0 << 8); + ADD64AA(v, a, b); + ADD64AC(v, a, y0, y1); + xor0 = v[d] ^ v[a]; + xor1 = v[d + 1] ^ v[a + 1]; + v[d] = (xor0 >>> 16) ^ (xor1 << 16); + v[d + 1] = (xor1 >>> 16) ^ (xor0 << 16); + ADD64AA(v, c, d); + xor0 = v[b] ^ v[c]; + xor1 = v[b + 1] ^ v[c + 1]; + v[b] = (xor1 >>> 31) ^ (xor0 << 1); + v[b + 1] = (xor0 >>> 31) ^ (xor1 << 1); + } + function blake2bCompress(ctx, last) { + let i; + for (i = 0; i < 16; i++) { + v[i] = ctx.h[i]; + v[i + 16] = BLAKE2B_IV32[i]; + } + v[24] = v[24] ^ ctx.t; + v[25] = v[25] ^ (ctx.t / 0x100000000); + if (last) { + v[28] = ~v[28]; + v[29] = ~v[29]; + } + for (i = 0; i < 32; i++) m[i] = B2B_GET32(ctx.b, 4 * i); + for (i = 0; i < 12; i++) { + B2B_G(0, 8, 16, 24, SIGMA82[i * 16 + 0], SIGMA82[i * 16 + 1]); + B2B_G(2, 10, 18, 26, SIGMA82[i * 16 + 2], SIGMA82[i * 16 + 3]); + B2B_G(4, 12, 20, 28, SIGMA82[i * 16 + 4], SIGMA82[i * 16 + 5]); + B2B_G(6, 14, 22, 30, SIGMA82[i * 16 + 6], SIGMA82[i * 16 + 7]); + B2B_G(0, 10, 20, 30, SIGMA82[i * 16 + 8], SIGMA82[i * 16 + 9]); + B2B_G(2, 12, 22, 24, SIGMA82[i * 16 + 10], SIGMA82[i * 16 + 11]); + B2B_G(4, 14, 16, 26, SIGMA82[i * 16 + 12], SIGMA82[i * 16 + 13]); + B2B_G(6, 8, 18, 28, SIGMA82[i * 16 + 14], SIGMA82[i * 16 + 15]); + } + for (i = 0; i < 16; i++) ctx.h[i] = ctx.h[i] ^ v[i] ^ v[i + 16]; + } + function blake2bInit(outlen, key) { + const ctx = { + b: new Uint8Array(128), + h: new Uint32Array(16), + t: 0, + c: 0, + outlen: outlen, + }; + parameterBlock.fill(0); + parameterBlock[0] = outlen; + if (key) parameterBlock[1] = key.length; + parameterBlock[2] = 1; + parameterBlock[3] = 1; + for (let i = 0; i < 16; i++) + ctx.h[i] = BLAKE2B_IV32[i] ^ B2B_GET32(parameterBlock, i * 4); + if (key) { + blake2bUpdate(ctx, key); + ctx.c = 128; + } + return ctx; + } + function blake2bUpdate(ctx, input) { + for (let i = 0; i < input.length; i++) { + if (ctx.c === 128) { + ctx.t += ctx.c; + blake2bCompress(ctx, false); + ctx.c = 0; + } + ctx.b[ctx.c++] = input[i]; + } + } + function blake2bFinal(ctx) { + ctx.t += ctx.c; + while (ctx.c < 128) ctx.b[ctx.c++] = 0; + blake2bCompress(ctx, true); + const out = new Uint8Array(ctx.outlen); + for (let i = 0; i < ctx.outlen; i++) + out[i] = ctx.h[i >> 2] >> (8 * (i & 3)); + return out; + } + function blake2b(input, key, outlen) { + outlen = outlen || 64; + if (!(input instanceof Uint8Array)) { + if (typeof input === "string") { + const enc = unescape(encodeURIComponent(input)); + input = new Uint8Array(enc.length); + for (let i = 0; i < enc.length; i++) input[i] = enc.charCodeAt(i); + } else throw new Error("Input must be string or Uint8Array"); + } + const ctx = blake2bInit(outlen, key); + blake2bUpdate(ctx, input); + return blake2bFinal(ctx); + } + return { blake2b: blake2b }; +})(); + +/** + * Convert WIF private key to BSC (Binance Smart Chain) address + * BSC uses the same address format as Ethereum (EVM-compatible) + * @param {string} publicKey - Compressed public key in hex + * @returns {string} BSC address (same as ETH address) + */ +function convertPublicKeyToBscAddress(publicKey) { + // BSC addresses are identical to Ethereum addresses + // Both use the same EVM-compatible format + if ( + typeof floEthereum === "undefined" || + !floEthereum.ethAddressFromCompressedPublicKey + ) { + throw new Error("floEthereum library not loaded"); + } + return floEthereum.ethAddressFromCompressedPublicKey(publicKey); +} + +/** + * Convert WIF private key to XRP (Ripple) address + * Uses xrpl library with Ed25519 derivation + * @param {string} wif - WIF format private key + * @returns {string|null} XRP address or null on error + */ +function convertWIFtoXrpAddress(wif) { + try { + if (typeof window.xrpl === "undefined") { + throw new Error("xrpl library not loaded"); + } + if (typeof bitjs === "undefined") { + throw new Error("bitjs library not loaded"); + } + // Use bitjs.wif2privkey to decode WIF and get the raw private key hex + const decoded = bitjs.wif2privkey(wif); + if (!decoded || !decoded.privkey) { + throw new Error("Failed to decode WIF private key"); + } + // Convert hex string to byte array for xrpl + const keyBytes = Crypto.util.hexToBytes(decoded.privkey); + // Create XRP wallet from entropy (raw private key bytes) + const wallet = xrpl.Wallet.fromEntropy(keyBytes); + return wallet.address; + } catch (error) { + console.error("WIF to XRP conversion error:", error); + return null; + } +} + +/** + * Convert WIF private key to SUI address + * Uses Ed25519 keypair + BLAKE2b-256 hashing + * @param {string} wif - WIF format private key + * @returns {string|null} SUI address (0x prefixed) or null on error + */ +function convertWIFtoSuiAddress(wif) { + try { + if (typeof nacl === "undefined") { + throw new Error("nacl (TweetNaCl) library not loaded"); + } + if (typeof bitjs === "undefined") { + throw new Error("bitjs library not loaded"); + } + // Use bitjs.wif2privkey to decode WIF and get the raw private key hex + const decoded = bitjs.wif2privkey(wif); + if (!decoded || !decoded.privkey) { + throw new Error("Failed to decode WIF private key"); + } + // Get first 32 bytes (64 hex chars) for Ed25519 seed + const privKeyHex = decoded.privkey.substring(0, 64); + const privBytes = Crypto.util.hexToBytes(privKeyHex); + const seed = new Uint8Array(privBytes.slice(0, 32)); + // Generate Ed25519 keypair from seed + const keyPair = nacl.sign.keyPair.fromSeed(seed); + const pubKey = keyPair.publicKey; + // Prefix public key with 0x00 (Ed25519 scheme flag) + const prefixedPubKey = new Uint8Array([0x00, ...pubKey]); + // Hash with BLAKE2b-256 + const hash = blakejs.blake2b(prefixedPubKey, null, 32); + // Convert to hex address with 0x prefix + const suiAddress = "0x" + Crypto.util.bytesToHex(hash); + return suiAddress; + } catch (error) { + console.error("WIF to SUI conversion error:", error); + return null; + } +} + +/** + * Convert WIF private key to TON address + * Uses Ed25519 keypair + TonWeb v4R2 wallet + * @param {string} wif - WIF format private key + * @returns {Promise} TON address (bounceable format) or null on error + */ +async function convertWIFtoTonAddress(wif) { + try { + if (typeof nacl === "undefined") { + throw new Error("nacl (TweetNaCl) library not loaded"); + } + if (typeof TonWeb === "undefined") { + throw new Error("TonWeb library not loaded"); + } + if (typeof bitjs === "undefined") { + throw new Error("bitjs library not loaded"); + } + // Use bitjs.wif2privkey to decode WIF and get the raw private key hex + const decoded = bitjs.wif2privkey(wif); + if (!decoded || !decoded.privkey) { + throw new Error("Failed to decode WIF private key"); + } + // Get first 32 bytes (64 hex chars) for Ed25519 seed + const privKeyHex = decoded.privkey.substring(0, 64); + const seed = Crypto.util.hexToBytes(privKeyHex); + // Generate Ed25519 keypair from seed + const keyPair = nacl.sign.keyPair.fromSeed(new Uint8Array(seed)); + // Create TON wallet using TonWeb v4R2 wallet + const tonweb = new TonWeb(); + const WalletClass = TonWeb.Wallets.all.v4R2; + if (!WalletClass) { + throw new Error("TonWeb v4R2 wallet not available"); + } + const wallet = new WalletClass(tonweb.provider, { + publicKey: keyPair.publicKey, + }); + const address = await wallet.getAddress(); + // Return user-friendly bounceable address + return address.toString(true, true, false); + } catch (error) { + console.error("WIF to TON conversion error:", error); + return null; + } +} + +/** + * Convert WIF private key to TRON address + * Uses TronWeb library for address derivation + * @param {string} wif - WIF format private key + * @returns {string|null} TRON address (Base58 format) or null on error + */ +function convertWIFtoTronAddress(wif) { + try { + if (typeof TronWeb === "undefined") { + throw new Error("TronWeb library not loaded"); + } + if (typeof bitjs === "undefined") { + throw new Error("bitjs library not loaded"); + } + // Use bitjs.wif2privkey to decode WIF and get the raw private key hex + const decoded = bitjs.wif2privkey(wif); + if (!decoded || !decoded.privkey) { + throw new Error("Failed to decode WIF private key"); + } + // Get the hex private key (64 chars) + const privKeyHex = decoded.privkey.substring(0, 64); + // Use TronWeb to derive address from private key + const tronAddress = TronWeb.address.fromPrivateKey(privKeyHex); + return tronAddress; + } catch (error) { + console.error("WIF to TRON conversion error:", error); + return null; + } +} + +/** + * Derive all blockchain addresses from a WIF private key + * @param {string} wif - WIF format private key + * @returns {Promise} Object containing all derived addresses + */ +async function deriveAllBlockchainAddresses(wif) { + const addresses = { + bsc: null, + matic: null, + hbar: null, + arb: null, + op: null, + xrp: null, + sui: null, + ton: null, + tron: null, + doge: null, + ltc: null, + bch: null, + dot: null, + algo: null, + xlm: null, + sol: null, + ada: null, + }; + + try { + // BSC, MATIC, HBAR, ARB, and OP use same address as ETH (requires public key, not WIF) + // These will be set from floGlobals.myEthID in the main code + addresses.bsc = null; // Set in main code as same as ETH + addresses.matic = null; // Set in main code as same as ETH + addresses.hbar = null; // Set in main code as same as ETH + addresses.arb = null; // Set in main code as same as ETH + addresses.op = null; // Set in main code as same as ETH + } catch (e) { + console.warn("BSC/MATIC/HBAR/ARB/OP derivation failed:", e); + } + try { + addresses.xrp = convertWIFtoXrpAddress(wif); + } catch (e) { + console.warn("XRP derivation failed:", e); + } + try { + addresses.sui = convertWIFtoSuiAddress(wif); + } catch (e) { + console.warn("SUI derivation failed:", e); + } + try { + addresses.ton = await convertWIFtoTonAddress(wif); + } catch (e) { + console.warn("TON derivation failed:", e); + } + try { + addresses.tron = convertWIFtoTronAddress(wif); + } catch (e) { + console.warn("TRON derivation failed:", e); + } + try { + addresses.doge = convertWIFtoDogeAddress(wif); + } catch (e) { + console.warn("DOGE derivation failed:", e); + } + try { + addresses.dot = await convertWIFtoPolkadotAddress(wif); + } catch (e) { + console.warn("DOT derivation failed:", e); + } + try { + addresses.algo = convertWIFtoAlgorandAddress(wif); + } catch (e) { + console.warn("ALGO derivation failed:", e); + } + try { + addresses.xlm = convertWIFtoStellarAddress(wif); + } catch (e) { + console.warn("XLM derivation failed:", e); + } + try { + addresses.ltc = convertWIFtoLitecoinAddress(wif); + } catch (e) { + console.warn("LTC derivation failed:", e); + } + try { + addresses.bch = convertWIFtoBitcoinCashAddress(wif); + } catch (e) { + console.warn("BCH derivation failed:", e); + } + try { + addresses.sol = convertWIFtoSolanaAddress(wif); + } catch (e) { + console.warn("SOL derivation failed:", e); + } + try { + addresses.ada = await convertWIFtoCardanoAddress(wif); + } catch (e) { + console.warn("ADA derivation failed:", e); + } + + return addresses; +} + +/** + * Convert WIF private key to DOGE (Dogecoin) address + * Uses secp256k1 with version byte 0x1e (30) + * @param {string} wif - WIF format private key + * @returns {string|null} DOGE address (Base58 format starting with 'D') or null on error + */ +function convertWIFtoDogeAddress(wif) { + try { + // Store original settings + const origPub = bitjs.pub; + const origPriv = bitjs.priv; + const origBitjsCompressed = bitjs.compressed; + + // Decode WIF to get raw private key and determine if compressed + const decode = Bitcoin.Base58.decode(wif); + const keyWithVersion = decode.slice(0, decode.length - 4); + let key = keyWithVersion.slice(1); + + let compressed = true; + if (key.length >= 33 && key[key.length - 1] === 0x01) { + // Compressed WIF has 0x01 suffix + key = key.slice(0, key.length - 1); + compressed = true; + } else { + compressed = false; + } + + const privKeyHex = Crypto.util.bytesToHex(key); + + // Set DOGE version bytes and compression + bitjs.pub = 0x1e; + bitjs.priv = 0x9e; + bitjs.compressed = compressed; + + // Generate public key from private key + const pubKey = bitjs.newPubkey(privKeyHex); + // Generate DOGE address from public key + const dogeAddress = bitjs.pubkey2address(pubKey); + + // Restore original settings + bitjs.pub = origPub; + bitjs.priv = origPriv; + bitjs.compressed = origBitjsCompressed; + + return dogeAddress; + } catch (error) { + console.error("WIF to DOGE conversion error:", error); + return null; + } +} + +/** + * Convert WIF private key to LTC (Litecoin) address + * Uses secp256k1 with version byte 0x30 (48) + * @param {string} wif - WIF format private key + * @returns {string|null} LTC address (Base58 format starting with 'L') or null on error + */ +function convertWIFtoLitecoinAddress(wif) { + try { + // Store original settings + const origPub = bitjs.pub; + const origPriv = bitjs.priv; + const origBitjsCompressed = bitjs.compressed; + + // Decode WIF to get raw private key and determine if compressed + const decode = Bitcoin.Base58.decode(wif); + const keyWithVersion = decode.slice(0, decode.length - 4); + let key = keyWithVersion.slice(1); + + let compressed = true; + if (key.length >= 33 && key[key.length - 1] === 0x01) { + // Compressed WIF has 0x01 suffix + key = key.slice(0, key.length - 1); + compressed = true; + } else { + compressed = false; + } + + const privKeyHex = Crypto.util.bytesToHex(key); + + // Set LTC version bytes and compression + bitjs.pub = 0x30; // Litecoin mainnet pubkey version + bitjs.priv = 0xb0; // Litecoin mainnet private key version + bitjs.compressed = compressed; + + // Generate public key from private key + const pubKey = bitjs.newPubkey(privKeyHex); + // Generate LTC address from public key + const ltcAddress = bitjs.pubkey2address(pubKey); + + // Restore original settings + bitjs.pub = origPub; + bitjs.priv = origPriv; + bitjs.compressed = origBitjsCompressed; + + return ltcAddress; + } catch (error) { + console.error("WIF to LTC conversion error:", error); + return null; + } +} + +/** + * Convert WIF private key to BCH (Bitcoin Cash) address in CashAddr format + * Uses secp256k1 with version byte 0x00 (same as BTC) but returns CashAddr format + * @param {string} wif - WIF format private key + * @returns {string|null} BCH address (CashAddr format without prefix) or null on error + */ +function convertWIFtoBitcoinCashAddress(wif) { + try { + // Helper function to convert legacy address to CashAddr + function toCashAddr(legacyAddr) { + if (!legacyAddr || typeof legacyAddr !== "string") return legacyAddr; + if (legacyAddr.includes(":")) return legacyAddr; // Already cashaddr + + try { + const ALPHABET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + + function polymod(values) { + let c = 1n; + for (let v of values) { + let b = c >> 35n; + c = ((c & 0x07ffffffffn) << 5n) ^ BigInt(v); + if (b & 1n) c ^= 0x98f2bc8e61n; + if (b & 2n) c ^= 0x79b76d99e2n; + if (b & 4n) c ^= 0xf33e5fb3c4n; + if (b & 8n) c ^= 0xae2eabe2a8n; + if (b & 16n) c ^= 0x1e4f43e470n; + } + return c ^ 1n; + } + + function expandPrefix(prefix) { + let ret = []; + for (let i = 0; i < prefix.length; i++) { + ret.push(prefix.charCodeAt(i) & 0x1f); + } + ret.push(0); + return ret; + } + + function convertBits(data, from, to, pad = true) { + let acc = 0; + let bits = 0; + const ret = []; + const maxv = (1 << to) - 1; + for (let i = 0; i < data.length; ++i) { + const value = data[i] & 0xff; + acc = (acc << from) | value; + bits += from; + while (bits >= to) { + bits -= to; + ret.push((acc >> bits) & maxv); + } + acc &= (1 << bits) - 1; + } + if (pad && bits > 0) { + ret.push((acc << (to - bits)) & maxv); + } + return ret; + } + + // Decode legacy address + const decoded = Bitcoin.Base58.decode(legacyAddr); + if (!decoded || decoded.length < 25) return legacyAddr; + + const version = decoded[0]; + const hash = decoded.slice(1, -4); + + // Version byte 0x00 is P2PKH (type 0), 0x05 is P2SH (type 1) + const type = version === 0x00 ? 0 : version === 0x05 ? 1 : null; + if (type === null) return legacyAddr; + + const prefix = "bitcoincash"; + const versionByte = type << 3; + const payload = [versionByte].concat(Array.from(hash)); + const payload5bit = convertBits(payload, 8, 5, true); + + const checksumData = expandPrefix(prefix) + .concat(payload5bit) + .concat([0, 0, 0, 0, 0, 0, 0, 0]); + const checksum = polymod(checksumData); + const checksum5bit = []; + for (let i = 0; i < 8; i++) { + checksum5bit.push(Number((checksum >> (5n * BigInt(7 - i))) & 0x1fn)); + } + + const combined = payload5bit.concat(checksum5bit); + let ret = ""; + for (let v of combined) { + ret += ALPHABET[v]; + } + return ret; + } catch (e) { + console.error("CashAddr conversion error:", e); + return legacyAddr; + } + } + + // Store original settings + const origPub = bitjs.pub; + const origPriv = bitjs.priv; + const origBitjsCompressed = bitjs.compressed; + + // Decode WIF to get raw private key and determine if compressed + const decode = Bitcoin.Base58.decode(wif); + const keyWithVersion = decode.slice(0, decode.length - 4); + let key = keyWithVersion.slice(1); + + let compressed = true; + if (key.length >= 33 && key[key.length - 1] === 0x01) { + key = key.slice(0, key.length - 1); + compressed = true; + } else { + compressed = false; + } + + const privKeyHex = Crypto.util.bytesToHex(key); + + // Set BCH version bytes (same as BTC mainnet) + bitjs.pub = 0x00; // Bitcoin Cash mainnet pubkey version + bitjs.priv = 0x80; // Bitcoin Cash mainnet private key version + bitjs.compressed = compressed; + + // Generate public key from private key + const pubKey = bitjs.newPubkey(privKeyHex); + // Generate legacy BCH address from public key + const legacyAddress = bitjs.pubkey2address(pubKey); + + // Convert to CashAddr format + const cashAddr = toCashAddr(legacyAddress); + + // Restore original settings + bitjs.pub = origPub; + bitjs.priv = origPriv; + bitjs.compressed = origBitjsCompressed; + + return cashAddr; + } catch (error) { + console.error("WIF to BCH conversion error:", error); + return null; + } +} + +/** + * Convert WIF private key to Polkadot (DOT) address + * Uses Sr25519 (Schnorrkel) keypair with SS58 address encoding (prefix 0 for Polkadot mainnet) + * @param {string} wif - WIF format private key + * @returns {Promise} DOT address (SS58 format) or null on error + */ +async function convertWIFtoPolkadotAddress(wif) { + try { + if (typeof bitjs === "undefined") { + throw new Error("bitjs library not loaded"); + } + + // Access Polkadot library from window object + const polkadotAPI = window.polkadotUtilCrypto; + + if (!polkadotAPI) { + throw new Error("@polkadot/util-crypto library not loaded"); + } + + // Wait for WASM crypto to be ready + if (typeof polkadotAPI.cryptoWaitReady === "function") { + await polkadotAPI.cryptoWaitReady(); + } + + // Use bitjs.wif2privkey to decode WIF and get the raw private key hex + const decoded = bitjs.wif2privkey(wif); + if (!decoded || !decoded.privkey) { + throw new Error("Failed to decode WIF private key"); + } + + // Get first 32 bytes (64 hex chars) for Sr25519 seed + const privKeyHex = decoded.privkey.substring(0, 64); + const privBytes = Crypto.util.hexToBytes(privKeyHex); + const seed = new Uint8Array(privBytes.slice(0, 32)); + + // Create Sr25519 keypair from seed (Polkadot uses Schnorrkel/Sr25519, not Ed25519) + const keyPair = polkadotAPI.sr25519PairFromSeed(seed); + + // Encode address in SS58 format with Polkadot prefix (0) + const dotAddress = polkadotAPI.encodeAddress(keyPair.publicKey, 0); + + return dotAddress; + } catch (error) { + console.error("WIF to Polkadot conversion error:", error); + return null; + } +} + +/** + * Convert WIF private key to Algorand (ALGO) address + * Uses Ed25519 keypair with Base32 encoding and SHA-512/256 checksum + * @param {string} wif - WIF format private key + * @returns {string|null} ALGO address (Base32 format) or null on error + */ +function convertWIFtoAlgorandAddress(wif) { + try { + if (typeof bitjs === "undefined") { + throw new Error("bitjs library not loaded"); + } + if (typeof nacl === "undefined") { + throw new Error("nacl (TweetNaCl) library not loaded"); + } + if (typeof sha512 === "undefined") { + throw new Error("js-sha512 library not loaded"); + } + + // Use bitjs.wif2privkey to decode WIF and get the raw private key hex + const decoded = bitjs.wif2privkey(wif); + if (!decoded || !decoded.privkey) { + throw new Error("Failed to decode WIF private key"); + } + + // Get first 32 bytes (64 hex chars) for Ed25519 seed + const privKeyHex = decoded.privkey.substring(0, 64); + const privBytes = Crypto.util.hexToBytes(privKeyHex); + const seed = new Uint8Array(privBytes.slice(0, 32)); + + // Generate Ed25519 keypair from seed + const keyPair = nacl.sign.keyPair.fromSeed(seed); + const pubKey = keyPair.publicKey; + + // Algorand uses SHA-512/256 (32 bytes output) for checksum + const hashResult = new Uint8Array(sha512.sha512_256.array(pubKey)); + const checksum = hashResult.slice(28, 32); + const addressBytes = new Uint8Array([...pubKey, ...checksum]); + + // Base32 encode the address + const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + let bits = 0; + let value = 0; + let output = ""; + + for (let i = 0; i < addressBytes.length; i++) { + value = (value << 8) | addressBytes[i]; + bits += 8; + + while (bits >= 5) { + output += BASE32_ALPHABET[(value >>> (bits - 5)) & 31]; + bits -= 5; + } + } + + if (bits > 0) { + output += BASE32_ALPHABET[(value << (5 - bits)) & 31]; + } + + return output; + } catch (error) { + console.error("WIF to Algorand conversion error:", error); + return null; + } +} + +/** + * Convert WIF private key to Stellar (XLM) address + * Uses Ed25519 keypair generation with Stellar-specific encoding + * @param {string} wif - The WIF private key + * @returns {string} - The Stellar address (starting with 'G') + */ +function convertWIFtoStellarAddress(wif) { + try { + // Helper function to convert hex to bytes + function hexToBytes(hex) { + const bytes = []; + for (let i = 0; i < hex.length; i += 2) { + bytes.push(parseInt(hex.substr(i, 2), 16)); + } + return bytes; + } + + // Calculate CRC16-XModem checksum (Stellar uses this) + function crc16XModem(data) { + let crc = 0x0000; + for (let i = 0; i < data.length; i++) { + crc ^= data[i] << 8; + for (let j = 0; j < 8; j++) { + if (crc & 0x8000) { + crc = (crc << 1) ^ 0x1021; + } else { + crc = crc << 1; + } + } + } + return crc & 0xffff; + } + + // Decode WIF to get private key (32 bytes) + const privKeyHex = bitjs.wif2privkey(wif).privkey; + const privBytes = hexToBytes(privKeyHex); + const seed = new Uint8Array(privBytes.slice(0, 32)); + + // Generate Ed25519 keypair from seed using TweetNaCl + const keyPair = nacl.sign.keyPair.fromSeed(seed); + const pubKey = keyPair.publicKey; + + // Stellar address encoding: version byte (0x30 for public key 'G') + public key + CRC16-XModem checksum + const versionByte = 0x30; // Results in 'G' prefix for public keys + const payload = new Uint8Array([versionByte, ...pubKey]); + + const checksum = crc16XModem(payload); + // Checksum is stored in little-endian format + const checksumBytes = new Uint8Array([ + checksum & 0xff, + (checksum >> 8) & 0xff, + ]); + const addressBytes = new Uint8Array([...payload, ...checksumBytes]); + + // Base32 encode the address (RFC 4648) + const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + let bits = 0; + let value = 0; + let output = ""; + + for (let i = 0; i < addressBytes.length; i++) { + value = (value << 8) | addressBytes[i]; + bits += 8; + + while (bits >= 5) { + output += BASE32_ALPHABET[(value >>> (bits - 5)) & 31]; + bits -= 5; + } + } + + if (bits > 0) { + output += BASE32_ALPHABET[(value << (5 - bits)) & 31]; + } + + return output; + } catch (error) { + console.error("WIF to Stellar conversion error:", error); + return null; + } +} + +/** + * Convert WIF private key to Solana (SOL) address + * Uses Ed25519 keypair generation with Base58 encoding + * @param {string} wif - The WIF private key + * @returns {string} - The Solana address (Base58 encoded public key) + */ +function convertWIFtoSolanaAddress(wif) { + try { + // Helper function to convert hex to bytes + function hexToBytes(hex) { + const bytes = []; + for (let i = 0; i < hex.length; i += 2) { + bytes.push(parseInt(hex.substr(i, 2), 16)); + } + return bytes; + } + + // Decode WIF to get private key (32 bytes) + const privKeyHex = bitjs.wif2privkey(wif).privkey; + const privBytes = hexToBytes(privKeyHex); + const seed = new Uint8Array(privBytes.slice(0, 32)); + + // Generate Ed25519 keypair from seed + // Use Solana Web3.js Keypair.fromSeed() + if (typeof solanaWeb3 === "undefined") { + throw new Error("Solana Web3.js library not loaded"); + } + + const keypair = solanaWeb3.Keypair.fromSeed(seed); + const solanaAddress = keypair.publicKey.toString(); + + return solanaAddress; + } catch (error) { + console.error("WIF to Solana conversion error:", error); + return null; + } +} + +/** + * Convert WIF private key to Cardano (ADA) address + * Uses cardanoCrypto library for address derivation + * @param {string} wif - WIF format private key + * @returns {Promise} Cardano address or null on error + */ +async function convertWIFtoCardanoAddress(wif) { + try { + if (typeof window.cardanoCrypto === "undefined") { + throw new Error("cardanoCrypto library not loaded"); + } + + // Use cardanoCrypto.importFromKey to derive all addresses from WIF + const wallet = await window.cardanoCrypto.importFromKey(wif); + + if (!wallet || !wallet.Cardano || !wallet.Cardano.address) { + throw new Error("Failed to derive Cardano address"); + } + + return wallet.Cardano.address; + } catch (error) { + console.error("WIF to Cardano conversion error:", error); + return null; + } +} diff --git a/messenger/scripts/floCloudAPI.js b/messenger/scripts/floCloudAPI.js index 19ede97..dfcbe7b 100644 --- a/messenger/scripts/floCloudAPI.js +++ b/messenger/scripts/floCloudAPI.js @@ -392,7 +392,22 @@ if (!address) return; 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); bytes = decode.slice(0, 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] ? 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') throw "library missing (lib_btc.js)"; let decode = coinjs.bech32_decode(address); @@ -414,14 +431,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 +692,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 +713,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 +722,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 +744,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 +753,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)) } }) diff --git a/messenger/scripts/floCrypto.js b/messenger/scripts/floCrypto.js index db90758..8d23c28 100644 --- a/messenger/scripts/floCrypto.js +++ b/messenger/scripts/floCrypto.js @@ -299,7 +299,7 @@ return true; else return false; - } else if (raw.type === 'ethereum') { + } else if (raw.type === 'ethereum' || raw.type === 'bch') { return true } else //unknown return false; @@ -431,14 +431,30 @@ } 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, type: 'ethereum', 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; + } } } diff --git a/messenger/scripts/floDapps.js b/messenger/scripts/floDapps.js index 480e4e4..96360e6 100644 --- a/messenger/scripts/floDapps.js +++ b/messenger/scripts/floDapps.js @@ -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); diff --git a/messenger/scripts/messenger.js b/messenger/scripts/messenger.js index 5e03c32..4b1d5a7 100644 --- a/messenger/scripts/messenger.js +++ b/messenger/scripts/messenger.js @@ -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, 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) { 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,46 @@ 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 '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) .then(result => resolve(result)) .catch(error => reject(error)) @@ -316,7 +403,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 +413,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 +465,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 +568,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 +586,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 +603,83 @@ 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 = [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, { - 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 +716,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) {