diff --git a/CHANGELOG.md b/CHANGELOG.md index 82b935c..0b4126c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -635,4 +635,10 @@ ##### [BREAKING Changes] Removed getAssets() method * Removed function `getAssets()` to get the list of assets of all the accounts associated as it is moved to an api service. -* Updated avalanche, base and zkEVM controllers. \ No newline at end of file +* Updated avalanche, base and zkEVM controllers. + +### 2.5.1 (2024-02-20) + +* Refactored recover vault logic and generalized it for evm and non evm chains +* updated labeling for evm and non evm wallet accounts +* Integrated restore account logs for vault recovery \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 63a17fb..38b1cc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@getsafle/safle-vault", - "version": "2.5.0", + "version": "2.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@getsafle/safle-vault", - "version": "2.5.0", + "version": "2.5.1", "license": "MIT", "dependencies": { "@getsafle/safle-identity-wallet": "^1.3.0", diff --git a/package.json b/package.json index aada76a..3f7709f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@getsafle/safle-vault", - "version": "2.5.0", + "version": "2.5.1", "description": "Safle Vault is a non-custodial, flexible and highly available crypto wallet which can be used to access dapps, send/receive crypto and store identity. Vault SDK is used to manage the vault and provide methods to generate vault, add new accounts, update the state and also enable the user to perform several vault related operations.", "main": "src/index.js", "scripts": { diff --git a/src/lib/keyring.js b/src/lib/keyring.js index 6ce7e08..42cd72d 100644 --- a/src/lib/keyring.js +++ b/src/lib/keyring.js @@ -221,6 +221,8 @@ class Keyring { const acc = await this.getAccounts(); if (Chains.evmChains.hasOwnProperty(this.chain) || this.chain === 'ethereum') { + + let labelPrefix = 'EVM' const accounts = await this.keyringInstance.getAccounts(); const keyring = await this.keyringInstance.getKeyringForAccount(accounts[0]); @@ -229,7 +231,7 @@ class Keyring { const newAccount = await this.keyringInstance.getAccounts(); - this.decryptedVault.eth.public.push({ address: newAccount[newAccount.length - 1], isDeleted: false, isImported: false, label: `Wallet ${acc.response.length + 1}` }) + this.decryptedVault.eth.public.push({ address: newAccount[newAccount.length - 1], isDeleted: false, isImported: false, label: `${labelPrefix} Wallet ${acc.response.length + 1}` }) this.decryptedVault.eth.numberOfAccounts++; const encryptedVault = await helper.cryptography(JSON.stringify(this.decryptedVault), JSON.stringify(encryptionKey), 'encryption'); @@ -245,6 +247,8 @@ class Keyring { let newAddress; + let labelPrefix = Chains.nonEvmChains[this.chain] + if (this[this.chain] === undefined) { const keyringInstance = await helper.getCoinInstance(this.chain, mnemonic); @@ -254,14 +258,14 @@ class Keyring { newAddress = address; - const publicData = [ { address, isDeleted: false, isImported: false, label: `${this.chain[0].toUpperCase() + this.chain.slice(1)} Wallet ${acc.response ? acc.response.length + 1 : 1}` } ]; + const publicData = [ { address, isDeleted: false, isImported: false, label: `${labelPrefix} Wallet ${acc.response ? acc.response.length + 1 : 1}` } ]; this.decryptedVault[this.chain] = { public: publicData, numberOfAccounts: 1 }; } else { const { address } = await this[this.chain].addAccount(); newAddress = address; - (this.decryptedVault[this.chain] === undefined) ? this.decryptedVault[this.chain] = { public: [ { address: newAddress, isDeleted: false, isImported: false, label: `${this.chain[0].toUpperCase() + this.chain.slice(1)} Wallet ${acc.response.length + 1}` } ], numberOfAccounts: 1 } : this.decryptedVault[this.chain].public.push({ address: newAddress, isDeleted: false, isImported: false, label: `${this.chain[0].toUpperCase() + this.chain.slice(1)} Wallet ${acc.response.length + 1}` }); + (this.decryptedVault[this.chain] === undefined) ? this.decryptedVault[this.chain] = { public: [ { address: newAddress, isDeleted: false, isImported: false, label: `${labelPrefix} Wallet ${acc.response.length + 1}` } ], numberOfAccounts: 1 } : this.decryptedVault[this.chain].public.push({ address: newAddress, isDeleted: false, isImported: false, label: `${labelPrefix} Wallet ${acc.response.length + 1}` }); this.decryptedVault[this.chain].numberOfAccounts++; } @@ -643,6 +647,8 @@ class Keyring { } if (Chains.evmChains.hasOwnProperty(this.chain) || this.chain === 'ethereum') { + + let labelPrefix = 'EVM' const keyringInstance = await helper.getCoinInstance(this.chain); @@ -663,13 +669,14 @@ class Keyring { } if (this.decryptedVault.importedWallets === undefined) { - this.decryptedVault.importedWallets = { evmChains: { data: [{ address, privateKey: encryptedPrivKey, isDeleted: false, isImported: true, label: `Wallet ${numOfAcc + 1}` }] } }; + this.decryptedVault.importedWallets = { evmChains: { data: [{ address, privateKey: encryptedPrivKey, isDeleted: false, isImported: true, label: `${labelPrefix} Wallet ${numOfAcc + 1}` }] } }; } else if (this.decryptedVault.importedWallets.evmChains === undefined) { - this.decryptedVault.importedWallets.evmChains = { data: [{ address, privateKey: encryptedPrivKey, isDeleted: false, isImported: true, label: `Wallet ${numOfAcc + 1}` }] }; + this.decryptedVault.importedWallets.evmChains = { data: [{ address, privateKey: encryptedPrivKey, isDeleted: false, isImported: true, label: `${labelPrefix} Wallet ${numOfAcc + 1}` }] }; } else { - this.decryptedVault.importedWallets.evmChains.data.push({ address, privateKey: encryptedPrivKey, isDeleted: false, isImported: true, label: `Wallet ${numOfAcc + 1}` }); + this.decryptedVault.importedWallets.evmChains.data.push({ address, privateKey: encryptedPrivKey, isDeleted: false, isImported: true, label: `${labelPrefix} Wallet ${numOfAcc + 1}` }); } } else { + let labelPrefix = Chains.nonEvmChains[this.chain] const { response: mnemonic } = await this.exportMnemonic(pin); if (this[this.chain] === undefined) { @@ -699,16 +706,16 @@ class Keyring { if (this.decryptedVault.importedWallets === undefined) { let object = {}; - const data = [ { address, isDeleted: false, isImported: true, privateKey: encryptedPrivKey, label: `${this.chain[0].toUpperCase() + this.chain.slice(1)} Wallet ${numOfAcc + 1}` } ]; + const data = [ { address, isDeleted: false, isImported: true, privateKey: encryptedPrivKey, label: `${labelPrefix} Wallet ${numOfAcc + 1}` } ]; object[this.chain] = { data }; this.decryptedVault.importedWallets = object; } else if (this.decryptedVault.importedWallets[this.chain] === undefined) { - const data = [ { address, isDeleted: false, isImported: true, privateKey: encryptedPrivKey, label: `${this.chain[0].toUpperCase() + this.chain.slice(1)} Wallet ${numOfAcc + 1}` } ]; + const data = [ { address, isDeleted: false, isImported: true, privateKey: encryptedPrivKey, label: `${labelPrefix} Wallet ${numOfAcc + 1}` } ]; this.decryptedVault.importedWallets[this.chain] = { data }; } else { - this.decryptedVault.importedWallets[this.chain].data.push({ address, isDeleted: false, isImported: true, privateKey: encryptedPrivKey, label: `${this.chain[0].toUpperCase() + this.chain.slice(1)} Wallet ${numOfAcc + 1}` }); + this.decryptedVault.importedWallets[this.chain].data.push({ address, isDeleted: false, isImported: true, privateKey: encryptedPrivKey, label: `${labelPrefix} Wallet ${numOfAcc + 1}` }); } } diff --git a/src/lib/vault.js b/src/lib/vault.js index 1eee836..ac01833 100644 --- a/src/lib/vault.js +++ b/src/lib/vault.js @@ -90,13 +90,13 @@ class Vault extends Keyring { const privData = await helper.generatePrivData(mnemonic, pin); - const rawVault = { eth: { public: [ { address: accounts[0], isDeleted: false, isImported: false, label: 'Wallet 1' } ], private: privData, numberOfAccounts: 1 }} + const rawVault = { eth: { public: [ { address: accounts[0], isDeleted: false, isImported: false, label: 'EVM Wallet 1' } ], private: privData, numberOfAccounts: 1 }} this.initializeSupportedChainKeyringController(mnemonic); for (const chain of Object.keys(Chains.nonEvmChains)) { const {address: addedAcc } = await this[chain].addAccount(); - let label = `${chain.charAt(0).toUpperCase() + chain.substr(1).toLowerCase()} Wallet 1` + let label = `${Chains.nonEvmChains[chain]} Wallet 1` rawVault[chain] = { public: [ { address: addedAcc, isDeleted: false, isImported: false, label: label } ], numberOfAccounts: 1 } } @@ -129,8 +129,14 @@ class Vault extends Keyring { const vaultState = await this.keyringInstance.createNewVaultAndRestore(JSON.stringify(encryptionKey), mnemonic); - const accountsArray = await helper.removeEmptyAccounts(vaultState.keyrings[0].accounts[0], this.keyringInstance, vaultState, unmarshalApiKey, recoverMechanism, logs); - + let accountsArray = []; + if(recoverMechanism === 'transactions') { + accountsArray = await helper.getAccountsFromTransactions(vaultState.keyrings[0].accounts[0], this.keyringInstance, vaultState, unmarshalApiKey) + } + else if (recoverMechanism === 'logs') { + accountsArray = await helper.getAccountsFromLogs('ethereum', this.keyringInstance, vaultState, logs, vaultState.keyrings[0].accounts[0]) + } + const privData = await helper.generatePrivData(mnemonic, pin); const numberOfAccounts = accountsArray.length; @@ -141,14 +147,14 @@ class Vault extends Keyring { //generate other chain's keyring instance and get accounts from logs let obj = {} - for ( let chainData of nonEvmChainList) { - - const keyringInstance = await helper.getCoinInstance(chainData.toLowerCase(), mnemonic); + for ( let chain of nonEvmChainList) { + const keyringInstance = await helper.getCoinInstance(chain.toLowerCase(), mnemonic); - const accArray = await helper.getAccountsFromLogs(keyringInstance, vaultState, recoverMechanism, logs); + let {address} = await keyringInstance.addAccount(); + const accArray = await helper.getAccountsFromLogs(chain, keyringInstance, vaultState, logs, address); const numberOfAcc = accArray.length; - rawVault[chainData.toLowerCase()] = { public: accArray, numberOfAccounts: numberOfAcc } + rawVault[chain.toLowerCase()] = { public: accArray, numberOfAccounts: numberOfAcc } } diff --git a/src/utils/helper.js b/src/utils/helper.js index 234b156..0917ba8 100644 --- a/src/utils/helper.js +++ b/src/utils/helper.js @@ -26,128 +26,123 @@ async function generatePrivData(mnemonic, pin) { return priv; } - -async function removeEmptyAccounts(indexAddress, keyringInstance, vaultState, unmarshalApiKey, recoverMechanism, logs) { +async function getAccountsFromTransactions(indexAddress, keyringInstance, vaultState, unmarshalApiKey) { const keyring = keyringInstance.getKeyringsByType(vaultState.keyrings[0].type); let zeroCounter = 0; - let accountCheckList = []; let accountsArray = []; - accountsArray.push({ address: indexAddress, isDeleted: false, isImported: false, label: 'Wallet 1' }); - accountCheckList.push(indexAddress) - let labelCounter = 2; // as an initial wallet is already created above with label 'Wallet 1' - const chains = Object.keys(Chains.evmChains); - - let newAccountAddr = indexAddress - - if( recoverMechanism === 'logs'){ - for(let i=0; i < logs.length; i++){ - if (logs[i].action === 'add-account' && (chains.includes(logs[i].chain) || logs[i].chain === undefined)){ - if (accountCheckList.includes(newAccountAddr)) { - vaultState = await keyringInstance.addNewAccount(keyring[0]); - newAccountAddr = Web3.utils.toChecksumAddress(vaultState.keyrings[0].accounts[vaultState.keyrings[0].accounts.length - 1]) - } - - if (logs[i].address.toLowerCase() !== newAccountAddr.toLowerCase() && !accountCheckList.includes(logs[i].address.toLowerCase())) { - do { - const label = this.createWalletLabels('all', labelCounter); - accountsArray.push({ address: newAccountAddr.toLowerCase(), isDeleted: false, isImported: false, label, isExported: false }); - accountCheckList.push(newAccountAddr.toLowerCase()) - labelCounter++; - let vaultState = await keyringInstance.addNewAccount(keyring[0]); - newAccountAddr = Web3.utils.toChecksumAddress(vaultState.keyrings[0].accounts[vaultState.keyrings[0].accounts.length - 1]) - } - while(logs[i].address.toLowerCase() !== newAccountAddr.toLowerCase() && !accountCheckList.includes(logs[i].address.toLowerCase())) - } - - if (logs[i].address.toLowerCase() === newAccountAddr.toLowerCase()) { - const label = this.createWalletLabels('all', labelCounter); - accountsArray.push({ address: newAccountAddr.toLowerCase(), isDeleted: false, isImported: false, label, isExported: false }); - accountCheckList.push(newAccountAddr.toLowerCase()) - labelCounter++; - } - - } - if(logs[i].action === 'delete-account' && (chains.includes(logs[i].chain) || logs[i].chain === undefined)) { - let ind = accountsArray.findIndex((acc) => acc.address === Web3.utils.toChecksumAddress(logs[i].address)) - ind >= 0 ? accountsArray[ind].isDeleted = true : false; - } - } - - } else if( recoverMechanism === 'transactions'){ - do { - zeroCounter = 0; - for(let i=0; i < 5; i++) { - const vaultState = await keyringInstance.addNewAccount(keyring[0]); - - const ethActivity = await getETHTransactions(vaultState.keyrings[0].accounts[vaultState.keyrings[0].accounts.length - 1], 'ethereum', unmarshalApiKey); - const polygonActivity = await getPolygonTransactions(vaultState.keyrings[0].accounts[vaultState.keyrings[0].accounts.length - 1], 'polygon', unmarshalApiKey); - const bscActivity = await getBSCTransactions(vaultState.keyrings[0].accounts[vaultState.keyrings[0].accounts.length - 1], 'bsc', unmarshalApiKey); - const label = this.createWalletLabels('all', i+2); - - if (!ethActivity && !polygonActivity && !bscActivity) { - accountsArray.push({ address: vaultState.keyrings[0].accounts[vaultState.keyrings[0].accounts.length - 1], isDeleted: true, isImported: false, label, isExported: false }); - zeroCounter++; - } else { - accountsArray.push({ address: vaultState.keyrings[0].accounts[vaultState.keyrings[0].accounts.length - 1], isDeleted: false, isImported: false, label, isExported: false }); - zeroCounter = 0; - } + accountsArray.push({ address: indexAddress, isDeleted: false, isImported: false, label: 'EVM Wallet 1' }); + + do { + zeroCounter = 0; + for(let i=0; i < 5; i++) { + const vaultState = await keyringInstance.addNewAccount(keyring[0]); + + const ethActivity = await getETHTransactions(vaultState.keyrings[0].accounts[vaultState.keyrings[0].accounts.length - 1], 'ethereum', unmarshalApiKey); + const polygonActivity = await getPolygonTransactions(vaultState.keyrings[0].accounts[vaultState.keyrings[0].accounts.length - 1], 'polygon', unmarshalApiKey); + const bscActivity = await getBSCTransactions(vaultState.keyrings[0].accounts[vaultState.keyrings[0].accounts.length - 1], 'bsc', unmarshalApiKey); + const label = this.createWalletLabels('EVM', i+2); + + if (!ethActivity && !polygonActivity && !bscActivity) { + accountsArray.push({ address: vaultState.keyrings[0].accounts[vaultState.keyrings[0].accounts.length - 1], isDeleted: true, isImported: false, label, isExported: false }); + zeroCounter++; + } else { + accountsArray.push({ address: vaultState.keyrings[0].accounts[vaultState.keyrings[0].accounts.length - 1], isDeleted: false, isImported: false, label, isExported: false }); + zeroCounter = 0; } } - - while (zeroCounter < 5 ) } + + while (zeroCounter < 5 ) + return accountsArray; } -async function getAccountsFromLogs(keyringInstance, vaultState, recoverMechanism, logs) { - //if mech = transaction - generate one acc for bitcoin - - let accountsArray = []; - let accountCheckList = []; - let {address} = await keyringInstance.addAccount(); - const label = this.createWalletLabels('bitcoin', 1); - accountsArray.push({ address: address, isDeleted: false, isImported: false, label, isExported: false }); - accountCheckList.push(address) - let labelCounter = 2; - const chains = Object.keys(Chains.nonEvmChains); - - if( recoverMechanism === 'logs'){ - for(let i=0; i < logs.length; i++){ - if (logs[i].action === 'add-account' && (chains.includes(logs[i].chain))){ - - if (accountCheckList.includes(address)) { - address = (await keyringInstance.addAccount()).address; - } - - if (logs[i].address.toLowerCase() !== address.toLowerCase() && !accountCheckList.includes(logs[i].address.toLowerCase())) { - do { - const label = this.createWalletLabels('bitcoin', labelCounter); - accountsArray.push({ address: address.toLowerCase(), isDeleted: false, isImported: false, label, isExported: false }); - accountCheckList.push(address.toLowerCase()) - labelCounter++; - address = (await keyringInstance.addAccount()).address; - } - while(logs[i].address.toLowerCase() !== address.toLowerCase() && !accountCheckList.includes(logs[i].address.toLowerCase())) +async function getAccountsFromLogs(chain, chainInstance, vaultState, logs = [], indexAddress) { + + const accountsMap = new Map(); + let generatedAddress = indexAddress; + let labelCounter = 1; + + // Create a new address based on the blockchain type + const createNewAddress = async (chain, chainInstance) => { + let address; + if (chain === 'ethereum' || chain === undefined) { + keyring = chainInstance.getKeyringsByType(vaultState.keyrings[0].type); + vaultState = await chainInstance.addNewAccount(keyring[0]); + address = (Web3.utils.toChecksumAddress(vaultState.keyrings[0].accounts[vaultState.keyrings[0].accounts.length - 1])).toLowerCase(); + } else { + address = (await chainInstance.addAccount()).address.toLowerCase(); + } + return address; + }; + + // Create an account object with a label based on the blockchain type + const createAccountObject = async (generatedAddress) => { + const labelPrefix = chain === 'ethereum' || chain === undefined || Chains.evmChains[chain] ? 'EVM' : Chains.nonEvmChains[chain]; + const label = this.createWalletLabels(labelPrefix, labelCounter++); + return { address: generatedAddress, isDeleted: false, isImported: false, label, isExported: false }; + }; + + // If indexAddress is empty, return the values of the accounts map + if (!indexAddress){ + return []; + } else { + // Set the indexAddress account in the accounts map + accountsMap.set(indexAddress, await createAccountObject(indexAddress)); + } + if(!logs.length) { + return Array.from(accountsMap.values()); + } + + // Add account if verified based on the log address + const addAccountIfVerified = async (logAddress) => { + if (accountsMap.has(logAddress)) { + let account = accountsMap.get(logAddress); + if (account.isDeleted === true) { + account.isDeleted = false; + } + } + + if (accountsMap.has(generatedAddress)) { + generatedAddress = await createNewAddress(chain, chainInstance); + } + + while (logAddress !== generatedAddress && !accountsMap.has(logAddress)) { + accountsMap.set(generatedAddress, await createAccountObject(generatedAddress)); + generatedAddress = await createNewAddress(chain, chainInstance); + } + + if (logAddress === generatedAddress) { + accountsMap.set(generatedAddress, await createAccountObject(generatedAddress)); + } + }; + + // Iterate through the logs and update the accounts map accordingly + for (let log of logs) { + const logAddress = log?.address?.toLowerCase(); + if (log?.chain === chain || (chain === 'ethereum' && log?.chain === undefined)) { + if (log.action === 'add-account') { + await addAccountIfVerified(logAddress); + } + else if(log.action === 'restore-account') { + const account = accountsMap.get(logAddress); + if (account) { + account.isDeleted = false; } - - if (logs[i].address.toLowerCase() === address.toLowerCase()) { - const label = this.createWalletLabels('bitcoin', labelCounter); - accountsArray.push({ address: address.toLowerCase(), isDeleted: false, isImported: false, label, isExported: false }); - accountCheckList.push(address.toLowerCase()) - labelCounter ++; + } else if (log.action === 'delete-account') { + const account = accountsMap.get(logAddress); + if (account) { + account.isDeleted = true; } - - } - if(logs[i].action === 'delete-account' && (chains.includes(logs[i].chain))) { - let ind = accountsArray.findIndex((acc) => acc.address.toLowerCase() === logs[i].address.toLowerCase()) - ind >= 0 ? accountsArray[ind].isDeleted = true : false; } } } - return accountsArray; + + // Return the values of the accounts map as an array + return Array.from(accountsMap.values()); } async function getETHTransactions(address, network, unmarshalApiKey) { @@ -228,32 +223,15 @@ function validateEncryptionKey(data, encryptionKey, encryptor, isCustomEncryptor } -function createWalletLabels(labelObj = 'all', walletIndex = 1) { - let labels = {}; - - const chains = Object.keys(Chains.evmChains); - - if (labelObj === 'all') { - chains.forEach(chain => labels[chain] = `${chain.charAt(0).toUpperCase() + chain.substr(1).toLowerCase()} Wallet ${walletIndex}` ); - } - else if (labelObj === 'bitcoin') { - labels = `${labelObj.charAt(0).toUpperCase() + labelObj.substr(1).toLowerCase()} Wallet ${walletIndex}`; - } - else { - chains.forEach(chain => { - if (labels[chain] !== undefined) { - labels[chain] = `${chain.charAt(0).toUpperCase() + chain.substr(1).toLowerCase()} Wallet ${walletIndex}`; - } - }) - } - +function createWalletLabels(labelPrefix, walletIndex = 1) { + let labels = `${labelPrefix} Wallet ${walletIndex}`; return labels; } module.exports = { stringToArrayBuffer, generatePrivData, - removeEmptyAccounts, + getAccountsFromTransactions, getAccountsFromLogs, getCoinInstance, cryptography,