From 829fbd9745009110c8e4aad67b9483a3afcf7fdb Mon Sep 17 00:00:00 2001 From: Ricardo Vieira Date: Mon, 30 Dec 2024 15:20:49 -0300 Subject: [PATCH 1/2] feat: add support for importing utxo accounts using a zprv private key --- source/components/Header/AccountHeader.tsx | 106 +++++++++----- .../components/Header/Menus/NetworkMenu.tsx | 138 +++++++++--------- .../Connections/ChangeConnectedAccount.tsx | 8 +- source/pages/Send/Confirm.tsx | 1 - source/pages/Settings/ImportAccount.tsx | 19 ++- .../Background/controllers/MainController.ts | 57 ++++++-- source/utils/validatePrivateKey.ts | 38 ++--- 7 files changed, 222 insertions(+), 145 deletions(-) diff --git a/source/components/Header/AccountHeader.tsx b/source/components/Header/AccountHeader.tsx index e9598e808..99dd1f5c5 100644 --- a/source/components/Header/AccountHeader.tsx +++ b/source/components/Header/AccountHeader.tsx @@ -58,8 +58,8 @@ const RenderAccountsListByBitcoinBased = ( className={`py-1.5 px-5 w-max backface-visibility-hidden flex items-center text-white text-sm font-medium active:bg-opacity-40 focus:outline-none cursor-pointer transform transition duration-300`} - onClick={() => { - setActiveAccount( + onClick={async () => { + await setActiveAccount( account.id, KeyringAccountType.HDAccount ); @@ -72,7 +72,7 @@ const RenderAccountsListByBitcoinBased = ( style={{ maxWidth: '16.25rem', textOverflow: 'ellipsis' }} className="w-max gap-[2px] flex items-center justify-start whitespace-nowrap overflow-hidden" > - + {account.label} ({ellipsis(account.address, 4, 4)}) @@ -89,6 +89,44 @@ const RenderAccountsListByBitcoinBased = ( ))} + {Object.values(accounts.Imported) + .filter((acc) => !acc.address.startsWith('0x')) + .map((account, index) => ( +
  • { + await setActiveAccount( + account.id, + KeyringAccountType.Imported + ); + close(); + }} + id={`account-${index}`} + key={account.id} + > + + + {account.label} ({ellipsis(account.address, 4, 4)}) + + + Imported + + {activeAccount.id === account.id && + activeAccount.type === KeyringAccountType.Imported && ( + + )} +
  • + ))} + {Object.values(accounts.Trezor) .filter((acc) => acc.isImported === false) //todo we don't have account.isImported anymore .map((account, index) => ( @@ -100,11 +138,14 @@ const RenderAccountsListByBitcoinBased = ( : 'cursor-pointer' } transform transition duration-300`} - onClick={() => { + onClick={async () => { if (account?.originNetwork.url !== activeNetwork.url) { return; } - setActiveAccount(account.id, KeyringAccountType.Trezor); + await setActiveAccount( + account.id, + KeyringAccountType.Trezor + ); close(); }} id={`account-${index}`} @@ -119,12 +160,13 @@ const RenderAccountsListByBitcoinBased = ( > + /> {account.label}{' '} {!(account?.originNetwork.url !== activeNetwork.url) && `(${ellipsis(account.address, 4, 4)})`} @@ -156,11 +198,14 @@ const RenderAccountsListByBitcoinBased = ( : 'cursor-pointer' } transform transition duration-300`} - onClick={() => { + onClick={async () => { if (account?.originNetwork.url !== activeNetwork.url) { return; } - setActiveAccount(account.id, KeyringAccountType.Ledger); + await setActiveAccount( + account.id, + KeyringAccountType.Ledger + ); close(); }} id={`account-${index}`} @@ -175,12 +220,13 @@ const RenderAccountsListByBitcoinBased = ( > + /> {account.label}{' '} {!(account?.originNetwork.url !== activeNetwork.url) && `(${ellipsis(account.address, 4, 4)})`} @@ -206,7 +252,7 @@ const RenderAccountsListByBitcoinBased = ( ([keyringAccountType, accountTypeAccounts]) => (
    {Object.values(accountTypeAccounts) - .filter((account) => account.xpub !== '') + .filter((account) => account.address.startsWith('0x')) .map((account, index) => (
  • { + onClick={async () => { if ( (account.isTrezorWallet && account?.originNetwork?.isBitcoinBased) || @@ -228,7 +274,7 @@ const RenderAccountsListByBitcoinBased = ( ) { return; } - setActiveAccount( + await setActiveAccount( account.id, keyringAccountType as KeyringAccountType ); @@ -245,27 +291,29 @@ const RenderAccountsListByBitcoinBased = ( className="w-full flex items-center justify-start whitespace-nowrap overflow-hidden" > {account.isImported ? ( - + ) : account.isTrezorWallet ? ( + /> ) : account.isLedgerWallet ? ( + /> ) : ( - + )}{' '} {account.label}{' '} {!( @@ -328,8 +376,6 @@ export const AccountMenu: React.FC = () => { await controllerEmitter(['wallet', 'setAccount'], [Number(id), type]); }; - const cursorType = isBitcoinBased ? 'cursor-not-allowed' : 'cursor-pointer'; - return (
    @@ -394,27 +440,13 @@ export const AccountMenu: React.FC = () => {
  • { - isBitcoinBased ? null : navigate('/settings/account/import'); - }} - className={`py-1.5 ${cursorType} px-6 w-full backface-visibility-hidden flex items-center justify-start gap-3 text-white text-sm font-medium active:bg-opacity-40 focus:outline-none`} + onClick={() => navigate('/settings/account/import')} + className={`py-1.5 cursor-pointer px-6 w-full backface-visibility-hidden flex items-center justify-start gap-3 text-white text-sm font-medium active:bg-opacity-40 focus:outline-none`} > - - - - {t('accountMenu.importAccount')} - + + + {t('accountMenu.importAccount')}
  • - {isBitcoinBased && ( - - {t('accountMenu.importAccMessage')} - - )}
    diff --git a/source/components/Header/Menus/NetworkMenu.tsx b/source/components/Header/Menus/NetworkMenu.tsx index ef009acd0..4467cec5d 100644 --- a/source/components/Header/Menus/NetworkMenu.tsx +++ b/source/components/Header/Menus/NetworkMenu.tsx @@ -193,7 +193,7 @@ export const NetworkMenu: React.FC = (
    - {!activeAccount.isImported ? ( + {isBitcoinBased || !activeAccount.isImported ? ( <> @@ -272,71 +272,77 @@ export const NetworkMenu: React.FC = ( ) : null} - - - {({ open }) => ( - <> - - - - - {t('networkMenu.evmNetworks')} - - - - - - - {Object.values(networks.ethereum) - .sort(customSort) - - .map((currentNetwork: any, index: number, arr) => ( -
  • - handleChangeNetwork( - currentNetwork, - 'ethereum' - ) - } - > - - {currentNetwork.label} - -
    - {!isBitcoinBased && - activeNetworkValidator(currentNetwork) && ( - - )} -
    -
  • - ))} -
    - - )} -
    -
    + {activeAccount.isImported && isBitcoinBased ? null : ( + + + {({ open }) => ( + <> + + + + + {t('networkMenu.evmNetworks')} + + + + + + + {Object.values(networks.ethereum) + .sort(customSort) + + .map( + (currentNetwork: any, index: number, arr) => ( +
  • + handleChangeNetwork( + currentNetwork, + 'ethereum' + ) + } + > + + {currentNetwork.label} + +
    + {!isBitcoinBased && + activeNetworkValidator( + currentNetwork + ) && ( + + )} +
    +
  • + ) + )} +
    + + )} +
    +
    + )} {t('networkMenu.networkSettings')} diff --git a/source/pages/Connections/ChangeConnectedAccount.tsx b/source/pages/Connections/ChangeConnectedAccount.tsx index 298c1640d..8550d79f5 100644 --- a/source/pages/Connections/ChangeConnectedAccount.tsx +++ b/source/pages/Connections/ChangeConnectedAccount.tsx @@ -19,8 +19,8 @@ export const ChangeConnectedAccount = () => { //TODO: validate this const { host, eventName, connectedAccount, accountType } = useQueryData(); - const handleConnectedAccount = () => { - controllerEmitter( + const handleConnectedAccount = async () => { + await controllerEmitter( ['wallet', 'setAccount'], [connectedAccount.id, accountType] ); @@ -30,8 +30,8 @@ export const ChangeConnectedAccount = () => { window.close(); }; - const handleActiveAccount = () => { - controllerEmitter( + const handleActiveAccount = async () => { + await controllerEmitter( ['dapp', 'changeAccount'], [host, activeAccount.id, activeAccount.type] ); diff --git a/source/pages/Send/Confirm.tsx b/source/pages/Send/Confirm.tsx index f0bbfcbc4..ee3ee7b5b 100755 --- a/source/pages/Send/Confirm.tsx +++ b/source/pages/Send/Confirm.tsx @@ -209,7 +209,6 @@ export const SendConfirm = () => { 'chainId', 'maxFeePerGas', 'maxPriorityFeePerGas', - , ]) as ITxState; const value = ethers.utils.parseUnits( diff --git a/source/pages/Settings/ImportAccount.tsx b/source/pages/Settings/ImportAccount.tsx index 2fcc2c441..b64439ac1 100644 --- a/source/pages/Settings/ImportAccount.tsx +++ b/source/pages/Settings/ImportAccount.tsx @@ -15,14 +15,17 @@ const ImportAccountView = () => { const { controllerEmitter } = useController(); const { navigate, alert } = useUtils(); const [form] = useForm(); + const [validPrivateKey, setValidPrivateKey] = useState(false); //* States const [isAccountImported, setIsAccountImported] = useState(false); const [isImporting, setIsImporting] = useState(false); - const { accounts, activeAccount: activeAccountMeta } = useSelector( - (state: RootState) => state.vault - ); + const { + accounts, + activeAccount: activeAccountMeta, + isBitcoinBased, + } = useSelector((state: RootState) => state.vault); const activeAccount = accounts[activeAccountMeta.type][activeAccountMeta.id]; @@ -96,15 +99,14 @@ const ImportAccountView = () => { className="md:w-full" hasFeedback rules={[ - { - required: true, - message: '', - }, + { required: true, message: '' }, () => ({ async validator(_, value) { - if (validatePrivateKeyValue(value)) { + if (validatePrivateKeyValue(value, isBitcoinBased)) { + setValidPrivateKey(true); return Promise.resolve(); } + setValidPrivateKey(false); return Promise.reject(); }, }), @@ -124,6 +126,7 @@ const ImportAccountView = () => { loading={isImporting} onClick={handleImportAccount} fullWidth={true} + disabled={!validPrivateKey} > {t('buttons.import')} diff --git a/source/scripts/Background/controllers/MainController.ts b/source/scripts/Background/controllers/MainController.ts index acb77de0a..e53966a34 100644 --- a/source/scripts/Background/controllers/MainController.ts +++ b/source/scripts/Background/controllers/MainController.ts @@ -89,6 +89,12 @@ import { import { validateAndManageUserTransactions } from './transactions/utils'; class MainController extends KeyringManager { + public account: { + eth: IEthAccountController; + sys: ISysAccountController; + }; + public assets: IAssetsManager; + public transactions: ITransactionsManager; private utilsController: IControllerUtils; private assetsManager: IAssetsManager; private nftsController: INftController; @@ -96,13 +102,6 @@ class MainController extends KeyringManager { private transactionsManager: ITransactionsManager; private balancesManager: IBalancesManager; private cancellablePromises: CancellablePromises; - public account: { - eth: IEthAccountController; - sys: ISysAccountController; - }; - public assets: IAssetsManager; - public transactions: ITransactionsManager; - private currentPromise: { cancel: () => void; promise: Promise<{ chainId: string; networkVersion: number }>; @@ -169,7 +168,23 @@ class MainController extends KeyringManager { }) .catch((error) => console.error('Unlock', error)); + const accounts = JSON.parse( + JSON.stringify(store.getState().vault.accounts) + ); + + // update xprv every time the wallet is unlocked + for (const type in accounts) { + for (const id in accounts[type]) { + accounts[type][id] = { + ...accounts[type][id], + xprv: this.wallet.accounts[type][id].xprv, + }; + } + } + + store.dispatch(setAccounts(accounts)); store.dispatch(setLastLogin()); + return canLogin; } @@ -340,8 +355,9 @@ class MainController extends KeyringManager { } } - this.setActiveAccount(id, type); - store.dispatch(setActiveAccount({ id, type })); + this.setActiveAccount(id, type).then(() => { + store.dispatch(setActiveAccount({ id, type })); + }); } public async setActiveNetwork( @@ -662,8 +678,10 @@ class MainController extends KeyringManager { } public async importAccountFromPrivateKey(privKey: string, label?: string) { - const { accounts } = store.getState().vault; + const { accounts, isBitcoinBased, activeAccount, activeNetwork } = + store.getState().vault; const importedAccount = await this.importAccount(privKey, label); + const paliImp: IPaliAccount = { ...importedAccount, assets: { @@ -675,6 +693,7 @@ class MainController extends KeyringManager { ethereum: {}, }, } as IPaliAccount; + store.dispatch( setAccounts({ ...accounts, @@ -684,10 +703,25 @@ class MainController extends KeyringManager { }, }) ); + + await this.setActiveAccount(paliImp.id, KeyringAccountType.Imported); + store.dispatch( setActiveAccount({ id: paliImp.id, type: KeyringAccountType.Imported }) ); + this.updateUserTransactionsState({ + isPolling: false, + isBitcoinBased, + activeAccount, + activeNetwork, + }); + this.updateAssetsFromCurrentAccount({ + activeAccount, + activeNetwork, + isBitcoinBased, + }); + return importedAccount; } @@ -718,6 +752,7 @@ class MainController extends KeyringManager { ethereum: {}, }, } as IPaliAccount; + store.dispatch( setAccounts({ ...accounts, @@ -727,7 +762,7 @@ class MainController extends KeyringManager { }, }) ); - this.setActiveAccount(paliImp.id, KeyringAccountType.Trezor); + await this.setActiveAccount(paliImp.id, KeyringAccountType.Trezor); store.dispatch( setActiveAccount({ id: paliImp.id, type: KeyringAccountType.Trezor }) ); diff --git a/source/utils/validatePrivateKey.ts b/source/utils/validatePrivateKey.ts index d626f77b3..c97d993b8 100644 --- a/source/utils/validatePrivateKey.ts +++ b/source/utils/validatePrivateKey.ts @@ -1,25 +1,27 @@ import { ethers } from 'ethers'; -export const validatePrivateKeyValue = (privKey: string) => { - //Get 2 first characters to validate if starts with 0x or not - const initialValue = privKey.slice(0, 2); +import { getController } from 'scripts/Background'; - switch (initialValue) { - case '0x': - try { - new ethers.Wallet(`${privKey}`); - } catch (error) { - return false; - } - return true; - - default: - try { - new ethers.Wallet(`0x${privKey}`); - } catch (error) { - return false; - } +export const validatePrivateKeyValue = ( + privKey: string, + isBitcoinBased: boolean +) => { + const mainController = getController(); + const initialValue = privKey.match(/^(0x|zprv)/)?.[0]; + if ([undefined, '0x'].includes(initialValue) && !isBitcoinBased) { + try { + new ethers.Wallet(initialValue === undefined ? `0x${privKey}` : privKey); return true; + } catch (error) { + return false; + } } + + if (initialValue === 'zprv' && isBitcoinBased) { + const { isValid } = mainController.wallet.validateZprv(privKey); + return isValid; + } + + return false; }; From 385bbc7735532df0f1a664ce716344aeb554a130 Mon Sep 17 00:00:00 2001 From: Lucas Santos Date: Thu, 16 Jan 2025 11:26:47 -0300 Subject: [PATCH 2/2] chore: update sysweb3 version --- package.json | 4 ++-- yarn.lock | 49 +++++++++++++++++++++++++++++++++++++------------ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index f2543dc21..7bdaaca89 100644 --- a/package.json +++ b/package.json @@ -52,9 +52,9 @@ "@headlessui/react": "^1.6.0", "@heroicons/react": "^1.0.5", "@pollum-io/sysweb3-core": "^1.0.27", - "@pollum-io/sysweb3-keyring": "^1.0.487", + "@pollum-io/sysweb3-keyring": "^1.0.488", "@pollum-io/sysweb3-network": "^1.0.95", - "@pollum-io/sysweb3-utils": "^1.1.235", + "@pollum-io/sysweb3-utils": "^1.1.236", "@reduxjs/toolkit": "^1.4.0", "@tippyjs/react": "^4.2.6", "@types/chrome": "^0.0.268", diff --git a/yarn.lock b/yarn.lock index aca32d21f..bbc055cdd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2379,10 +2379,10 @@ resolved "https://registry.yarnpkg.com/@pollum-io/sysweb3-core/-/sysweb3-core-1.0.27.tgz#2ecbf38c3b5f6821ec2bcc95472eed0a2f11cd10" integrity sha512-HHW2ozdXCac2Xo0oUQgRk/sczBIGQCBvsqYBIwondipA3Rx4/jGC6Y9U/M4wpTi5wQ+ZD18DyEbKAVMXElGcbQ== -"@pollum-io/sysweb3-keyring@^1.0.487": - version "1.0.487" - resolved "https://registry.yarnpkg.com/@pollum-io/sysweb3-keyring/-/sysweb3-keyring-1.0.487.tgz#832ec8809ede161db883930ee9c54eb9aace392f" - integrity sha512-oBrRnJPWYwv0IcRRLheYk1+rvNIl8lCKkoW2Z+Nw7UadFOPwXsSsrzbByjc7rHKkVKM39a+iD63mZt1f/pSUQg== +"@pollum-io/sysweb3-keyring@^1.0.488": + version "1.0.488" + resolved "https://registry.yarnpkg.com/@pollum-io/sysweb3-keyring/-/sysweb3-keyring-1.0.488.tgz#db90e2dff05128d7d01350ed64ad6bb747e656a9" + integrity sha512-7nc7fkuzbhDqn17LQiNkCZ3GCFDQ5asAhPOr5EiBLBCW6HlmPmln8Hy3EaxyvMKYoognSeZ3t9QdNLy4Uq6puw== dependencies: "@bitcoinerlab/descriptors" "^2.0.1" "@bitcoinerlab/secp256k1" "^1.0.5" @@ -2391,7 +2391,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" @@ -2429,10 +2429,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" @@ -13422,7 +13422,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -13500,7 +13509,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -13514,6 +13523,13 @@ strip-ansi@^3.0.0: dependencies: ansi-regex "^2.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -15030,7 +15046,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -15048,6 +15064,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"