diff --git a/packages/sysweb3-keyring/package.json b/packages/sysweb3-keyring/package.json index acaa147..9c416e9 100644 --- a/packages/sysweb3-keyring/package.json +++ b/packages/sysweb3-keyring/package.json @@ -1,6 +1,6 @@ { "name": "@pollum-io/sysweb3-keyring", - "version": "1.0.487", + "version": "1.0.488", "description": "Keyring Manager for UTXO and Web3 Wallets", "main": "cjs/index.js", "types": "types/index.d.ts", @@ -33,7 +33,7 @@ "@ledgerhq/logs": "^6.10.1", "@pollum-io/sysweb3-core": "^1.0.27", "@pollum-io/sysweb3-network": "^1.0.95", - "@pollum-io/sysweb3-utils": "^1.1.235", + "@pollum-io/sysweb3-utils": "^1.1.236", "@trezor/connect-web": "^9.1.5", "@trezor/connect-webextension": "^9.3.0", "@trezor/utxo-lib": "^1.0.12", diff --git a/packages/sysweb3-keyring/src/keyring-manager.ts b/packages/sysweb3-keyring/src/keyring-manager.ts index 9605d24..4b656fd 100644 --- a/packages/sysweb3-keyring/src/keyring-manager.ts +++ b/packages/sysweb3-keyring/src/keyring-manager.ts @@ -1,6 +1,10 @@ //@ts-nocheck @ts-ignore +import ecc from '@bitcoinerlab/secp256k1'; +import { BIP32Factory } from 'bip32'; import { generateMnemonic, validateMnemonic, mnemonicToSeed } from 'bip39'; import BIP84, { fromZPrv } from 'bip84'; +import * as bjs from 'bitcoinjs-lib'; +import bs58check from 'bs58check'; import crypto from 'crypto'; import CryptoJS from 'crypto-js'; import { hdkey } from 'ethereumjs-wallet'; @@ -64,29 +68,28 @@ export interface ISysAccountWithId extends ISysAccount { const ethHdPath: Readonly = "m/44'/60'/0'"; export class KeyringManager implements IKeyringManager { + public trezorSigner: TrezorKeyring; + public ledgerSigner: LedgerKeyring; + public activeChain: INetworkType; + public initialTrezorAccountState: IKeyringAccountState; + public initialLedgerAccountState: IKeyringAccountState; + public utf8Error: boolean; + //transactions objects + public ethereumTransaction: IEthereumTransactions; + public syscoinTransaction: ISyscoinTransactions; + wallet: IWalletState; //todo change this name, we will use wallets for another const -> Maybe for defaultInitialState / defaultStartState; private storage: any; //todo type - private wallet: IWalletState; //todo change this name, we will use wallets for another const -> Maybe for defaultInitialState / defaultStartState; - //local variables private hd: SyscoinHDSigner | null; private syscoinSigner: SyscoinMainSigner | undefined; - public trezorSigner: TrezorKeyring; - public ledgerSigner: LedgerKeyring; private memMnemonic: string; private memPassword: string; private currentSessionSalt: string; private sessionPassword: string; - private sessionMnemonic: string; + private sessionMnemonic: string; // can be a mnemonic or a zprv, can be changed to a zprv when using an imported wallet + private sessionMainMnemonic: string; // mnemonic of the main account, does not change private sessionSeed: string; - public activeChain: INetworkType; - public initialTrezorAccountState: IKeyringAccountState; - public initialLedgerAccountState: IKeyringAccountState; private trezorAccounts: any[]; - public utf8Error: boolean; - - //transactions objects - public ethereumTransaction: IEthereumTransactions; - public syscoinTransaction: ISyscoinTransactions; constructor(opts?: IkeyringManagerOpts | null) { this.storage = sysweb3.sysweb3Di.getStateStorageDb(); @@ -110,6 +113,7 @@ export class KeyringManager implements IKeyringManager { this.memMnemonic = ''; this.sessionSeed = ''; this.sessionMnemonic = ''; + this.sessionMainMnemonic = ''; this.memPassword = ''; //Lock wallet in case opts.password has been provided this.initialTrezorAccountState = initialActiveTrezorAccountState; this.initialLedgerAccountState = initialActiveLedgerAccountState; @@ -133,60 +137,6 @@ export class KeyringManager implements IKeyringManager { ); } - // ===================================== AUXILIARY METHOD - FOR TRANSACTIONS CLASSES ===================================== // - private getDecryptedPrivateKey = (): { - address: string; - decryptedPrivateKey: string; - } => { - try { - if (!this.sessionPassword) - throw new Error('Wallet is locked cant proceed with transaction'); - if (this.activeChain !== INetworkType.Ethereum) - throw new Error('Switch to EVM chain'); - const { accounts, activeAccountId, activeAccountType } = this.wallet; - - const { xprv, address } = accounts[activeAccountType][activeAccountId]; - const decryptedPrivateKey = CryptoJS.AES.decrypt( - xprv, - this.sessionPassword - ).toString(CryptoJS.enc.Utf8); - - return { - address, - decryptedPrivateKey, - }; - } catch (error) { - console.log('ERROR getDecryptedPrivateKey', { - error, - values: { - memPass: this.memPassword, - memMnemonic: this.memMnemonic, - vaultKeys: this.storage.get('vault-keys'), - vault: getDecryptedVault(this.memPassword), - }, - }); - this.validateAndHandleErrorByMessage(error.message); - } - }; - private getSigner = (): { - hd: SyscoinHDSigner; - main: any; //TODO: Type this following syscoinJSLib interface - } => { - if (!this.sessionPassword) { - throw new Error('Wallet is locked cant proceed with transaction'); - } - if (this.activeChain !== INetworkType.Syscoin) { - throw new Error('Switch to UTXO chain'); - } - if (!this.syscoinSigner || !this.hd) { - throw new Error( - 'Wallet is not initialised yet call createKeyringVault first' - ); - } - - return { hd: this.hd, main: this.syscoinSigner }; - }; - // ===================================== PUBLIC METHODS - KEYRING MANAGER FOR HD - SYS ALL ===================================== // public setStorage = (client: any) => this.storage.setClient(client); @@ -249,6 +199,8 @@ export class KeyringManager implements IKeyringManager { this.sessionPassword ).toString(); + this.sessionMainMnemonic = this.sessionMnemonic; + this.memMnemonic = ''; } if (prvPwd) this.updateWalletKeys(prvPwd); @@ -260,40 +212,6 @@ export class KeyringManager implements IKeyringManager { }); }; - private validateAndHandleErrorByMessage(message: string) { - const utf8ErrorMessage = 'Malformed UTF-8 data'; - if ( - message.includes(utf8ErrorMessage) || - message.toLowerCase().includes(utf8ErrorMessage.toLowerCase()) - ) { - this.storage.set('utf8Error', { hasUtf8Error: true }); - } - } - - private async recoverLastSessionPassword(pwd: string) { - //As before locking the wallet we always keep the value of the last currentSessionSalt correctly stored in vault, - //we use the value in vault instead of the one present in the class to get the last correct value for sessionPassword - const initialVaultKeys = await this.storage.get('vault-keys'); - - //Here we need to validate if user has the currentSessionSalt in the vault-keys, because for Pali Users that - //already has accounts created in some old version this value will not be in the storage. So we need to check it - //and if user doesn't have we set it and if has we use the storage value - if ( - !this.currentSessionSalt || - typeof initialVaultKeys.currentSessionSalt === 'undefined' || - this.currentSessionSalt === '' - ) { - this.storage.set('vault-keys', { - ...initialVaultKeys, - currentSessionSalt: this.currentSessionSalt, - }); - - return this.encryptSHA512(pwd, this.currentSessionSalt); - } - - return this.encryptSHA512(pwd, initialVaultKeys.currentSessionSalt); - } - public unlock = async ( password: string, isForPvtKey?: boolean @@ -371,6 +289,7 @@ export class KeyringManager implements IKeyringManager { //Sysweb3 only allow segwit change addresses for now return await this.hd.getNewChangeAddress(true, 84); }; + public getChangeAddress = async (id: number): Promise => { if (this.hd === null) throw new Error('HD not created yet, unlock or initialize wallet first'); @@ -379,6 +298,7 @@ export class KeyringManager implements IKeyringManager { this.hd.setAccountIndex(this.wallet.activeAccountId); return address; }; + public updateReceivingAddress = async (): Promise => { const { activeAccountType, accounts, activeAccountId } = this.wallet; const { xpub } = accounts[activeAccountType][activeAccountId]; @@ -452,9 +372,11 @@ export class KeyringManager implements IKeyringManager { throw new Error( 'Initialise wallet first, cant change accounts without an active HD' ); - if (accountType === KeyringAccountType.HDAccount && this.hd) { + + if (this.wallet.accounts[accountType][accountId].address && this.hd) { this.hd.setAccountIndex(accountId); } + const accounts = this.wallet.accounts[accountType]; if (!accounts[accountId].xpub) throw new Error('Account not set'); this.wallet = { @@ -462,6 +384,20 @@ export class KeyringManager implements IKeyringManager { activeAccountId: accounts[accountId].id, activeAccountType: accountType, }; + + if (this.activeChain === INetworkType.Syscoin) { + const isHDAccount = accountType === KeyringAccountType.HDAccount; + + this.sessionMnemonic = isHDAccount + ? this.sessionMainMnemonic + : accounts[accountId].xprv; + + const { rpc, isTestnet } = await this.getSignerUTXO( + this.wallet.activeNetwork + ); + + await this.updateUTXOAccounts(rpc, isTestnet); + } }; public getAccountById = ( @@ -652,7 +588,6 @@ export class KeyringManager implements IKeyringManager { }, }, }; - // this.wallet.networks[chain] = updatedNetworks; }; public setSignerNetwork = async ( @@ -674,6 +609,7 @@ export class KeyringManager implements IKeyringManager { const prevActiveChainState = this.activeChain; const prevHDState = this.hd; const prevSyscoinSignerState = this.syscoinSigner; + try { if (chain === INetworkType.Syscoin) { const { rpc, isTestnet } = await this.getSignerUTXO(network); @@ -696,6 +632,7 @@ export class KeyringManager implements IKeyringManager { }, activeNetwork: network, }; + this.wallet.activeNetwork = network; this.activeChain = networkChain; @@ -714,6 +651,7 @@ export class KeyringManager implements IKeyringManager { vault: getDecryptedVault(this.memPassword), }, }); + this.validateAndHandleErrorByMessage(err.message); //Rollback to previous values @@ -721,6 +659,7 @@ export class KeyringManager implements IKeyringManager { this.wallet = prevWalletState; this.activeChain = prevActiveChainState; + if (this.activeChain === INetworkType.Ethereum) { this.ethereumTransaction.setWeb3Provider(this.wallet.activeNetwork); } else if (this.activeChain === INetworkType.Syscoin) { @@ -760,7 +699,9 @@ export class KeyringManager implements IKeyringManager { }; public isSeedValid = (seedPhrase: string) => validateMnemonic(seedPhrase); + public createNewSeed = () => generateMnemonic(); + public setSeed = (seedPhrase: string) => { if (validateMnemonic(seedPhrase)) { this.memMnemonic = seedPhrase; @@ -770,6 +711,8 @@ export class KeyringManager implements IKeyringManager { this.sessionPassword ).toString(); + this.sessionMainMnemonic = this.sessionMnemonic; + this.memMnemonic = ''; } return seedPhrase; @@ -777,11 +720,6 @@ export class KeyringManager implements IKeyringManager { throw new Error('Invalid Seed'); }; - private getAccountsState = () => { - const { activeAccountId, accounts, activeAccountType, activeNetwork } = - this.wallet; - return { activeAccountId, accounts, activeAccountType, activeNetwork }; - }; public getUTXOState = () => { if (this.activeChain !== INetworkType.Syscoin) { throw new Error('Cannot get state in a ethereum network'); @@ -851,7 +789,9 @@ export class KeyringManager implements IKeyringManager { xprv: undefined, }; }; + public getNetwork = () => this.wallet.activeNetwork; + public verifyIfIsTestnet = () => { const { chainId } = this.wallet.activeNetwork; if (this.wallet.networks.syscoin[chainId] && this.hd) { @@ -859,13 +799,256 @@ export class KeyringManager implements IKeyringManager { } return undefined; }; + public createEthAccount = (privateKey: string) => new ethers.Wallet(privateKey); + public getAddress = async ( + xpub: string, + isChangeAddress: boolean, + index: number + ) => { + const { hd, main } = this.getSigner(); + const options = 'tokens=used&details=tokens'; + + const { tokens } = await sys.utils.fetchBackendAccount( + main.blockbookURL, + xpub, + options, + true, + undefined + ); + const { receivingIndex, changeIndex } = + this.setLatestIndexesFromXPubTokens(tokens); + + const currentAccount = new BIP84.fromZPub( + xpub, + hd.Signer.pubTypes, + hd.Signer.networks + ); + + this.trezorAccounts.push(currentAccount); + + const address = this.trezorAccounts[index] + ? (this.trezorAccounts[index].getAddress( + isChangeAddress ? changeIndex : receivingIndex, + isChangeAddress, + 84 + ) as string) + : (this.trezorAccounts[this.trezorAccounts.length - 1].getAddress( + isChangeAddress ? changeIndex : receivingIndex, + isChangeAddress, + 84 + ) as string); + + return address; + }; + + public logout = () => { + this.sessionPassword = ''; + this.sessionSeed = ''; + this.currentSessionSalt = ''; + this.sessionMnemonic = ''; + this.sessionMainMnemonic = ''; + }; + + public async importAccount(privKey: string, label?: string) { + const importedAccount = await this._getPrivateKeyAccountInfos( + privKey, + label + ); + + this.wallet.accounts.Imported[importedAccount.id] = importedAccount; + + return importedAccount; + } + + //TODO: validate updateAllPrivateKeyAccounts updating 2 accounts or more works properly + public async updateAllPrivateKeyAccounts() { + try { + const accountPromises = Object.values( + this.wallet.accounts[KeyringAccountType.Imported] + ).map(async (account) => await this.updatePrivWeb3Account(account)); + + const updatedWallets = await Promise.all(accountPromises); + + this.wallet.accounts[KeyringAccountType.Imported] = updatedWallets; + } catch (error) { + console.log('ERROR updateAllPrivateKeyAccounts', { + error, + values: { + memPass: this.memPassword, + memMnemonic: this.memMnemonic, + vaultKeys: this.storage.get('vault-keys'), + vault: getDecryptedVault(this.memPassword), + }, + }); + this.validateAndHandleErrorByMessage(error.message); + } + } + + public updateAccountLabel = ( + label: string, + accountId: number, + accountType: KeyringAccountType + ) => { + this.wallet.accounts[accountType][accountId].label = label; + }; + + public validateZprv(zprv: string) { + const isTestnet = this.verifyIfIsTestnet(); + + const networks = { + mainnet: { + messagePrefix: '\x18Syscoin Signed Message:\n', + bech32: 'sys', + bip32: { + public: 0x04b24746, // zpub + private: 0x04b2430c, // zprv + }, + pubKeyHash: 0x3f, + scriptHash: 0x05, + slip44: 57, + wif: 0x80, + }, + testnet: { + messagePrefix: '\x18Syscoin Signed Message:\n', + bech32: 'tsys', + bip32: { + public: 0x043587cf, // tpub + private: 0x04358394, // tprv + }, + pubKeyHash: 0x6f, + scriptHash: 0xc4, + slip44: 1, + wif: 0xef, + }, + }; + + const network = isTestnet ? networks.testnet : networks.mainnet; + + try { + const bip32 = BIP32Factory(ecc); + const decoded = bs58check.decode(zprv); + + if (decoded.length !== 78) { + throw new Error('Invalid length for a BIP-32 key'); + } + + const node = bip32.fromBase58(zprv, network); + + if (!node.privateKey) { + throw new Error('Private key not found in zprv'); + } + + if (!ecc.isPrivate(node.privateKey)) { + throw new Error('Invalid private key for secp256k1 curve.'); + } + + return { + isValid: true, + node, + network, + message: 'The zprv is valid.', + }; + } catch (error) { + return { isValid: false, message: error.message }; + } + } + /** * PRIVATE METHODS */ + // ===================================== AUXILIARY METHOD - FOR TRANSACTIONS CLASSES ===================================== // + private getDecryptedPrivateKey = (): { + address: string; + decryptedPrivateKey: string; + } => { + try { + const { accounts, activeAccountId, activeAccountType } = this.wallet; + if (!this.sessionPassword) + throw new Error('Wallet is locked cant proceed with transaction'); + + const { xprv, address } = accounts[activeAccountType][activeAccountId]; + const decryptedPrivateKey = CryptoJS.AES.decrypt( + xprv, + this.sessionPassword + ).toString(CryptoJS.enc.Utf8); + + return { + address, + decryptedPrivateKey, + }; + } catch (error) { + console.log('ERROR getDecryptedPrivateKey', { + error, + values: { + memPass: this.memPassword, + memMnemonic: this.memMnemonic, + vaultKeys: this.storage.get('vault-keys'), + vault: getDecryptedVault(this.memPassword), + }, + }); + this.validateAndHandleErrorByMessage(error.message); + } + }; + + private getSigner = (): { + hd: SyscoinHDSigner; + main: any; //TODO: Type this following syscoinJSLib interface + } => { + if (!this.sessionPassword) { + throw new Error('Wallet is locked cant proceed with transaction'); + } + if (this.activeChain !== INetworkType.Syscoin) { + throw new Error('Switch to UTXO chain'); + } + if (!this.syscoinSigner || !this.hd) { + throw new Error( + 'Wallet is not initialised yet call createKeyringVault first' + ); + } + + return { hd: this.hd, main: this.syscoinSigner }; + }; + + private validateAndHandleErrorByMessage(message: string) { + const utf8ErrorMessage = 'Malformed UTF-8 data'; + if ( + message.includes(utf8ErrorMessage) || + message.toLowerCase().includes(utf8ErrorMessage.toLowerCase()) + ) { + this.storage.set('utf8Error', { hasUtf8Error: true }); + } + } + + private async recoverLastSessionPassword(pwd: string) { + //As before locking the wallet we always keep the value of the last currentSessionSalt correctly stored in vault, + //we use the value in vault instead of the one present in the class to get the last correct value for sessionPassword + const initialVaultKeys = await this.storage.get('vault-keys'); + + //Here we need to validate if user has the currentSessionSalt in the vault-keys, because for Pali Users that + //already has accounts created in some old version this value will not be in the storage. So we need to check it + //and if user doesn't have we set it and if has we use the storage value + if (!this.currentSessionSalt || !initialVaultKeys?.currentSessionSalt) { + this.storage.set('vault-keys', { + ...initialVaultKeys, + currentSessionSalt: this.currentSessionSalt, + }); + + return this.encryptSHA512(pwd, this.currentSessionSalt); + } + + return this.encryptSHA512(pwd, initialVaultKeys.currentSessionSalt); + } + + private getAccountsState = () => { + const { activeAccountId, accounts, activeAccountType, activeNetwork } = + this.wallet; + return { activeAccountId, accounts, activeAccountType, activeNetwork }; + }; + /** * * @param password @@ -960,6 +1143,7 @@ export class KeyringManager implements IKeyringManager { this.hd.Signer.accounts.push(derivedAccount); this.hd.setAccountIndex(accountId); } + const xpub = this.hd.getAccountXpub(); const xprv = this.getEncryptedXprv(); @@ -968,12 +1152,15 @@ export class KeyringManager implements IKeyringManager { accountId ); + const isImported = + this.wallet.activeAccountType === KeyringAccountType.Imported; + const createdAccount = { xprv, - isImported: false, + isImported, ...basicAccountInfo, }; - this.wallet.accounts[KeyringAccountType.HDAccount][accountId] = + this.wallet.accounts[this.wallet.activeAccountType][accountId] = createdAccount; } catch (error) { console.log('ERROR addUTXOAccount', { @@ -1183,47 +1370,6 @@ export class KeyringManager implements IKeyringManager { return ledgerAccount; } - public getAddress = async ( - xpub: string, - isChangeAddress: boolean, - index: number - ) => { - const { hd, main } = this.getSigner(); - const options = 'tokens=used&details=tokens'; - - const { tokens } = await sys.utils.fetchBackendAccount( - main.blockbookURL, - xpub, - options, - true, - undefined - ); - const { receivingIndex, changeIndex } = - this.setLatestIndexesFromXPubTokens(tokens); - - const currentAccount = new BIP84.fromZPub( - xpub, - hd.Signer.pubTypes, - hd.Signer.networks - ); - - this.trezorAccounts.push(currentAccount); - - const address = this.trezorAccounts[index] - ? (this.trezorAccounts[index].getAddress( - isChangeAddress ? changeIndex : receivingIndex, - isChangeAddress, - 84 - ) as string) - : (this.trezorAccounts[this.trezorAccounts.length - 1].getAddress( - isChangeAddress ? changeIndex : receivingIndex, - isChangeAddress, - 84 - ) as string); - - return address; - }; - private getFormattedBackendAccount = async ({ url, xpub, @@ -1256,6 +1402,7 @@ export class KeyringManager implements IKeyringManager { } catch (e) { throw new Error(`Error fetching account from network ${url}: ${e}`); } + return { address: stealthAddr, xpub: xpub, @@ -1325,12 +1472,16 @@ export class KeyringManager implements IKeyringManager { //todo network type private async addNewAccountToSyscoinChain(network: any, label?: string) { try { - if (this.hd === null || !this.hd.mnemonic) { + if (this.hd === null || !this.hd.mnemonicOrZprv) { throw new Error( 'Keyring Vault is not created, should call createKeyringVault first ' ); } + if (this.wallet.activeAccountType !== KeyringAccountType.HDAccount) { + await this.setActiveAccount(0, KeyringAccountType.HDAccount); + } + const id = this.hd.createAccount(84); const xpub = this.hd.getAccountXpub(); const xprv = this.getEncryptedXprv(); @@ -1461,11 +1612,10 @@ export class KeyringManager implements IKeyringManager { private updateWeb3Accounts = async () => { try { const { accounts, activeAccountId, activeAccountType } = this.wallet; + const hdAccounts = Object.values(accounts[KeyringAccountType.HDAccount]); //Account of HDAccount is always initialized as it is required to create a network - for (const index in Object.values( - accounts[KeyringAccountType.HDAccount] - )) { + for (const index in hdAccounts) { const id = Number(index); const label = @@ -1473,6 +1623,7 @@ export class KeyringManager implements IKeyringManager { await this.setDerivedWeb3Accounts(id, label); } + if ( accounts[KeyringAccountType.Imported] && Object.keys(accounts[KeyringAccountType.Imported]).length > 0 @@ -1589,19 +1740,33 @@ export class KeyringManager implements IKeyringManager { if (!this.sessionPassword) { throw new Error('Unlock wallet first'); } + + const accounts = this.wallet.accounts[this.wallet.activeAccountType]; + const isHDAccount = + this.wallet.activeAccountType === KeyringAccountType.HDAccount; + + const encryptedMnemonic = isHDAccount + ? this.sessionMainMnemonic + : this.sessionMnemonic; + const mnemonic = CryptoJS.AES.decrypt( - this.sessionMnemonic, + encryptedMnemonic, this.sessionPassword ).toString(CryptoJS.enc.Utf8); - const accounts = this.wallet.accounts[KeyringAccountType.HDAccount]; + const { hd, main } = getSyscoinSigners({ - mnemonic: mnemonic, + mnemonic, isTestnet, rpc, }); + this.hd = hd; this.syscoinSigner = main; - const walletAccountsArray = Object.values(accounts); + const walletAccountsArray = isHDAccount + ? Object.values(accounts) + : Object.values(accounts).filter( + ({ address }) => !ethers.utils.isAddress(address) + ); // Create an array of promises. const accountPromises = walletAccountsArray.map(async ({ id }) => { @@ -1635,13 +1800,6 @@ export class KeyringManager implements IKeyringManager { this.logout(); }; - public logout = () => { - this.sessionPassword = ''; - this.sessionSeed = ''; - this.currentSessionSalt = ''; - this.sessionMnemonic = ''; - }; - private isSyscoinChain = (network: any) => Boolean(this.wallet.networks.syscoin[network.chainId]) && network.url.includes('blockbook'); @@ -1650,13 +1808,35 @@ export class KeyringManager implements IKeyringManager { // ===================================== PRIVATE KEY ACCOUNTS METHODS - SIMPLE KEYRING ===================================== // private async updatePrivWeb3Account(account: IKeyringAccountState) { - const balance = await this.ethereumTransaction.getBalance(account.address); + const isEthAddress = ethers.utils.isAddress(account.address); + let balance: null | number = null; + + if (isEthAddress) { + balance = await this.ethereumTransaction.getBalance(account.address); + } else { + const options = 'tokens=used&details=tokens'; + const response = await sys.utils.fetchBackendAccount( + this.wallet.activeNetwork.url, + account.xpub, + options, + true, + undefined + ); + + if (response !== null) balance = response.balance / 1e8; + } const updatedAccount = { ...account, balances: { - syscoin: 0, - ethereum: balance, + syscoin: + !isEthAddress && balance !== null + ? balance + : account.balances.syscoin, + ethereum: + isEthAddress && balance !== null + ? balance + : account.balances.ethereum, }, } as IKeyringAccountState; @@ -1665,20 +1845,41 @@ export class KeyringManager implements IKeyringManager { private async restoreWallet(hdCreated: boolean, pwd: string) { if (!this.sessionMnemonic) { - let { mnemonic } = await getDecryptedVault(pwd); - mnemonic = CryptoJS.AES.decrypt(mnemonic, pwd).toString( + const isImported = + this.wallet.activeAccountType === KeyringAccountType.Imported; + + const { mnemonic } = await getDecryptedVault(pwd); + const hdWalletSeed = CryptoJS.AES.decrypt(mnemonic, pwd).toString( CryptoJS.enc.Utf8 ); - this.sessionMnemonic = CryptoJS.AES.encrypt( - mnemonic, + + if (isImported && this.activeChain === INetworkType.Syscoin) { + const zprv = this.getDecryptedPrivateKey()?.decryptedPrivateKey; + + this.sessionMnemonic = CryptoJS.AES.encrypt( + zprv, + this.sessionPassword + ).toString(); + } else { + this.sessionMnemonic = CryptoJS.AES.encrypt( + hdWalletSeed, + this.sessionPassword + ).toString(); + } + + const seed = (await mnemonicToSeed(hdWalletSeed)).toString('hex'); + + this.sessionMainMnemonic = CryptoJS.AES.encrypt( + hdWalletSeed, this.sessionPassword ).toString(); - const seed = (await mnemonicToSeed(mnemonic)).toString('hex'); + this.sessionSeed = CryptoJS.AES.encrypt( seed, this.sessionPassword ).toString(); } + if (this.activeChain === INetworkType.Syscoin && !hdCreated) { const { rpc, isTestnet } = await this.getSignerUTXO( this.wallet.activeNetwork @@ -1691,9 +1892,12 @@ export class KeyringManager implements IKeyringManager { private guaranteeUpdatedPrivateValues(pwd: string) { try { + const isHDAccount = + this.wallet.activeAccountType === KeyringAccountType.HDAccount; + //Here we need to decrypt the sessionMnemonic and sessionSeed values with the sessionPassword value before it changes and get updated const decryptedSessionMnemonic = CryptoJS.AES.decrypt( - this.sessionMnemonic, + isHDAccount ? this.sessionMainMnemonic : this.sessionMnemonic, this.sessionPassword ).toString(CryptoJS.enc.Utf8); @@ -1702,6 +1906,11 @@ export class KeyringManager implements IKeyringManager { this.sessionPassword ).toString(CryptoJS.enc.Utf8); + const decryptedSessionMainMnemonic = CryptoJS.AES.decrypt( + this.sessionMainMnemonic, + this.sessionPassword + ).toString(CryptoJS.enc.Utf8); + //Generate a new salt this.currentSessionSalt = this.generateSalt(); @@ -1718,6 +1927,11 @@ export class KeyringManager implements IKeyringManager { decryptedSessionMnemonic, this.sessionPassword ).toString(); + + this.sessionMainMnemonic = CryptoJS.AES.encrypt( + decryptedSessionMainMnemonic, + this.sessionPassword + ).toString(); } catch (error) { console.log('ERROR updateValuesToUpdateWalletKeys', { error, @@ -1735,11 +1949,30 @@ export class KeyringManager implements IKeyringManager { private async updateWalletKeys(pwd: string) { try { const vaultKeys = await this.storage.get('vault-keys'); + let { accounts } = this.wallet; + + const decryptedXprvs = Object.entries(accounts).reduce( + (acc, [key, value]) => { + const accounts = {}; + + Object.entries(value).forEach(([key, value]) => { + accounts[key] = CryptoJS.AES.decrypt( + value.xprv, + this.sessionPassword + ).toString(CryptoJS.enc.Utf8); + }); + + acc[key] = accounts; + return acc; + }, + {} + ); //Update values this.guaranteeUpdatedPrivateValues(pwd); - const { accounts } = this.wallet; + accounts = this.wallet.accounts; + for (const accountTypeKey in accounts) { // Exclude 'Trezor' accounts if (accountTypeKey !== KeyringAccountType.Trezor) { @@ -1751,7 +1984,7 @@ export class KeyringManager implements IKeyringManager { activeAccount.address ); - let decryptedXprv = ''; + let encryptNewXprv = ''; if (!isBitcoinBased) { let { mnemonic } = await getDecryptedVault(pwd); @@ -1759,19 +1992,18 @@ export class KeyringManager implements IKeyringManager { CryptoJS.enc.Utf8 ); const { privateKey } = ethers.Wallet.fromMnemonic(mnemonic); - decryptedXprv = privateKey; + + encryptNewXprv = CryptoJS.AES.encrypt( + privateKey, + this.sessionPassword + ).toString(); } else { - decryptedXprv = this.getEncryptedXprv(); + encryptNewXprv = CryptoJS.AES.encrypt( + decryptedXprvs[accountTypeKey as KeyringAccountType][id], + this.sessionPassword + ).toString(); } - // Update xprv - const encryptNewXprv = isBitcoinBased - ? decryptedXprv - : CryptoJS.AES.encrypt( - decryptedXprv, - this.sessionPassword - ).toString(); - activeAccount.xprv = encryptNewXprv; } } @@ -1797,13 +2029,54 @@ export class KeyringManager implements IKeyringManager { private async _getPrivateKeyAccountInfos(privKey: string, label?: string) { const { accounts } = this.wallet; + let importedAccountValue: { + address: string; + publicKey: string; + privateKey: string; + }; + + const balances = { + syscoin: 0, + ethereum: 0, + }; //Validate if the private key value that we receive already starts with 0x or not - const hexPrivateKey = - privKey.slice(0, 2) === '0x' ? privKey : `0x${privKey}`; + const { isValid: isZprv, node, network } = this.validateZprv(privKey); + + if (isZprv) { + const { balance: _balance, tokens } = await sys.utils.fetchBackendAccount( + this.wallet.activeNetwork.url, + node.neutered().toBase58(), + 'tokens=used&details=tokens', + true, + undefined + ); + + const { receivingIndex } = this.setLatestIndexesFromXPubTokens(tokens); + const nodeChild = node.derivePath(`0/${receivingIndex}`); + const { address } = bjs.payments.p2wpkh({ + pubkey: Buffer.from(nodeChild.publicKey), + network, + }); + + importedAccountValue = { + address, + publicKey: node.neutered().toBase58(), + privateKey: privKey, + }; + + balances.syscoin = _balance / 1e8; + } else { + const hexPrivateKey = + privKey.slice(0, 2) === '0x' ? privKey : `0x${privKey}`; + + importedAccountValue = + this.ethereumTransaction.importAccount(hexPrivateKey); - const importedAccountValue = - this.ethereumTransaction.importAccount(hexPrivateKey); + balances.ethereum = await this.ethereumTransaction.getBalance( + importedAccountValue.address + ); + } const { address, publicKey, privateKey } = importedAccountValue; @@ -1822,21 +2095,18 @@ export class KeyringManager implements IKeyringManager { 'Account already exists, try again with another Private Key.' ); - const ethereumBalance = await this.ethereumTransaction.getBalance(address); const id = Object.values(accounts[KeyringAccountType.Imported]).length < 1 ? 0 : Object.values(accounts[KeyringAccountType.Imported]).length; - const importedAccount = { + return { ...initialActiveImportedAccountState, address, label: label ? label : `Imported ${id + 1}`, - id: id, - balances: { - syscoin: 0, - ethereum: ethereumBalance, - }, + id, + balances, + isImported: true, xprv: CryptoJS.AES.encrypt(privateKey, this.sessionPassword).toString(), xpub: publicKey, assets: { @@ -1844,56 +2114,5 @@ export class KeyringManager implements IKeyringManager { ethereum: [], }, } as IKeyringAccountState; - - return importedAccount; - } - - public async importAccount(privKey: string, label?: string) { - const importedAccount = await this._getPrivateKeyAccountInfos( - privKey, - label - ); - this.wallet.accounts[KeyringAccountType.Imported][importedAccount.id] = - importedAccount; - - return importedAccount; } - - //TODO: validate updateAllPrivateKeyAccounts updating 2 accounts or more works properly - public async updateAllPrivateKeyAccounts() { - try { - const accountPromises = Object.values( - this.wallet.accounts[KeyringAccountType.Imported] - ).map(async (account) => await this.updatePrivWeb3Account(account)); - - const updatedWallets = await Promise.all(accountPromises); - - // const updatedWallets = await Promise.all( - // Object.values(this.wallet.accounts[KeyringAccountType.Imported]).map( - // async (account) => await this.updatePrivWeb3Account(account) - // ) - // ); - - this.wallet.accounts[KeyringAccountType.Imported] = updatedWallets; - } catch (error) { - console.log('ERROR updateAllPrivateKeyAccounts', { - error, - values: { - memPass: this.memPassword, - memMnemonic: this.memMnemonic, - vaultKeys: this.storage.get('vault-keys'), - vault: getDecryptedVault(this.memPassword), - }, - }); - this.validateAndHandleErrorByMessage(error.message); - } - } - - public updateAccountLabel = ( - label: string, - accountId: number, - accountType: KeyringAccountType - ) => { - this.wallet.accounts[accountType][accountId].label = label; - }; } diff --git a/packages/sysweb3-keyring/src/signers.ts b/packages/sysweb3-keyring/src/signers.ts index 5372ee8..0a35768 100644 --- a/packages/sysweb3-keyring/src/signers.ts +++ b/packages/sysweb3-keyring/src/signers.ts @@ -108,8 +108,8 @@ export interface SyscoinHDSigner { setIndexFlag: number; blockbookURL: string; }; - mnemonic: string; - fromMnemonic: { + mnemonicOrZprv: string; + node: { seed: Buffer; isTestnet: boolean; coinType: number; diff --git a/packages/sysweb3-keyring/src/types.ts b/packages/sysweb3-keyring/src/types.ts index a1be967..eac7dbe 100644 --- a/packages/sysweb3-keyring/src/types.ts +++ b/packages/sysweb3-keyring/src/types.ts @@ -229,6 +229,7 @@ export interface IKeyringManager { accountType: KeyringAccountType ) => void; utf8Error: boolean; + validateZprv: (zprv: string) => IValidateZprvResponse; } export enum KeyringAccountType { @@ -277,6 +278,11 @@ type IsBitcoinBased = { type IOriginNetwork = INetwork & IsBitcoinBased; +interface IValidateZprvResponse { + isValid: boolean; + message: string; +} + export interface IKeyringAccountState { address: string; id: number; diff --git a/packages/sysweb3-keyring/yarn.lock b/packages/sysweb3-keyring/yarn.lock index 2fbeca2..dde596c 100644 --- a/packages/sysweb3-keyring/yarn.lock +++ b/packages/sysweb3-keyring/yarn.lock @@ -900,10 +900,10 @@ ethers "^5.6.9" web3 "^1.7.1" -"@pollum-io/sysweb3-utils@^1.1.235": - version "1.1.235" - resolved "https://registry.yarnpkg.com/@pollum-io/sysweb3-utils/-/sysweb3-utils-1.1.235.tgz#9ae08757631b40a4e2e4fe3da13b14457137f9f4" - integrity sha512-El0fvuZg1AQ/a/fb8dwEfLQwDuVPR8Xj12uVWC2hPal5HjAVH9fNDXyHwUWeblH9oUUUyR5bXjbbf5slVW3KgA== +"@pollum-io/sysweb3-utils@^1.1.236": + version "1.1.236" + resolved "https://registry.yarnpkg.com/@pollum-io/sysweb3-utils/-/sysweb3-utils-1.1.236.tgz#512a8d34257e881a32e92ad5d24ed6f8b635a9a5" + integrity sha512-tqZdMdp1ffriQZ5HPwWI71489UZNAdLOACGcXbIbRRvvbX64HKlQFdav/+k9BzuKR7d12zFWmoUvOwHNkSW5OA== dependencies: "@ethersproject/contracts" "^5.6.2" axios "^0.26.1" diff --git a/packages/sysweb3-utils/package.json b/packages/sysweb3-utils/package.json index fab3199..0f9bef9 100644 --- a/packages/sysweb3-utils/package.json +++ b/packages/sysweb3-utils/package.json @@ -1,6 +1,6 @@ { "name": "@pollum-io/sysweb3-utils", - "version": "1.1.235", + "version": "1.1.236", "description": "A helper for multi-chain accounts.", "main": "cjs/index.js", "types": "types/index.d.ts", diff --git a/packages/sysweb3-utils/src/syscoints/utils.ts b/packages/sysweb3-utils/src/syscoints/utils.ts index da74098..e4ee733 100644 --- a/packages/sysweb3-utils/src/syscoints/utils.ts +++ b/packages/sysweb3-utils/src/syscoints/utils.ts @@ -24,11 +24,12 @@ const syscoinNetworks = { messagePrefix: '\x18Syscoin Signed Message:\n', bech32: 'sys', bip32: { - public: 0x0488b21e, - private: 0x0488ade4, + public: 0x04b24746, // zpub + private: 0x04b2430c, // zprv }, pubKeyHash: 0x3f, scriptHash: 0x05, + slip44: 57, wif: 0x80, }, testnet: { @@ -79,8 +80,9 @@ const tokenFreezeFunction = const axiosConfig = { withCredentials: true, }; + /* fetchNotarizationFromEndPoint -Purpose: Fetch notarization signature via axois from an endPoint URL, see spec for more info: https://github.com/syscoin/sips/blob/master/sip-0002.mediawiki +Purpose: Fetch notarization signature via axios from an endPoint URL, see spec for more info: https://github.com/syscoin/sips/blob/master/sip-0002.mediawiki Param endPoint: Required. Fully qualified URL which will take transaction information and respond with a signature or error on denial Param txHex: Required. Raw transaction hex Returns: Returns JSON object in response, signature on success and error on denial of notarization @@ -691,6 +693,7 @@ async function signWithWIF(psbt: any, wif: any, network: any) { return await signPSBTWithWIF(psbt, wif, network); } } + /* buildEthProof Purpose: Build Ethereum SPV proof using eth-proof library Param assetOpts: Required. Object containing web3url and ethtxid fields populated @@ -1065,6 +1068,7 @@ function setTransactionMemo(rawHex: any, memoHeader: any, buffMemo: any) { } return txn; } + function copyPSBT( psbt: any, networkIn: any, @@ -1131,6 +1135,7 @@ class Signer { public accountIndex: number; public setIndexFlag: number; public blockbookURL: any; + constructor( password?: any, isTestnet?: boolean, @@ -1382,14 +1387,17 @@ Param accountIndex: Required. Account number to use return payment.address; }; } + class HDSigner { public Signer: Signer; - public mnemonic: string; - public fromMnemonic: any; + public mnemonicOrZprv: string; + public node: any; public changeIndex: number; + public importMethod: 'fromBase58' | 'fromSeed'; public receivingIndex: number; + constructor( - mnemonic: string, + mnemonicOrZpr: string, password?: any, isTestnet?: boolean, networks?: any, @@ -1400,20 +1408,34 @@ class HDSigner { this.changeIndex = -1; this.receivingIndex = -1; this.Signer = new Signer(password, isTestnet, networks, SLIP44, pubTypes); - this.mnemonic = mnemonic; // serialized + this.mnemonicOrZprv = mnemonicOrZpr; + this.importMethod = 'fromSeed'; + + const isZprv = mnemonicOrZpr.startsWith('zprv'); + + if (isZprv) { + this.importMethod = 'fromBase58'; + + this.node = new BIP84.fromZPrv( + mnemonicOrZpr, + this.Signer.pubTypes, + this.Signer.networks + ); + } else { + /* eslint new-cap: ["error", { "newIsCap": false }] */ + this.node = new BIP84.fromMnemonic( + mnemonicOrZpr, + this.Signer.password, + this.Signer.isTestnet, + this.Signer.SLIP44, + this.Signer.pubTypes, + this.Signer.network + ); + } - /* eslint new-cap: ["error", { "newIsCap": false }] */ - this.fromMnemonic = new BIP84.fromMnemonic( - mnemonic, - this.Signer.password, - this.Signer.isTestnet, - this.Signer.SLIP44, - this.Signer.pubTypes, - this.Signer.network - ); // try to restore, if it does not succeed then initialize from scratch if (!this.Signer.password || !this.restore(this.Signer.password, bipNum)) { - this.createAccount(bipNum); + this.createAccount(bipNum, isZprv ? mnemonicOrZpr : undefined); } } @@ -1426,14 +1448,13 @@ class HDSigner { Param keypath: Required. HD BIP32 path of key desired based on internal seed and network Returns: bitcoinjs-lib keypair */ - deriveKeypair = (keypath: any) => { - const keyPair = bjs.bip32 - .fromSeed(this.fromMnemonic.seed, this.Signer.network) - .derivePath(keypath); - if (!keyPair) { - return null; - } - return keyPair; + deriveKeypair = (keypath: string) => { + const keyPair = bjs.bip32[this.importMethod]( + this.node.seed || this.mnemonicOrZprv, + this.Signer.network + ).derivePath(keypath); + + return !keyPair ? null : keyPair; }; /* derivePubKey @@ -1441,14 +1462,16 @@ class HDSigner { Param keypath: Required. HD BIP32 path of key desired based on internal seed and network Returns: bitcoinjs-lib pubkey */ - derivePubKey = (keypath: any) => { - const keyPair = bjs.bip32 - .fromSeed(this.fromMnemonic.seed, this.Signer.network) - .derivePath(keypath); - if (!keyPair) { - return null; - } - return keyPair.publicKey; + derivePubKey = (path: any) => { + // const path = + // this.importMethod === 'fromBase58' ? keypath.slice(13) : keypath; + + const keyPair = bjs.bip32[this.importMethod]( + this.node.seed || this.mnemonicOrZprv, + this.Signer.network + ).derivePath(path); + + return !keyPair ? null : keyPair.publicKey; }; /* getRootNode @@ -1456,7 +1479,10 @@ class HDSigner { Returns: BIP32 root node representing the seed */ getRootNode = () => { - return bjs.bip32.fromSeed(this.fromMnemonic.seed, this.Signer.network); + return bjs.bip32[this.importMethod]( + this.node.seed || this.mnemonicOrZprv, + this.Signer.network + ); }; /* sign @@ -1473,8 +1499,10 @@ Returns: psbt from bitcoinjs-lib Returns: bip32 root master fingerprint */ getMasterFingerprint = () => { - return bjs.bip32.fromSeed(this.fromMnemonic.seed, this.Signer.network) - .fingerprint; + return bjs.bip32[this.importMethod]( + this.node.seed || this.mnemonicOrZprv, + this.Signer.network + ).fingerprint; }; /* deriveAccount @@ -1494,7 +1522,8 @@ Returns: psbt from bitcoinjs-lib ) { bipNum = 84; } - return this.fromMnemonic.deriveAccount(index, bipNum); + + return this.node.deriveAccount(index, bipNum); }; /* signPSBT @@ -1505,7 +1534,8 @@ Returns: psbt from bitcoinjs-lib */ signPSBT = async (psbt: any, pathIn: any) => { const txInputs = psbt.txInputs; - const fp = this.getMasterFingerprint(); + const rootNode = this.getRootNode(); + for (let i = 0; i < txInputs.length; i++) { const dataInput = psbt.data.inputs[i]; if ( @@ -1516,24 +1546,30 @@ Returns: psbt from bitcoinjs-lib (!dataInput.bip32Derivation || dataInput.bip32Derivation.length === 0)) ) { - const path = pathIn || dataInput.unknownKeyVals[1].value.toString(); + const keyPath = pathIn || dataInput.unknownKeyVals[1].value.toString(); + const path = + this.importMethod === 'fromBase58' ? keyPath.slice(13) : keyPath; + const pubkey = this.derivePubKey(path); const address = this.getAddressFromPubKey(pubkey); + if ( pubkey && (pathIn || dataInput.unknownKeyVals[0].value.toString() === address) ) { dataInput.bip32Derivation = [ { - masterFingerprint: fp, - path: path, - pubkey: pubkey, + masterFingerprint: rootNode.fingerprint, + path, + pubkey, }, ]; } } } - await psbt.signAllInputsHDAsync(this.getRootNode()); + + await psbt.signAllInputsHDAsync(rootNode); + try { if (psbt.validateSignaturesOfAllInputs()) { psbt.finalizeAllInputs(); @@ -1573,7 +1609,7 @@ Returns: psbt from bitcoinjs-lib return false; } const decryptedData = JSON.parse(bytes.toString(CryptoJS.enc.Utf8)); - this.mnemonic = decryptedData.mnemonic; + this.mnemonicOrZprv = decryptedData.mnemonic; const numAccounts = decryptedData.numAccounts; // sanity checks if (this.Signer.accountIndex > 1000) { @@ -1615,7 +1651,7 @@ Returns: psbt from bitcoinjs-lib } const key = this.Signer.network.bech32 + '_hdsigner'; const obj = { - mnemonic: this.mnemonic, + mnemonic: this.mnemonicOrZprv, numAccounts: this.Signer.accounts.length, }; const ciphertext = CryptoJS.AES.encrypt( @@ -1640,10 +1676,16 @@ Returns: psbt from bitcoinjs-lib Returns: Account index of new account */ - createAccount = (bipNum: any) => { + createAccount = (bipNum: any, zprv?: string) => { this.Signer.changeIndex = -1; this.Signer.receivingIndex = -1; - const child = this.deriveAccount(this.Signer.accounts.length, bipNum); + const zPrivate = + zprv || this.mnemonicOrZprv.startsWith('zprv') + ? this.mnemonicOrZprv + : null; + + const child = + zPrivate || this.deriveAccount(this.Signer.accounts.length, bipNum); this.Signer.accountIndex = this.Signer.accounts.length; /* eslint new-cap: ["error", { "newIsCap": false }] */ this.Signer.accounts.push( @@ -1705,24 +1747,29 @@ Returns: psbt from bitcoinjs-lib /* Override PSBT stuff so fee check isn't done as Syscoin Allocation burns outputs > inputs */ function scriptWitnessToWitnessStack(buffer: any) { let offset = 0; + function readSlice(n: any) { offset += n; return buffer.slice(offset - n, offset); } + function readVarInt() { const vi = varuint.decode(buffer, offset); offset += varuint.decode.bytes; return vi; } + function readVarSlice() { return readSlice(readVarInt()); } + function readVector() { const count = readVarInt(); const vector = []; for (let i = 0; i < count; i++) vector.push(readVarSlice()); return vector; } + return readVector(); } @@ -1840,6 +1887,15 @@ function getTxCacheValue(key: any, name: any, inputs: any, c: any) { } class SPSBT extends bjs.Psbt { + static fromBase64(data: any, opts = {}) { + const buffer = Buffer.from(data, 'base64'); + const psbt = this.fromBuffer(buffer, opts); + psbt.getFeeRate = SPSBT.prototype.getFeeRate; + psbt.getFee = SPSBT.prototype.getFee; + psbt.extractTransaction = SPSBT.prototype.extractTransaction; + return psbt; + } + getFeeRate() { return getTxCacheValue( '__FEE_RATE', @@ -1868,15 +1924,6 @@ class SPSBT extends bjs.Psbt { inputFinalizeGetAmts(this.data.inputs, tx, c, true); return tx; } - - static fromBase64(data: any, opts = {}) { - const buffer = Buffer.from(data, 'base64'); - const psbt = this.fromBuffer(buffer, opts); - psbt.getFeeRate = SPSBT.prototype.getFeeRate; - psbt.getFee = SPSBT.prototype.getFee; - psbt.extractTransaction = SPSBT.prototype.extractTransaction; - return psbt; - } } function exportPsbtToJson(psbt: any, assetsMap: any) { @@ -1909,6 +1956,7 @@ function getAssetIDs(assetGuid: any) { const BN_NFT = new BN(assetGuid).shrn(32); return { baseAssetID: getBaseAssetID(assetGuid), NFTID: BN_NFT.toString(10) }; } + const Psbt = SPSBT; export {