Auto-update messenger
This commit is contained in:
parent
99af2eddd4
commit
0dba4e20ec
@ -18,6 +18,15 @@
|
||||
}
|
||||
</script>
|
||||
<script src="https://unpkg.com/uhtml@3.0.1/es.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xrpl@2.7.0/build/xrpl-latest-min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/tweetnacl@1.0.3/nacl.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/js-sha512@0.8.0/build/sha512.min.js"></script>
|
||||
<script src="https://unpkg.com/tonweb/dist/tonweb.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/tronweb@5.3.2/dist/TronWeb.js"></script>
|
||||
<script src="https://unpkg.com/@polkadot/util@12.6.2/bundle-polkadot-util.js"></script>
|
||||
<script src="https://unpkg.com/@polkadot/util-crypto@12.6.2/bundle-polkadot-util-crypto.js"></script>
|
||||
<script src="https://unpkg.com/@solana/web3.js@latest/lib/index.iife.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/gh/void-57/cardano-wallet-test@main/cardano-crypto.iife.js"></script>
|
||||
<script src="scripts/lib.min.js"></script>
|
||||
<script src="scripts/floCrypto.js"></script>
|
||||
<script src="scripts/btcOperator.js"></script>
|
||||
@ -27,8 +36,10 @@
|
||||
<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">
|
||||
|
||||
function canConnectToCloud() {
|
||||
const nodes = Object.values(floCloudAPI.nodes);
|
||||
// check if any node is online with websocket
|
||||
@ -67,9 +78,62 @@
|
||||
document.body.prepend(document.createElement('adblocker-warning'))
|
||||
return
|
||||
}
|
||||
floGlobals.myFloID = floCrypto.getFloID(floDapps.user.public);
|
||||
floGlobals.myBtcID = btcOperator.convert.legacy2bech(floGlobals.myFloID)
|
||||
floGlobals.myEthID = floEthereum.ethAddressFromCompressedPublicKey(floDapps.user.public)
|
||||
let activeChain = localStorage.getItem(`${floGlobals.application}#activeChain`);
|
||||
if (activeChain === 'XRP') {
|
||||
// XRP-only login: skip FLO/BTC/ETH derivations
|
||||
floGlobals.myFloID = floDapps.user.id; // This is the XRP address (r...)
|
||||
floGlobals.myXrpID = floDapps.user.id;
|
||||
floGlobals.myBtcID = null;
|
||||
floGlobals.myEthID = null;
|
||||
floGlobals.myAvaxID = null;
|
||||
floGlobals.myBscID = null;
|
||||
floGlobals.myMaticID = null;
|
||||
floGlobals.myArbID = null;
|
||||
floGlobals.myOpID = null;
|
||||
floGlobals.myHbarID = null;
|
||||
floGlobals.mySuiID = null;
|
||||
floGlobals.myAdaID = null;
|
||||
floGlobals.myTonID = null;
|
||||
floGlobals.myTronID = null;
|
||||
floGlobals.myDogeID = null;
|
||||
floGlobals.myLtcID = null;
|
||||
floGlobals.myBchID = null;
|
||||
floGlobals.myDotID = null;
|
||||
floGlobals.myAlgoID = null;
|
||||
floGlobals.myXlmID = null;
|
||||
floGlobals.mySolID = null;
|
||||
} else {
|
||||
floGlobals.myFloID = floCrypto.getFloID(floDapps.user.public);
|
||||
floGlobals.myBtcID = btcOperator.convert.legacy2bech(floGlobals.myFloID)
|
||||
floGlobals.myEthID = floEthereum.ethAddressFromCompressedPublicKey(floDapps.user.public)
|
||||
// AVAX C-Chain uses same address format as Ethereum
|
||||
floGlobals.myAvaxID = floGlobals.myEthID;
|
||||
// BSC (Binance Smart Chain) uses same address format as Ethereum
|
||||
floGlobals.myBscID = floGlobals.myEthID;
|
||||
// MATIC (Polygon) uses same address format as Ethereum
|
||||
floGlobals.myMaticID = floGlobals.myEthID;
|
||||
// Arbitrum uses same address format as Ethereum
|
||||
floGlobals.myArbID = floGlobals.myEthID;
|
||||
// Optimism uses same address format as Ethereum
|
||||
floGlobals.myOpID = floGlobals.myEthID;
|
||||
// HBAR (Hedera) uses same address format as Ethereum
|
||||
floGlobals.myHbarID = floGlobals.myEthID;
|
||||
|
||||
// Initialize private-key-dependent addresses to null (will be derived after messenger init)
|
||||
floGlobals.myXrpID = null;
|
||||
floGlobals.mySuiID = null;
|
||||
floGlobals.myAdaID = null;
|
||||
floGlobals.myTonID = null;
|
||||
floGlobals.myTronID = null;
|
||||
floGlobals.myDogeID = null;
|
||||
floGlobals.myLtcID = null;
|
||||
floGlobals.myBchID = null;
|
||||
floGlobals.myDotID = null;
|
||||
floGlobals.myAlgoID = null;
|
||||
floGlobals.myXlmID = null;
|
||||
floGlobals.mySolID = null;
|
||||
// Note: Cardano (ADA) address will be derived from private key later
|
||||
}
|
||||
document.querySelectorAll('.user-profile-id').forEach(el => el.textContent = floGlobals.myFloID)
|
||||
//load messages from IDB and render them
|
||||
console.log(`Loading Data! Please Wait...`)
|
||||
@ -80,9 +144,102 @@
|
||||
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(result => {
|
||||
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`);
|
||||
if (indexArr) {
|
||||
const indices = JSON.parse(indexArr);
|
||||
const shares = await Promise.all(indices.map(idx => compactIDB.readData('credentials', idx, floGlobals.application)));
|
||||
const secret = floCrypto.retrieveShamirSecret(shares);
|
||||
if (secret && secret.length !== 52) {
|
||||
// Secret is longer than 52 chars = it's AES encrypted = password-secured
|
||||
floGlobals.isPrivKeySecured = true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not check password security status:', e);
|
||||
}
|
||||
|
||||
// Derive private-key-dependent blockchain addresses after messenger is initialized
|
||||
// Check if we have a stored login password (for password-secured accounts)
|
||||
if (floGlobals._loginPassword) {
|
||||
// Use stored password to derive addresses
|
||||
try {
|
||||
const privKey = await getPrivKeyWithPassword(floGlobals._loginPassword);
|
||||
if (typeof privKey === 'string' && privKey.length > 0) {
|
||||
try { floGlobals.myXrpID = convertWIFtoXrpAddress(privKey); } catch (e) { console.warn('XRP derivation failed:', e); }
|
||||
try { floGlobals.mySuiID = convertWIFtoSuiAddress(privKey); } catch (e) { console.warn('SUI derivation failed:', e); }
|
||||
try { floGlobals.myTonID = await convertWIFtoTonAddress(privKey); } catch (e) { console.warn('TON derivation failed:', e); }
|
||||
try { floGlobals.myTronID = convertWIFtoTronAddress(privKey); } catch (e) { console.warn('TRON derivation failed:', e); }
|
||||
try { floGlobals.myDogeID = convertWIFtoDogeAddress(privKey); } catch (e) { console.warn('DOGE derivation failed:', e); }
|
||||
try { floGlobals.myLtcID = convertWIFtoLitecoinAddress(privKey); } catch (e) { console.warn('LTC derivation failed:', e); }
|
||||
try { floGlobals.myBchID = convertWIFtoBitcoinCashAddress(privKey); } catch (e) { console.warn('BCH derivation failed:', e); }
|
||||
try { floGlobals.myDotID = await convertWIFtoPolkadotAddress(privKey); } catch (e) { console.warn('DOT derivation failed:', e); }
|
||||
try { floGlobals.myAlgoID = convertWIFtoAlgorandAddress(privKey); } catch (e) { console.warn('ALGO derivation failed:', e); }
|
||||
try { floGlobals.myXlmID = convertWIFtoStellarAddress(privKey); } catch (e) { console.warn('XLM derivation failed:', e); }
|
||||
try { floGlobals.mySolID = convertWIFtoSolanaAddress(privKey); } catch (e) { console.warn('SOL derivation failed:', e); }
|
||||
try { floGlobals.myAdaID = await convertWIFtoCardanoAddress(privKey); } catch (e) { console.warn('ADA derivation failed:', e); }
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to derive addresses with stored password:', e);
|
||||
}
|
||||
// Clear the stored password for security
|
||||
delete floGlobals._loginPassword;
|
||||
} else {
|
||||
// Try to access private key directly (non-password-secured accounts)
|
||||
try {
|
||||
let privKey = floDapps.user.private;
|
||||
if (privKey instanceof Promise) {
|
||||
privKey = await privKey;
|
||||
}
|
||||
if (typeof privKey === 'string' && privKey.length > 0) {
|
||||
try { floGlobals.myXrpID = convertWIFtoXrpAddress(privKey); } catch (e) { console.warn('XRP derivation failed:', e); }
|
||||
try { floGlobals.mySuiID = convertWIFtoSuiAddress(privKey); } catch (e) { console.warn('SUI derivation failed:', e); }
|
||||
try { floGlobals.myTonID = await convertWIFtoTonAddress(privKey); } catch (e) { console.warn('TON derivation failed:', e); }
|
||||
try { floGlobals.myTronID = convertWIFtoTronAddress(privKey); } catch (e) { console.warn('TRON derivation failed:', e); }
|
||||
try { floGlobals.myDogeID = convertWIFtoDogeAddress(privKey); } catch (e) { console.warn('DOGE derivation failed:', e); }
|
||||
try { floGlobals.myLtcID = convertWIFtoLitecoinAddress(privKey); } catch (e) { console.warn('LTC derivation failed:', e); }
|
||||
try { floGlobals.myBchID = convertWIFtoBitcoinCashAddress(privKey); } catch (e) { console.warn('BCH derivation failed:', e); }
|
||||
try { floGlobals.myDotID = await convertWIFtoPolkadotAddress(privKey); } catch (e) { console.warn('DOT derivation failed:', e); }
|
||||
try { floGlobals.myAlgoID = convertWIFtoAlgorandAddress(privKey); } catch (e) { console.warn('ALGO derivation failed:', e); }
|
||||
try { floGlobals.myXlmID = convertWIFtoStellarAddress(privKey); } catch (e) { console.warn('XLM derivation failed:', e); }
|
||||
try { floGlobals.mySolID = convertWIFtoSolanaAddress(privKey); } catch (e) { console.warn('SOL derivation failed:', e); }
|
||||
try { floGlobals.myAdaID = await convertWIFtoCardanoAddress(privKey); } catch (e) { console.warn('ADA derivation failed:', e); }
|
||||
}
|
||||
} catch (e) {
|
||||
// Private key is password-secured but we don't have the password
|
||||
floGlobals.isPrivKeySecured = true;
|
||||
console.info('Private key is password-secured. Use Unlock button in Profile to derive addresses.');
|
||||
}
|
||||
}
|
||||
|
||||
// Reconnect inbox now that late-derived addresses (ADA, XRP, SOL, etc.) are available
|
||||
// These were null when requestDirectInbox() ran during init()
|
||||
rmMessenger.reconnectInbox();
|
||||
|
||||
//Check for available bg image
|
||||
setBgImage();
|
||||
routeTo(window.location.hash, { firstLoad: true })
|
||||
@ -98,7 +255,7 @@
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
notify(error, "error")
|
||||
notify(e, "error")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -126,6 +283,46 @@
|
||||
</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>
|
||||
<button class="button w-100" onclick="resolveBlockchainSelection('HBAR')">Hedera (HBAR)</button>
|
||||
<button class="button w-100" onclick="resolveBlockchainSelection('ARB')">Arbitrum (ARB)</button>
|
||||
<button class="button w-100" onclick="resolveBlockchainSelection('OP')">Optimism (OP)</button>
|
||||
</div>
|
||||
<div class="flex align-center gap-0-5 margin-top-1 margin-left-auto">
|
||||
<button class="button cancel-button" onclick="resolveBlockchainSelection(null)">Cancel</button>
|
||||
</div>
|
||||
</sm-popup>
|
||||
<sm-popup id="btc_bch_select_popup" dismissable="false">
|
||||
<h4 id="btc_bch_select_title">Select Blockchain</h4>
|
||||
<p>Your private key format is used by Bitcoin and Bitcoin Cash. Please select the correct blockchain for this
|
||||
key:</p>
|
||||
<div class="grid gap-0-5 margin-top-1">
|
||||
<button class="button w-100" onclick="resolveBlockchainSelection('BTC')">Bitcoin (BTC)</button>
|
||||
<button class="button w-100" onclick="resolveBlockchainSelection('BCH')">Bitcoin Cash (BCH)</button>
|
||||
</div>
|
||||
<div class="flex align-center gap-0-5 margin-top-1 margin-left-auto">
|
||||
<button class="button cancel-button" onclick="resolveBlockchainSelection(null)">Cancel</button>
|
||||
</div>
|
||||
</sm-popup>
|
||||
<sm-popup id="ton_algo_select_popup" dismissable="false">
|
||||
<h4 id="ton_algo_select_title">Select Blockchain</h4>
|
||||
<p>Your private key format is used by TON and Algorand. Please select the correct blockchain for this key:</p>
|
||||
<div class="grid gap-0-5 margin-top-1">
|
||||
<button class="button w-100" onclick="resolveBlockchainSelection('TON')">TON</button>
|
||||
<button class="button w-100" onclick="resolveBlockchainSelection('ALGO')">Algorand (ALGO)</button>
|
||||
</div>
|
||||
<div class="flex align-center gap-0-5 margin-top-1 margin-left-auto">
|
||||
<button class="button cancel-button" onclick="resolveBlockchainSelection(null)">Cancel</button>
|
||||
</div>
|
||||
</sm-popup>
|
||||
<div id="adblocker_warning"></div>
|
||||
<div id="secondary_pages" class="page hidden">
|
||||
<header class="flex align-center gap-1 space-between">
|
||||
@ -166,7 +363,7 @@
|
||||
<p>Welcome back, glad to see you again</p>
|
||||
<sm-form id="sign_in_form">
|
||||
<sm-input id="private_key_field" class="password-field" type="password"
|
||||
placeholder="FLO/BTC/ETH private key" error-text="Private key is invalid" data-private-key
|
||||
placeholder="Enter Blockchain Private Key" error-text="Private key is invalid" data-private-key
|
||||
required>
|
||||
<label slot="right" class="interact">
|
||||
<input type="checkbox" class="hidden" autocomplete="off" readonly
|
||||
@ -286,7 +483,7 @@
|
||||
<div id="chat_sections">
|
||||
<div class="flex flex-direction-column gap-0-5" style="overflow: hidden;">
|
||||
<div class="flex align-center gap-0-5" style="padding: 0 1rem;">
|
||||
<sm-input id="search_chats" type="search" placeholder="FLO/BTC/ETH address or name">
|
||||
<sm-input id="search_chats" type="search" placeholder="Blockchain address or name">
|
||||
<svg slot="icon" class="icon" xmlns="http://www.w3.org/2000/svg" height="24px"
|
||||
viewBox="0 0 24 24" width="24px" fill="#000000">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
@ -723,8 +920,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>
|
||||
@ -831,7 +1028,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>
|
||||
@ -871,7 +1068,7 @@
|
||||
<div id="select_contacts_container" class="observe-empty-state"></div>
|
||||
<div class="empty-state">
|
||||
<h4 class="margin-bottom-0-5">No saved contacts.</h4>
|
||||
<p class="margin-bottom-1">Use 'Add contact' to add new FLO/BTC/ETH address as a contact.</p>
|
||||
<p class="margin-bottom-1">Use 'Add contact' to add a new blockchain address as a contact.</p>
|
||||
<button class="button interactive" onclick="openPopup('add_contact_popup')">
|
||||
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg"
|
||||
enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px"
|
||||
@ -1434,7 +1631,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))
|
||||
}
|
||||
}
|
||||
@ -1674,18 +1871,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')
|
||||
@ -1693,7 +1951,7 @@
|
||||
if (!value) return { isValid: false, errorText: 'Please enter an address' }
|
||||
|
||||
return {
|
||||
isValid: isValidEthereumAddress(value) || floCrypto.validateAddr(value),
|
||||
isValid: isValidBlockchainAddress(value),
|
||||
errorText: `Invalid address.`
|
||||
}
|
||||
})
|
||||
@ -1858,11 +2116,44 @@
|
||||
removeNotificationBadge('#chat_page_button')
|
||||
if (floGlobals.idInterval)
|
||||
clearInterval(floGlobals.idInterval)
|
||||
let showingFloID = true
|
||||
// alternating between floID and btcID every 10 seconds
|
||||
let activeChain = localStorage.getItem(`${floGlobals.application}#activeChain`) || 'FLO';
|
||||
let currentIdIndex = 0
|
||||
const allIds = [
|
||||
{ id: floGlobals.myFloID, label: 'FLO' },
|
||||
{ id: floGlobals.myBtcID, label: 'BTC' },
|
||||
{ id: floGlobals.myEthID, label: 'ETH' },
|
||||
{ id: floGlobals.myAvaxID, label: 'AVAX' },
|
||||
{ id: floGlobals.myBscID, label: 'BSC' },
|
||||
{ id: floGlobals.myMaticID, label: 'MATIC' },
|
||||
{ id: floGlobals.myArbID, label: 'ARB' },
|
||||
{ id: floGlobals.myOpID, label: 'OP' },
|
||||
{ id: floGlobals.myHbarID, label: 'HBAR' },
|
||||
{ id: floGlobals.myXrpID, label: 'XRP' },
|
||||
{ id: floGlobals.mySuiID, label: 'SUI' },
|
||||
{ id: floGlobals.myTonID, label: 'TON' },
|
||||
{ id: floGlobals.myTronID, label: 'TRON' },
|
||||
{ id: floGlobals.myDogeID, label: 'DOGE' },
|
||||
{ id: floGlobals.myLtcID, label: 'LTC' },
|
||||
{ id: floGlobals.myBchID, label: 'BCH' },
|
||||
{ id: floGlobals.myDotID, label: 'DOT' },
|
||||
{ id: floGlobals.myAlgoID, label: 'ALGO' },
|
||||
{ id: floGlobals.myXlmID, label: 'XLM' },
|
||||
{ id: floGlobals.mySolID, label: 'SOL' },
|
||||
{ id: floGlobals.myAdaID, label: 'ADA' }
|
||||
];
|
||||
|
||||
const idList = allIds.filter(item => {
|
||||
if (!item.id) return false;
|
||||
if (activeChain === 'XRP') return item.label === 'XRP';
|
||||
if (item.label === 'FLO') return true; // Always include FLO
|
||||
if (item.label === activeChain) return true; // Include active chain
|
||||
// For EVM compatible chains which share the ETH ID:
|
||||
if (['ETH', 'AVAX', 'BSC', 'MATIC', 'ARB', 'OP', 'HBAR'].includes(activeChain) && item.label === activeChain) return true;
|
||||
return false;
|
||||
});
|
||||
floGlobals.idInterval = setInterval(() => {
|
||||
document.querySelectorAll('.user-profile-id').forEach(el => el.textContent = showingFloID ? floGlobals.myFloID : floGlobals.myBtcID)
|
||||
showingFloID = !showingFloID
|
||||
currentIdIndex = (currentIdIndex + 1) % idList.length
|
||||
document.querySelectorAll('.user-profile-id').forEach(el => el.textContent = idList[currentIdIndex].id)
|
||||
}, 10000)
|
||||
break;
|
||||
case 'mail_page':
|
||||
@ -2348,6 +2639,134 @@
|
||||
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) => {
|
||||
const indexArr = localStorage.getItem(`${floGlobals.application}#privKey`);
|
||||
if (!indexArr) {
|
||||
reject('No login credentials found');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const indices = JSON.parse(indexArr);
|
||||
// Read from the app database where credentials are stored
|
||||
const promises = indices.map(idx => compactIDB.readData('credentials', idx, floGlobals.application));
|
||||
const shares = await Promise.all(promises);
|
||||
const secret = floCrypto.retrieveShamirSecret(shares);
|
||||
if (!secret) {
|
||||
reject('Failed to retrieve credentials');
|
||||
return;
|
||||
}
|
||||
if (secret.length === 52) {
|
||||
// Not password-secured, return directly
|
||||
resolve(secret);
|
||||
} else {
|
||||
// Password-secured, decrypt with password
|
||||
try {
|
||||
const privKey = Crypto.AES.decrypt(secret, password);
|
||||
resolve(privKey);
|
||||
} catch (e) {
|
||||
reject('Incorrect password');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Derive private-key-dependent blockchain addresses (XRP, SUI, TON, TRON, DOGE, DOT, ALGO) with password
|
||||
async function derivePrivKeyAddresses() {
|
||||
try {
|
||||
const password = await getPromptInput('Enter password to unlock addresses', '', { isPassword: true });
|
||||
if (!password) {
|
||||
notify('Password is required', 'error');
|
||||
return;
|
||||
}
|
||||
// Get private key using the password
|
||||
const privKey = await getPrivKeyWithPassword(password);
|
||||
if (typeof privKey === 'string' && privKey.length > 0) {
|
||||
// Derive all addresses
|
||||
try {
|
||||
floGlobals.myXrpID = convertWIFtoXrpAddress(privKey);
|
||||
} catch (e) {
|
||||
console.warn('XRP derivation failed:', e);
|
||||
}
|
||||
try {
|
||||
floGlobals.mySuiID = convertWIFtoSuiAddress(privKey);
|
||||
} catch (e) {
|
||||
console.warn('SUI derivation failed:', e);
|
||||
}
|
||||
try {
|
||||
floGlobals.myTonID = await convertWIFtoTonAddress(privKey);
|
||||
} catch (e) {
|
||||
console.warn('TON derivation failed:', e);
|
||||
}
|
||||
try {
|
||||
floGlobals.myTronID = convertWIFtoTronAddress(privKey);
|
||||
} catch (e) {
|
||||
console.warn('TRON derivation failed:', e);
|
||||
}
|
||||
try {
|
||||
floGlobals.myDogeID = convertWIFtoDogeAddress(privKey);
|
||||
} catch (e) {
|
||||
console.warn('DOGE derivation failed:', e);
|
||||
}
|
||||
try {
|
||||
floGlobals.myLtcID = convertWIFtoLitecoinAddress(privKey);
|
||||
} catch (e) {
|
||||
console.warn('LTC derivation failed:', e);
|
||||
}
|
||||
try {
|
||||
floGlobals.myBchID = convertWIFtoBitcoinCashAddress(privKey);
|
||||
} catch (e) {
|
||||
console.warn('BCH derivation failed:', e);
|
||||
}
|
||||
try {
|
||||
floGlobals.myDotID = await convertWIFtoPolkadotAddress(privKey);
|
||||
} catch (e) {
|
||||
console.warn('DOT derivation failed:', e);
|
||||
}
|
||||
try {
|
||||
floGlobals.myAlgoID = convertWIFtoAlgorandAddress(privKey);
|
||||
} catch (e) {
|
||||
console.warn('ALGO derivation failed:', e);
|
||||
}
|
||||
try {
|
||||
floGlobals.myXlmID = convertWIFtoStellarAddress(privKey);
|
||||
} catch (e) {
|
||||
console.warn('XLM derivation failed:', e);
|
||||
}
|
||||
try {
|
||||
floGlobals.mySolID = convertWIFtoSolanaAddress(privKey);
|
||||
} catch (e) {
|
||||
console.warn('SOL derivation failed:', e);
|
||||
}
|
||||
try {
|
||||
floGlobals.myAdaID = await convertWIFtoCardanoAddress(privKey);
|
||||
} catch (e) {
|
||||
console.warn('ADA derivation failed:', e);
|
||||
}
|
||||
// Re-render profile to show new addresses
|
||||
routeTo(window.location.hash);
|
||||
notify('Addresses unlocked successfully!', 'success');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to derive addresses:', e);
|
||||
notify(typeof e === 'string' ? e : 'Failed to unlock addresses', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
let _blockchainResolve = null;
|
||||
function resolveBlockchainSelection(chain) {
|
||||
closePopup('blockchain_select_popup');
|
||||
if (_blockchainResolve) {
|
||||
_blockchainResolve(chain);
|
||||
_blockchainResolve = null;
|
||||
}
|
||||
}
|
||||
|
||||
function getSignedIn(passwordType) {
|
||||
return new Promise((resolve, reject) => {
|
||||
routeTo(window.location.hash)
|
||||
@ -2368,22 +2787,148 @@
|
||||
} else {
|
||||
floGlobals.isPrivKeySecured = false;
|
||||
getRef('private_key_field').dataset.privateKey = ''
|
||||
getRef('private_key_field').setAttribute('placeholder', 'FLO/BTC/ETH private key');
|
||||
getRef('private_key_field').setAttribute('placeholder', 'Enter Blockchain Private Key');
|
||||
getRef('private_key_field').customValidation = (value) => {
|
||||
if (!value) return { isValid: false, errorText: 'Please enter a private key' }
|
||||
let isValid = false;
|
||||
let checkVal = value.startsWith('0x') ? value.substring(2) : value;
|
||||
|
||||
// Hex lengths
|
||||
if (/^[0-9a-fA-F]{64}$/.test(checkVal) || /^[0-9a-fA-F]{128}$/.test(checkVal)) {
|
||||
isValid = true;
|
||||
}
|
||||
else if (
|
||||
value.startsWith('suiprivkey1')
|
||||
) {
|
||||
isValid = true;
|
||||
} else if (value.startsWith('s') && value.length >= 29 && value.length <= 32) {
|
||||
isValid = true;
|
||||
} else if (value.startsWith('S') && value.length === 56) {
|
||||
isValid = true;
|
||||
} else {
|
||||
try { isValid = !!floCrypto.getPubKeyHex(value); } catch (e) { }
|
||||
}
|
||||
return {
|
||||
isValid: floCrypto.getPubKeyHex(value),
|
||||
errorText: `Invalid private key.<br> It's a long string of random characters usually starting with 'R'.`
|
||||
isValid,
|
||||
errorText: `Invalid private key.<br> Please enter a valid WIF or Hex string.`
|
||||
}
|
||||
};
|
||||
}
|
||||
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);
|
||||
// For users who prefix their hex keys with 0x
|
||||
if (privateKey.startsWith('0x')) privateKey = privateKey.substring(2);
|
||||
|
||||
let activeChain = null;
|
||||
|
||||
if (/^[0-9a-fA-F]{64}$/.test(privateKey)) { //if hex private key might be ethereum, ADA, SOL, TRON, DOT, HBAR
|
||||
activeChain = await new Promise(resolve => {
|
||||
_blockchainResolve = resolve;
|
||||
openPopup('blockchain_select_popup');
|
||||
});
|
||||
|
||||
if (!activeChain) {
|
||||
return;
|
||||
}
|
||||
let key = new Bitcoin.ECKey(privateKey);
|
||||
key.setCompressed(true);
|
||||
privateKey = key.getBitcoinWalletImportFormat();
|
||||
} else if (/^[0-9a-fA-F]{128}$/.test(privateKey)) {
|
||||
activeChain = await new Promise(resolve => {
|
||||
_blockchainResolve = resolve;
|
||||
openPopup('ton_algo_select_popup');
|
||||
});
|
||||
if (!activeChain) return;
|
||||
|
||||
// Convert 128-char ed25519 key to workable FLO WIF using the first 32-bytes (seed)
|
||||
let key = new Bitcoin.ECKey(privateKey.substring(0, 64));
|
||||
key.setCompressed(true);
|
||||
privateKey = key.getBitcoinWalletImportFormat();
|
||||
} else {
|
||||
if (privateKey.startsWith('suiprivkey1')) {
|
||||
activeChain = 'SUI';
|
||||
// Convert Bech32 SUI key to workable FLO WIF using the 32-byte seed
|
||||
try {
|
||||
let decoded = coinjs.bech32_decode(privateKey);
|
||||
if (!decoded) throw new Error("Invalid SUI private key checksum");
|
||||
let bytes = coinjs.bech32_convert(decoded.data, 5, 8, false);
|
||||
// bytes[0] is the scheme flag (0x00 for Ed25519)
|
||||
// bytes.slice(1) contains the 32-byte seed
|
||||
let seedHex = Crypto.util.bytesToHex(bytes.slice(1));
|
||||
let key = new Bitcoin.ECKey(seedHex);
|
||||
key.setCompressed(true);
|
||||
privateKey = key.getBitcoinWalletImportFormat();
|
||||
} catch (e) {
|
||||
console.error("Failed to decode SUI key", e);
|
||||
}
|
||||
}
|
||||
else if (privateKey.startsWith('s') && privateKey.length >= 29 && privateKey.length <= 32) {
|
||||
activeChain = 'XRP';
|
||||
// Convert XRP secret to workable FLO WIF using the private key bytes
|
||||
try {
|
||||
let wallet = xrpl.Wallet.fromSeed(privateKey);
|
||||
let privKeyHex = wallet.privateKey;
|
||||
// Remove ED prefix if present (Ed25519 keys)
|
||||
if (privKeyHex.startsWith('ED') || privKeyHex.startsWith('ed'))
|
||||
privKeyHex = privKeyHex.substring(2);
|
||||
let key = new Bitcoin.ECKey(privKeyHex);
|
||||
key.setCompressed(true);
|
||||
privateKey = key.getBitcoinWalletImportFormat();
|
||||
} catch (e) {
|
||||
console.error("Failed to decode XRP key", e);
|
||||
}
|
||||
}
|
||||
else if (privateKey.startsWith('Q')) activeChain = 'DOGE';
|
||||
else if (privateKey.startsWith('T') && privateKey.length === 51) activeChain = 'LTC';
|
||||
else if (privateKey.startsWith('K') || privateKey.startsWith('L') || privateKey.startsWith('5')) {
|
||||
activeChain = await new Promise(resolve => {
|
||||
_blockchainResolve = resolve;
|
||||
openPopup('btc_bch_select_popup');
|
||||
});
|
||||
if (!activeChain) return;
|
||||
}
|
||||
else if (privateKey.startsWith('S') && privateKey.length === 56) {
|
||||
activeChain = 'XLM';
|
||||
try {
|
||||
const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
let bits = 0;
|
||||
let value = 0;
|
||||
let output = [];
|
||||
for (let i = 0; i < privateKey.length; i++) {
|
||||
let val = BASE32_ALPHABET.indexOf(privateKey[i].toUpperCase());
|
||||
if (val < 0) throw new Error("Invalid Base32 character");
|
||||
value = (value << 5) | val;
|
||||
bits += 5;
|
||||
if (bits >= 8) {
|
||||
output.push((value >>> (bits - 8)) & 255);
|
||||
bits -= 8;
|
||||
}
|
||||
}
|
||||
// First byte is version, next 32 bytes are Ed25519 seed
|
||||
const seedBytes = output.slice(1, 33);
|
||||
const seedHex = Crypto.util.bytesToHex(seedBytes);
|
||||
let key = new Bitcoin.ECKey(seedHex);
|
||||
key.setCompressed(true);
|
||||
privateKey = key.getBitcoinWalletImportFormat();
|
||||
} catch (e) {
|
||||
console.error("Failed to decode XLM key", e);
|
||||
}
|
||||
}
|
||||
else if (privateKey.startsWith('R')) activeChain = 'FLO';
|
||||
else activeChain = 'UNKNOWN';
|
||||
}
|
||||
|
||||
if (activeChain) {
|
||||
localStorage.setItem(`${floGlobals.application}#activeChain`, activeChain);
|
||||
}
|
||||
|
||||
// Store password temporarily for deriving other blockchain addresses
|
||||
if (floGlobals.isPrivKeySecured) {
|
||||
floGlobals._loginPassword = privateKey;
|
||||
}
|
||||
resolve(privateKey);
|
||||
getRef('private_key_field').value = '';
|
||||
routeTo('loading');
|
||||
@ -2825,8 +3370,19 @@
|
||||
}
|
||||
},
|
||||
profile() {
|
||||
let activeChain = localStorage.getItem(`${floGlobals.application}#activeChain`);
|
||||
return html`
|
||||
<div class="grid gap-1-5">
|
||||
${activeChain === 'XRP' ? html`
|
||||
<div class="grid gap-0-5">
|
||||
<h4>XRP-only login</h4>
|
||||
<p>You are logged in with an XRP private key. Only the XRP address is available.</p>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My XRP (Ripple) address</b>
|
||||
<sm-copy class="user-xrp-id" value=${floGlobals.myXrpID}></sm-copy>
|
||||
</div>
|
||||
` : html`
|
||||
<div class="grid gap-0-5">
|
||||
<h4>
|
||||
BTC integrated with FLO
|
||||
@ -2839,16 +3395,95 @@
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My FLO address</b>
|
||||
<sm-copy class="user-flo-id" clip-text value=${floGlobals.myFloID}></sm-copy>
|
||||
<sm-copy class="user-flo-id" value=${floGlobals.myFloID}></sm-copy>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My Bitcoin address</b>
|
||||
<sm-copy class="user-btc-id" clip-text value=${floGlobals.myBtcID}></sm-copy>
|
||||
<sm-copy class="user-btc-id" value=${floGlobals.myBtcID}></sm-copy>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My Ethereum address</b>
|
||||
<sm-copy class="user-eth-id" clip-text value=${floGlobals.myEthID}></sm-copy>
|
||||
<sm-copy class="user-eth-id" value=${floGlobals.myEthID}></sm-copy>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My AVAX (Avalanche C-Chain) address</b>
|
||||
<sm-copy class="user-avax-id" value=${floGlobals.myAvaxID}></sm-copy>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My BSC (Binance Smart Chain) address</b>
|
||||
<sm-copy class="user-bsc-id" value=${floGlobals.myBscID}></sm-copy>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My MATIC (Polygon) address</b>
|
||||
<sm-copy class="user-matic-id" value=${floGlobals.myMaticID}></sm-copy>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My ARB (Arbitrum) address</b>
|
||||
<sm-copy class="user-arb-id" value=${floGlobals.myArbID}></sm-copy>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My OP (Optimism) address</b>
|
||||
<sm-copy class="user-op-id" value=${floGlobals.myOpID}></sm-copy>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My HBAR (Hedera) address</b>
|
||||
<sm-copy class="user-hbar-id" value=${floGlobals.myHbarID}></sm-copy>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My XRP (Ripple) address</b>
|
||||
<sm-copy class="user-xrp-id" value=${floGlobals.myXrpID || 'Not available'}></sm-copy>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My SUI address</b>
|
||||
<sm-copy class="user-sui-id" value=${floGlobals.mySuiID || 'Not available'}></sm-copy>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My TON address</b>
|
||||
<sm-copy class="user-ton-id" value=${floGlobals.myTonID || 'Not available'}></sm-copy>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My TRON address</b>
|
||||
<sm-copy class="user-tron-id" value=${floGlobals.myTronID || 'Not available'}></sm-copy>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My DOGE address</b>
|
||||
<sm-copy class="user-doge-id" value=${floGlobals.myDogeID || 'Not available'}></sm-copy>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My LTC (Litecoin) address</b>
|
||||
<sm-copy class="user-ltc-id" value=${floGlobals.myLtcID || 'Not available'}></sm-copy>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My BCH (Bitcoin Cash) address</b>
|
||||
<sm-copy class="user-bch-id" value=${floGlobals.myBchID || 'Not available'}></sm-copy>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My DOT (Polkadot) address</b>
|
||||
<sm-copy class="user-dot-id" value=${floGlobals.myDotID || 'Not available'}></sm-copy>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My ALGO (Algorand) address</b>
|
||||
<sm-copy class="user-algo-id" value=${floGlobals.myAlgoID || 'Not available'}></sm-copy>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My XLM (Stellar) address</b>
|
||||
<sm-copy class="user-xlm-id" value=${floGlobals.myXlmID || 'Not available'}></sm-copy>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My SOL (Solana) address</b>
|
||||
<sm-copy class="user-sol-id" value=${floGlobals.mySolID || 'Not available'}></sm-copy>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<b>My ADA (Cardano) address</b>
|
||||
<sm-copy class="user-ada-id" value=${floGlobals.myAdaID || 'Not available'}></sm-copy>
|
||||
</div>
|
||||
${(!floGlobals.myXrpID && floGlobals.isPrivKeySecured) ? html`
|
||||
<button class="button button--primary justify-self-start" onclick=${derivePrivKeyAddresses}>
|
||||
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>
|
||||
Unlock all addresses
|
||||
</button>
|
||||
` : ''}
|
||||
`}
|
||||
<button class="button button--danger justify-self-start" onclick="signOut()">Sign out</button>
|
||||
</div>
|
||||
<div class="grid gap-1">
|
||||
@ -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`<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
|
||||
@ -5282,4 +5945,4 @@
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
1019
messenger/scripts/blockchainAddresses.js
Normal file
1019
messenger/scripts/blockchainAddresses.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -392,7 +392,22 @@
|
||||
if (!address)
|
||||
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))
|
||||
}
|
||||
})
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user