From a8a82b283e7d9165f516b82be7c5e9c80cada9d0 Mon Sep 17 00:00:00 2001 From: imti Date: Thu, 7 Nov 2024 14:27:57 -0800 Subject: [PATCH 01/15] wip: getting operations + sending down tx for signing + sending --- src/core/keychain/KeychainManager.ts | 10 +- src/core/providers/proxy.ts | 59 +++-- src/core/utils/orb.ts | 232 ++++++++++++++++++ src/entries/popup/App.tsx | 6 +- src/entries/popup/handlers/wallet.ts | 2 + .../useApproveAppRequestValidations.ts | 3 +- .../SendTransactionActions.tsx | 10 +- .../pages/messages/SendTransaction/index.tsx | 163 +++++++++--- .../pages/messages/SignMessage/index.tsx | 92 ++++++- static/allowlist.json | 5 +- 10 files changed, 515 insertions(+), 67 deletions(-) create mode 100644 src/core/utils/orb.ts diff --git a/src/core/keychain/KeychainManager.ts b/src/core/keychain/KeychainManager.ts index a10f72f4c4..ce364f7d8f 100644 --- a/src/core/keychain/KeychainManager.ts +++ b/src/core/keychain/KeychainManager.ts @@ -536,7 +536,15 @@ class KeychainManager { for (let i = 0; i < this.state.keychains.length; i++) { const keychain = this.state.keychains[i]; const accounts = await keychain.getAccounts(); - if (accounts.includes(address)) { + console.log('address', address); + console.log('accounts', accounts); + console.log( + 'if check', + accounts.map((a) => a.toLowerCase()).includes(address.toLowerCase()), + ); + if ( + accounts.map((a) => a.toLowerCase()).includes(address.toLowerCase()) + ) { return keychain; } } diff --git a/src/core/providers/proxy.ts b/src/core/providers/proxy.ts index 71c01b2b88..8fb57aaf14 100644 --- a/src/core/providers/proxy.ts +++ b/src/core/providers/proxy.ts @@ -16,18 +16,49 @@ const isRainbowEndpoint = (endpoint: string) => getHost(endpoint).includes('rainbow.me'); export const proxyRpcEndpoint = (endpoint: string, chainId: ChainId) => { - if ( - endpoint && - endpoint !== 'http://127.0.0.1:8545' && - endpoint !== 'http://localhost:8545' && - !endpoint.includes('http://10.') && - !endpoint.includes('http://192.168') && - !endpoint.match(/http:\/\/172.(1[6-9]|2[0-9]|3[0-1])./) && - !isRainbowEndpoint(endpoint) - ) { - return `${process.env.RPC_PROXY_BASE_URL}/${chainId}/${ - process.env.RPC_PROXY_API_KEY - }?custom_rpc=${encodeURIComponent(endpoint)}`; - } - return endpoint; + const idToChainstackName = { + [ChainId.base]: 'base-mainnet', + [ChainId.baseSepolia]: 'base-sepolia', + + [ChainId.bsc]: 'bsc-mainnet', + [ChainId.bscTestnet]: 'bsc-testnet', + + [ChainId.arbitrum]: 'arbitrum-mainnet', + [ChainId.arbitrumSepolia]: 'arbitrum-sepolia', + + [ChainId.optimism]: 'optimism-mainnet', + [ChainId.optimismSepolia]: 'optimism-sepolia', + + [ChainId.polygon]: 'polygon-mainnet', + [ChainId.polygonAmoy]: 'polygon-amoy', + + [ChainId.avalanche]: 'avalanche-mainnet', + [ChainId.avalancheFuji]: 'avalanche-fuji', + + [ChainId.mainnet]: 'ethereum-mainnet', + [ChainId.sepolia]: 'ethereum-sepolia', + [ChainId.holesky]: 'ethereum-holesky', + }; + + console.log('endpoint', endpoint); + console.log('chainId', chainId); + + const CHAINSTACK_API_KEY = ''; + + return `https://${idToChainstackName[chainId]}.core.chainstack.com/${CHAINSTACK_API_KEY}`; + + // if ( + // endpoint && + // endpoint !== 'http://127.0.0.1:8545' && + // endpoint !== 'http://localhost:8545' && + // !endpoint.includes('http://10.') && + // !endpoint.includes('http://192.168') && + // !endpoint.match(/http:\/\/172.(1[6-9]|2[0-9]|3[0-1])./) && + // !isRainbowEndpoint(endpoint) + // ) { + // return `${process.env.RPC_PROXY_BASE_URL}/${chainId}/${ + // process.env.RPC_PROXY_API_KEY + // }?custom_rpc=${encodeURIComponent(endpoint)}`; + // } + // return endpoint; }; diff --git a/src/core/utils/orb.ts b/src/core/utils/orb.ts new file mode 100644 index 0000000000..812ab61f57 --- /dev/null +++ b/src/core/utils/orb.ts @@ -0,0 +1,232 @@ +import { providers } from 'ethers'; +import { useEffect, useState } from 'react'; +import { Address } from 'viem'; + +import { keychainManager } from '~/core/keychain/KeychainManager'; + +const PUBLIC_ORB_RPC_BASE = 'https://api-rpc-dev.orblabs.xyz'; +const PUBLIC_ORB_API_KEY = '4ff141e9-98c5-43ee-8b0e-d552f831b68e'; +const PRIVATE_ORB_API_KEY = 'f1c1d996-8df4-4d23-b926-ca702173021d'; + +export const useCreateClusterId = (currentAddress) => { + const [clusterId, setClusterId] = useState(null); + + useEffect(() => { + const createClusterId = async (address) => { + const accounts = [ + { + address, + vmType: 'EVM', + accountType: 'EOA', + }, + ]; + const response = await fetch( + `${PUBLIC_ORB_RPC_BASE}/${PRIVATE_ORB_API_KEY}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + id: 1, + jsonrpc: '2.0', + method: 'orby_createAccountCluster', + params: [{ accounts }], + }), + }, + ); + const { result } = await response.json(); + console.log('cluster data', result); + setClusterId(result.accountClusterId); + }; + createClusterId(currentAddress); + }, [currentAddress]); + + return clusterId; +}; + +export const useVirtualNodeRpcUrl = ( + clusterId, + currentAddress, + testnetMode, +) => { + const [virtualNodeRpcUrl, setVirtualNodeRpcUrl] = useState( + null, + ); + + useEffect(() => { + const fetchVirtualNodeRpcUrl = async () => { + const response = await fetch( + `${PUBLIC_ORB_RPC_BASE}/${PRIVATE_ORB_API_KEY}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + id: 2, + jsonrpc: '2.0', + method: 'orby_getVirtualNodeRpcUrl', + params: [ + { + accountClusterId: clusterId, + entrypointAccountAddress: currentAddress, + chainId: testnetMode ? `EIP155-11155420` : `EIP155-8453`, + }, + ], + }), + }, + ); + const { result } = await response.json(); + console.log('virtual node rpc url', result); + setVirtualNodeRpcUrl(result.virtualNodeRpcUrl); + }; + if (clusterId && currentAddress) { + fetchVirtualNodeRpcUrl(); + } + }, [clusterId, currentAddress]); + + return virtualNodeRpcUrl; +}; + +export const getOperationsToExecuteTransaction = async ({ + virtualNodeRpcUrl, + request, +}: { + virtualNodeRpcUrl: string; + request: { + to: string; + data: string; + value: string; + }; +}) => { + const response = await fetch(virtualNodeRpcUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'orby_getOperationsToExecuteTransaction', + params: [{ ...request }], + }), + }); + + const { result } = await response.json(); + console.log('getOperationsToExecuteTransaction result', result); + return result; +}; + +export const getOperationsToSignTypedData = async ({ + clusterId, + virtualNodeRpcUrl, + to, + data, +}) => { + const response = await fetch(virtualNodeRpcUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + id: 2, + jsonrpc: '2.0', + method: 'orby_getOperationsToSignTypedData', + params: [{ to, data, accountClusterId: clusterId }], + }), + }); + + const { result } = await response.json(); + console.log('getOperationsToSignTypedData result', result); + return result; +}; + +export const sendSignedOperations = async ({ + clusterId, + signedOperations, + virtualNodeRpcUrl, +}) => { + const response = await fetch(virtualNodeRpcUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + id: 2, + jsonrpc: '2.0', + method: 'orby_sendSignedOperations', + params: [{ accountClusterId: clusterId, signedOperations }], + }), + }); + const { result } = await response.json(); + console.log('sendSignedOperations result', result); + return result; +}; + +// Function that signs an operation set. +export async function signOperationSet(operations) { + const signedOperations = []; + + // Loop through and sign all the operations + for (let i = 0; i < operations.length; i++) { + console.log('operations[i]', operations[i]); + // Set the provider and wallet instances for each operation + const provider = new providers.JsonRpcProvider(operations[i].txRpcUrl); + const signer = await keychainManager.getSigner( + operations[i].from as Address, + ); + const wallet = signer.connect(provider); + + console.log('provider', provider); + console.log('signer', signer); + console.log('wallet', wallet); + + let signedOperation; + + // Sign transactions or typed data + if (operations[i].format == 'TRANSACTION') { + const txData = { + from: operations[i].from, + to: operations[i].to, + value: operations[i].value, + data: operations[i].data, + nonce: operations[i].nonce, + gasLimit: operations[i].gasLimit, + // TODO: make note of this, add this to Monday, remind Felix of this + // gasPrice: operations[i].gasPrice, + maxFeePerGas: operations[i].maxFeePerGas, + maxPriorityFeePerGas: operations[i].maxPriorityFeePerGas, + }; + + console.log('txData', txData); + + const tx = await wallet.populateTransaction(txData); + console.log('tx', tx); + const signedTx = await wallet.signTransaction(tx); + console.log('signedTx', signedTx); + signedOperation = { type: operations[i].type, signature: signedTx }; + } else if (operations[i].format == 'TYPED_DATA') { + const parsedData = JSON.parse(operations[i].data); + + const signature = await wallet.signTypedData( + parsedData.domain, + parsedData.types, + parsedData.message, + ); + + console.log('signature', signature); + + signedOperation = { + type: operations[i].type, + signature, + data: operations[i].data, + }; + } + // append transaction to the signed operations array + signedOperations.push(signedOperation); + } + console.log('signedOperations: ', signedOperations); + // Return the signed operations array + return signedOperations; +} diff --git a/src/entries/popup/App.tsx b/src/entries/popup/App.tsx index e11a5cf215..a365733b15 100644 --- a/src/entries/popup/App.tsx +++ b/src/entries/popup/App.tsx @@ -74,9 +74,9 @@ export function App() { lazyLoad: true, }); - if (process.env.IS_DEV !== 'true') { - document.addEventListener('contextmenu', (e) => e.preventDefault()); - } + // if (process.env.IS_DEV !== 'true') { + // document.addEventListener('contextmenu', (e) => e.preventDefault()); + // } // prevent trackpad double tap zoom const app = document.getElementById('app'); diff --git a/src/entries/popup/handlers/wallet.ts b/src/entries/popup/handlers/wallet.ts index b02ed58710..3e8fd8c1d2 100644 --- a/src/entries/popup/handlers/wallet.ts +++ b/src/entries/popup/handlers/wallet.ts @@ -106,6 +106,8 @@ export const sendTransaction = async ( provider, }); + console.log('selectedGas', selectedGas); + const nonce = transactionRequest.nonce ?? (await getNextNonce({ diff --git a/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts b/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts index db740645a8..d19bd41778 100644 --- a/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts +++ b/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts @@ -19,7 +19,8 @@ export const useApproveAppRequestValidations = ({ const { connectedToHardhat, connectedToHardhatOp } = useConnectedToHardhatStore(); - const enoughNativeAssetForGas = useHasEnoughGas(session); + // const enoughNativeAssetForGas = useHasEnoughGas(session); + const enoughNativeAssetForGas = true; const buttonLabel = useMemo(() => { const activeChainId = chainIdToUse( diff --git a/src/entries/popup/pages/messages/SendTransaction/SendTransactionActions.tsx b/src/entries/popup/pages/messages/SendTransaction/SendTransactionActions.tsx index a1442ac2de..07bb810e37 100644 --- a/src/entries/popup/pages/messages/SendTransaction/SendTransactionActions.tsx +++ b/src/entries/popup/pages/messages/SendTransaction/SendTransactionActions.tsx @@ -24,8 +24,12 @@ export const SendTransactionActions = ({ loading: boolean; dappStatus?: DAppStatus; }) => { - const { enoughNativeAssetForGas, buttonLabel } = - useApproveAppRequestValidations({ session, dappStatus }); + const { buttonLabel } = useApproveAppRequestValidations({ + session, + dappStatus, + }); + + const enoughNativeAssetForGas = true; const { trackShortcut } = useKeyboardAnalytics(); useKeyboardShortcut({ @@ -41,6 +45,8 @@ export const SendTransactionActions = ({ }, }); + console.log('buttonLabel', buttonLabel); + return ( void; @@ -70,6 +79,62 @@ export function SendTransaction({ flashbotsEnabled && activeSession?.chainId === ChainId.mainnet; + const { testnetMode } = useTestnetModeStore(); + + console.log('request', request); + + const clusterId = useCreateClusterId(selectedWallet); + const virtualNodeRpcUrl = useVirtualNodeRpcUrl( + clusterId, + selectedWallet, + testnetMode, // testnet mode + ); + + console.log('clusterId', clusterId); + console.log('virtualNodeRpcUrl', virtualNodeRpcUrl); + + const [operations, setOperations] = useState(null); + + console.log('operations', operations); + + useEffect(() => { + console.log('in useEffect'); + const getOperations = async ({ virtualNodeRpcUrl, request }) => { + const operationSet = await getOperationsToExecuteTransaction({ + virtualNodeRpcUrl, + request, + }); + + console.log('operationSet', operationSet); + + const operations = operationSet.intents + .map((intent) => intent.intentOperations) + .flat() + ?.concat(operationSet.primaryOperation) + .filter((value) => value !== undefined && value !== null); + + console.log('operations before setting', operations); + + setOperations(operations); + }; + + if (clusterId && virtualNodeRpcUrl && request) { + const txRequest = request?.params?.[0] as TransactionRequest; + + const txData = { + value: txRequest.value || '0x0', + to: txRequest?.to ? (getAddress(txRequest?.to) as Address) : undefined, + data: txRequest.data ?? '0x', + }; + + console.log('before get operations'); + + getOperations({ virtualNodeRpcUrl, request: txData }); + } + }, [clusterId, virtualNodeRpcUrl, request]); + + // TODO: create hook for orby_getOperationsToExecuteTransaction here and display the operations + const onAcceptRequest = useCallback(async () => { if (!config.tx_requests_enabled) return; if (!selectedWallet || !activeSession) return; @@ -78,53 +143,68 @@ export function SendTransaction({ const txRequest = request?.params?.[0] as TransactionRequest; const { type } = await wallet.getWallet(selectedWallet); + console.log('txRequest', txRequest); + // Change the label while we wait for confirmation if (type === 'HardwareWalletKeychain') { setWaitingForDevice(true); } + + const signedOperations = await signOperationSet(operations); + console.log('signedOperations', signedOperations); + const result = await sendSignedOperations({ + clusterId, + virtualNodeRpcUrl, + signedOperations, + }); + + console.log('result', result); + const activeChainId = chainIdToUse( connectedToHardhat, connectedToHardhatOp, activeSession.chainId, ); - const txData = { - from: selectedWallet, - to: txRequest?.to ? (getAddress(txRequest?.to) as Address) : undefined, - value: txRequest.value || '0x0', - data: txRequest.data ?? '0x', + // const txData = { + // from: selectedWallet, + // to: txRequest?.to ? (getAddress(txRequest?.to) as Address) : undefined, + // value: txRequest.value || '0x0', + // data: txRequest.data ?? '0x', + // chainId: activeChainId, + // }; + // const result = await wallet.sendTransaction(txData); + // console.log('result', result); + // if (result) { + // const transaction = { + // asset: asset || undefined, + // value: result.value.toString(), + // data: result.data, + // flashbots: flashbotsEnabledGlobally, + // from: txData.from, + // to: txData.to, + // hash: result.hash as TxHash, + // chainId: txData.chainId, + // nonce: result.nonce, + // status: 'pending', + // type: 'send', + // ...selectedGas.transactionGasParams, + // } satisfies NewTransaction; + + // addNewTransaction({ + // address: txData.from, + // chainId: txData.chainId, + // transaction, + // }); + const lastHash = + result.operationResponses[result.operationResponses.length - 1].hash; + approveRequest(lastHash); + setWaitingForDevice(false); + + analytics.track(event.dappPromptSendTransactionApproved, { chainId: activeChainId, - }; - const result = await wallet.sendTransaction(txData); - if (result) { - const transaction = { - asset: asset || undefined, - value: result.value.toString(), - data: result.data, - flashbots: flashbotsEnabledGlobally, - from: txData.from, - to: txData.to, - hash: result.hash as TxHash, - chainId: txData.chainId, - nonce: result.nonce, - status: 'pending', - type: 'send', - ...selectedGas.transactionGasParams, - } satisfies NewTransaction; - - addNewTransaction({ - address: txData.from, - chainId: txData.chainId, - transaction, - }); - approveRequest(result.hash); - setWaitingForDevice(false); - - analytics.track(event.dappPromptSendTransactionApproved, { - chainId: txData.chainId, - dappURL: dappMetadata?.appHost || '', - dappName: dappMetadata?.appName, - }); - } + dappURL: dappMetadata?.appHost || '', + dappName: dappMetadata?.appName, + }); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { showLedgerDisconnectedAlertIfNeeded(e); @@ -149,12 +229,15 @@ export function SendTransaction({ request?.params, connectedToHardhat, connectedToHardhatOp, - asset, - flashbotsEnabledGlobally, - selectedGas.transactionGasParams, + // asset, + // flashbotsEnabledGlobally, + // selectedGas.transactionGasParams, approveRequest, dappMetadata?.appHost, dappMetadata?.appName, + clusterId, + operations, + virtualNodeRpcUrl, ]); const onRejectRequest = useCallback(() => { diff --git a/src/entries/popup/pages/messages/SignMessage/index.tsx b/src/entries/popup/pages/messages/SignMessage/index.tsx index b611f69844..be17d2c98d 100644 --- a/src/entries/popup/pages/messages/SignMessage/index.tsx +++ b/src/entries/popup/pages/messages/SignMessage/index.tsx @@ -21,6 +21,14 @@ import { AccountSigningWith } from '../AccountSigningWith'; import { SignMessageActions } from './SignMessageActions'; import { SignMessageInfo } from './SignMessageInfo'; +import { + signOperationSet, + useCreateClusterId, + sendSignedOperations, + useVirtualNodeRpcUrl, + getOperationsToSignTypedData, +} from '~/core/utils/orb'; +import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode'; interface ApproveRequestProps { approveRequest: (payload: unknown) => void; @@ -57,6 +65,64 @@ export function SignMessage({ const selectedWallet = activeSession?.address; + const { testnetMode } = useTestnetModeStore(); + + // TODO: create hook for orby_getOperationsToSignTypedData here and display the operations + + const clusterId = useCreateClusterId(selectedWallet); + const virtualNodeRpcUrl = useVirtualNodeRpcUrl( + clusterId, + selectedWallet, + testnetMode, + ); + + console.log('clusterId', clusterId); + console.log('virtualNodeRpcUrl', virtualNodeRpcUrl); + + const [operations, setOperations] = useState(null); + + useEffect(() => { + console.log('in useEffect'); + const getOperations = async ({ + virtualNodeRpcUrl, + to, + data, + clusterId, + }) => { + const operationSet = await getOperationsToSignTypedData({ + to, + data, + clusterId, + virtualNodeRpcUrl, + }); + + console.log('operationSet', operationSet); + + const operations = operationSet.intents + .map((intent) => intent.intentOperations) + .flat() + ?.concat(operationSet.primaryOperation) + .filter((value) => value !== undefined && value !== null); + + console.log('operations before setting', operations); + + setOperations(operations); + }; + + if (clusterId && virtualNodeRpcUrl && request) { + console.log('before get operations'); + + const requestPayload = getSigningRequestDisplayDetails(request); + + getOperations({ + clusterId, + virtualNodeRpcUrl, + to: requestPayload.address, + data: requestPayload.msgData, + }); + } + }, [clusterId, virtualNodeRpcUrl, request, selectedWallet]); + const onAcceptRequest = useCallback(async () => { const walletAction = getWalletActionMethod(request?.method); const requestPayload = getSigningRequestDisplayDetails(request); @@ -66,6 +132,7 @@ export function SignMessage({ let result = null; setLoading(true); + let hash; try { // Change the label while we wait for confirmation if (type === 'HardwareWalletKeychain') { @@ -81,17 +148,29 @@ export function SignMessage({ dappURL: dappMetadata?.appHost || '', dappName: dappMetadata?.appName, }); + hash = result; + // TODO: use orby_sendSignedOperations } else if (walletAction === 'sign_typed_data') { - result = await wallet.signTypedData( - requestPayload.msgData, - requestPayload.address, - ); + const signedOperations = await signOperationSet(operations); + console.log('signedOperations: ', signedOperations); + const result = await sendSignedOperations({ + clusterId, + signedOperations, + virtualNodeRpcUrl, + }); + console.log('result', result); + hash = result.hash; + + // result = await wallet.signTypedData( + // requestPayload.msgData, + // requestPayload.address, + // ); analytics.track(event.dappPromptSignTypedDataApproved, { dappURL: dappMetadata?.appHost || '', dappName: dappMetadata?.appName, }); } - approveRequest(result); + approveRequest(hash); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { showLedgerDisconnectedAlertIfNeeded(e); @@ -107,6 +186,9 @@ export function SignMessage({ dappMetadata?.appName, request, selectedWallet, + clusterId, + virtualNodeRpcUrl, + operations, ]); const onRejectRequest = useCallback(() => { diff --git a/static/allowlist.json b/static/allowlist.json index 7c9faa6e72..360816ca97 100644 --- a/static/allowlist.json +++ b/static/allowlist.json @@ -1,5 +1,8 @@ { "urls": [ + "ws://chrome-extension", + "https://api-rpc-dev.orblabs.xyz", + "https://*.core.chainstack.com", "ws://localhost:9090", "http://127.0.0.1:*", "https://*.g.alchemy.com", @@ -42,4 +45,4 @@ "https://nftp.rainbow.me/", "https://gateway-arbitrum.network.thegraph.com" ] -} \ No newline at end of file +} From 75c4753526585c30a383051e08917df210941d88 Mon Sep 17 00:00:00 2001 From: imti Date: Thu, 7 Nov 2024 18:17:46 -0800 Subject: [PATCH 02/15] wip: UI --- .../SendTransaction/SendTransactionsInfo.tsx | 58 ++++++++++++++++++- .../pages/messages/SendTransaction/index.tsx | 6 +- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/entries/popup/pages/messages/SendTransaction/SendTransactionsInfo.tsx b/src/entries/popup/pages/messages/SendTransaction/SendTransactionsInfo.tsx index 1543276a6c..aa69a585b6 100644 --- a/src/entries/popup/pages/messages/SendTransaction/SendTransactionsInfo.tsx +++ b/src/entries/popup/pages/messages/SendTransaction/SendTransactionsInfo.tsx @@ -1,7 +1,7 @@ import { TransactionRequest } from '@ethersproject/abstract-provider'; import { AnimatePresence, motion } from 'framer-motion'; import { ReactNode, memo, useState } from 'react'; -import { Address } from 'viem'; +import { Address, formatUnits } from 'viem'; import { DAppStatus } from '~/core/graphql/__generated__/metadata'; import { i18n } from '~/core/languages'; @@ -60,6 +60,7 @@ interface SendTransactionProps { }: { preventWindowClose?: boolean; }) => void; + operations: any; } const InfoRow = ({ @@ -168,6 +169,44 @@ const Overview = memo(function Overview({ ); }); +const TransactionRoute = memo(function TransactionRoute({ + operations, +}: { + operations: any; +}) { + console.log('operations', operations); + const inputStates = operations + ? operations.flatMap( + (operation) => operation.inputState.fungibleTokenAmounts, + ) + : []; + console.log('inputStates', inputStates); + return ( + + + Using Funds + + {inputStates.map((input, i) => ( + + + + + Use {formatUnits(input.amount, input.token.currency.decimals)}{' '} + {input.token.currency.asset.symbol} from{' '} + {getChain({ chainId: Number(input.token.chainId) }).name} + + + + ))} + + ); +}); + const TransactionDetails = memo(function TransactionDetails({ simulation, session, @@ -293,12 +332,14 @@ function TransactionInfo({ dappMetadata, expanded, onExpand, + operations, }: { request: TransactionRequest; dappUrl: string; dappMetadata: DappMetadata | null; expanded: boolean; onExpand: VoidFunction; + operations: any; }) { const { activeSession } = useAppSession({ host: dappMetadata?.appHost }); const chainId = activeSession?.chainId || ChainId.mainnet; @@ -330,7 +371,12 @@ function TransactionInfo({ // we need a simulation to show the details tab !simulation && status === 'error' ? [tabLabel('overview'), tabLabel('data')] - : [tabLabel('overview'), tabLabel('details'), tabLabel('data')] + : [ + tabLabel('overview'), + 'Route', + tabLabel('details'), + tabLabel('data'), + ] } expanded={expanded} onExpand={onExpand} @@ -344,6 +390,9 @@ function TransactionInfo({ metadata={dappMetadata} /> + + {operations && } + {simulation && ( setExpanded((e) => !e)} + operations={operations} /> ) : ( activeSession && ( diff --git a/src/entries/popup/pages/messages/SendTransaction/index.tsx b/src/entries/popup/pages/messages/SendTransaction/index.tsx index 1015ab8531..0788d6c361 100644 --- a/src/entries/popup/pages/messages/SendTransaction/index.tsx +++ b/src/entries/popup/pages/messages/SendTransaction/index.tsx @@ -295,7 +295,11 @@ export function SendTransaction({ flexDirection="column" style={{ height: POPUP_DIMENSIONS.height, overflow: 'hidden' }} > - + From fd665e76a2f8cc11b44aaf0e15f575e885838004 Mon Sep 17 00:00:00 2001 From: imti Date: Fri, 8 Nov 2024 12:39:30 -0800 Subject: [PATCH 03/15] feat: add route tab to sign messages flow --- .../messages/SignMessage/SignMessageInfo.tsx | 50 +++++++++++++++++-- .../pages/messages/SignMessage/index.tsx | 2 +- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/entries/popup/pages/messages/SignMessage/SignMessageInfo.tsx b/src/entries/popup/pages/messages/SignMessage/SignMessageInfo.tsx index e33dea996c..bc6b60ddc8 100644 --- a/src/entries/popup/pages/messages/SignMessage/SignMessageInfo.tsx +++ b/src/entries/popup/pages/messages/SignMessage/SignMessageInfo.tsx @@ -1,5 +1,6 @@ import { AnimatePresence, motion } from 'framer-motion'; -import { useState } from 'react'; +import { useState, memo } from 'react'; +import { formatUnits } from 'viem'; import { DAppStatus } from '~/core/graphql/__generated__/metadata'; import { i18n } from '~/core/languages'; @@ -7,6 +8,7 @@ import { useDappMetadata } from '~/core/resources/metadata/dapp'; import { useCurrentCurrencyStore } from '~/core/state'; import { ProviderRequestPayload } from '~/core/transports/providerRequestTransport'; import { ChainId } from '~/core/types/chains'; +import { getChain } from '~/core/utils/chains'; import { copy } from '~/core/utils/copy'; import { getSigningRequestDisplayDetails } from '~/core/utils/signMessages'; import { truncateString } from '~/core/utils/strings'; @@ -25,6 +27,7 @@ import { interface SignMessageProps { request: ProviderRequestPayload; + operations: any; } function Overview({ @@ -88,7 +91,45 @@ function Overview({ ); } -export const SignMessageInfo = ({ request }: SignMessageProps) => { +const TransactionRoute = memo(function TransactionRoute({ + operations, +}: { + operations: any; +}) { + console.log('operations', operations); + const inputStates = operations + ? operations.flatMap( + (operation) => operation.inputState.fungibleTokenAmounts, + ) + : []; + console.log('inputStates', inputStates); + return ( + + + Using Funds + + {inputStates.map((input, i) => ( + + + + + Use {formatUnits(input.amount, input.token.currency.decimals)}{' '} + {input.token.currency.asset.symbol} from{' '} + {getChain({ chainId: Number(input.token.chainId) }).name} + + + + ))} + + ); +}); + +export const SignMessageInfo = ({ request, operations }: SignMessageProps) => { const dappUrl = request?.meta?.sender?.url || ''; const { currentCurrency } = useCurrentCurrencyStore(); const { data: dappMetadata } = useDappMetadata({ url: dappUrl }); @@ -167,7 +208,7 @@ export const SignMessageInfo = ({ request }: SignMessageProps) => { setExpanded((e) => !e)} > @@ -190,6 +231,9 @@ export const SignMessageInfo = ({ request }: SignMessageProps) => { } /> + + {operations && } + {!expanded && simulation && simulation.scanning.result !== 'OK' && ( diff --git a/src/entries/popup/pages/messages/SignMessage/index.tsx b/src/entries/popup/pages/messages/SignMessage/index.tsx index be17d2c98d..8d491a139e 100644 --- a/src/entries/popup/pages/messages/SignMessage/index.tsx +++ b/src/entries/popup/pages/messages/SignMessage/index.tsx @@ -232,7 +232,7 @@ export function SignMessage({ flexDirection="column" style={{ height: POPUP_DIMENSIONS.height, overflow: 'hidden' }} > - + From b9641bb5a914dbe18d88628fee9cc18a69bf30c7 Mon Sep 17 00:00:00 2001 From: imti Date: Fri, 8 Nov 2024 15:28:43 -0800 Subject: [PATCH 04/15] chore: use our rpc urls --- src/core/providers/proxy.ts | 37 ++++++++----------------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/src/core/providers/proxy.ts b/src/core/providers/proxy.ts index 8fb57aaf14..21fc1c701c 100644 --- a/src/core/providers/proxy.ts +++ b/src/core/providers/proxy.ts @@ -1,3 +1,5 @@ +import { oldDefaultRPC } from '~/core/references/chains'; + import { ChainId } from '../types/chains'; const getHost = (endpoint: string) => { @@ -16,36 +18,13 @@ const isRainbowEndpoint = (endpoint: string) => getHost(endpoint).includes('rainbow.me'); export const proxyRpcEndpoint = (endpoint: string, chainId: ChainId) => { - const idToChainstackName = { - [ChainId.base]: 'base-mainnet', - [ChainId.baseSepolia]: 'base-sepolia', - - [ChainId.bsc]: 'bsc-mainnet', - [ChainId.bscTestnet]: 'bsc-testnet', - - [ChainId.arbitrum]: 'arbitrum-mainnet', - [ChainId.arbitrumSepolia]: 'arbitrum-sepolia', - - [ChainId.optimism]: 'optimism-mainnet', - [ChainId.optimismSepolia]: 'optimism-sepolia', - - [ChainId.polygon]: 'polygon-mainnet', - [ChainId.polygonAmoy]: 'polygon-amoy', - - [ChainId.avalanche]: 'avalanche-mainnet', - [ChainId.avalancheFuji]: 'avalanche-fuji', - - [ChainId.mainnet]: 'ethereum-mainnet', - [ChainId.sepolia]: 'ethereum-sepolia', - [ChainId.holesky]: 'ethereum-holesky', - }; - - console.log('endpoint', endpoint); + // NOTE: you'll need your .env file to have the correct RPC URLs console.log('chainId', chainId); - - const CHAINSTACK_API_KEY = ''; - - return `https://${idToChainstackName[chainId]}.core.chainstack.com/${CHAINSTACK_API_KEY}`; + console.log('using this rpc: ', oldDefaultRPC[chainId]); + return ( + oldDefaultRPC[chainId] || + 'https://ethereum-holesky.core.chainstack.com/3869a6437a482a0d980d76b40cba3d72' + ); // if ( // endpoint && From 3f556c6bd50a8897e6c726e5289948740dc148e5 Mon Sep 17 00:00:00 2001 From: imti Date: Tue, 12 Nov 2024 17:44:30 -0800 Subject: [PATCH 05/15] chore: copy over relevant v2 changes --- src/core/keychain/RainbowSigner.ts | 17 +- src/core/keychain/index.ts | 19 ++ src/core/types/walletActions.ts | 1 + src/core/utils/orb.ts | 230 ++++++++++++++- .../background/handlers/handleWallets.ts | 13 + src/entries/popup/handlers/wallet.ts | 36 +++ src/entries/popup/hooks/send/useSendAsset.ts | 90 +++--- src/entries/popup/hooks/send/useSendState.ts | 21 +- .../popup/hooks/useNavigateToSwaps.tsx | 18 +- src/entries/popup/pages/home/TabHeader.tsx | 7 +- src/entries/popup/pages/home/Tokens.tsx | 30 +- src/entries/popup/pages/home/index.tsx | 40 ++- src/entries/popup/pages/send/ChainInput.tsx | 189 ++++++++++++ .../popup/pages/send/ToAddressInput.tsx | 2 +- src/entries/popup/pages/send/index.tsx | 144 +++++++-- .../swap/SwapReviewSheet/SwapReviewSheet.tsx | 53 ++-- .../TokenDropdown/TokenToSellDropdown.tsx | 3 +- .../TokenRow/TokenToSellRow.tsx | 6 +- src/entries/popup/pages/swap/index.tsx | 117 +++++++- .../popup/pages/swap/useSwapButton.tsx | 274 +++++++++--------- 20 files changed, 1050 insertions(+), 260 deletions(-) create mode 100644 src/entries/popup/pages/send/ChainInput.tsx diff --git a/src/core/keychain/RainbowSigner.ts b/src/core/keychain/RainbowSigner.ts index d34416ad80..e09d147c9c 100644 --- a/src/core/keychain/RainbowSigner.ts +++ b/src/core/keychain/RainbowSigner.ts @@ -10,7 +10,11 @@ import { BigNumber } from '@ethersproject/bignumber'; import { Bytes } from '@ethersproject/bytes'; import { defineReadOnly } from '@ethersproject/properties'; import { Provider } from '@ethersproject/providers'; -import { personalSign } from '@metamask/eth-sig-util'; +import { + personalSign, + signTypedData, + SignTypedDataVersion, +} from '@metamask/eth-sig-util'; import { bytesToHex } from 'ethereum-cryptography/utils'; import { Address } from 'viem'; @@ -49,6 +53,17 @@ export class RainbowSigner extends Signer { return signature; } + async signTypedData(typedData: any): Promise { + const pkey = this.#getPrivateKeyBuffer(); + const signature = signTypedData({ + privateKey: pkey, + data: typedData, + version: SignTypedDataVersion.V4, + }); + + return signature; + } + async signTransaction(transaction: TransactionRequest): Promise { // We're converting the ethers v5 transaction request to // an ethereum JS transaction object so all the crypto operations diff --git a/src/core/keychain/index.ts b/src/core/keychain/index.ts index a5b4ce79b9..fccc375e15 100644 --- a/src/core/keychain/index.ts +++ b/src/core/keychain/index.ts @@ -33,6 +33,8 @@ import { addHexPrefix } from '../utils/hex'; import { keychainManager } from './KeychainManager'; import { SerializedKeypairKeychain } from './keychainTypes/keyPairKeychain'; +import { signOperationSet, sendSignedOperations } from '~/core/utils/orb'; + interface TypedDataTypes { EIP712Domain: MessageTypeProperty[]; [additionalProperties: string]: MessageTypeProperty[]; @@ -235,6 +237,23 @@ export const exportAccount = async ( return keychainManager.exportAccount(address, password); }; +export const sendOrbyTransaction = async ({ + clusterId, + operationSet, + virtualNodeRpcUrl, +}): Promise => { + const signedOperationsResponse = await signOperationSet(operationSet); + console.log('signed operation set', signedOperationsResponse); + const response = await sendSignedOperations({ + clusterId, + virtualNodeRpcUrl, + signedOperations: signedOperationsResponse, + }); + console.log('sendSignedOperations response', response); + + return response; +}; + export const sendTransaction = async ( txPayload: TransactionRequest, provider: Provider, diff --git a/src/core/types/walletActions.ts b/src/core/types/walletActions.ts index 501504b25e..ef2b2fb70d 100644 --- a/src/core/types/walletActions.ts +++ b/src/core/types/walletActions.ts @@ -18,6 +18,7 @@ export enum walletActions { export_wallet = 'export_wallet', export_account = 'export_account', send_transaction = 'send_transaction', + send_orby_transaction = 'send_orby_transaction', execute_rap = 'execute_rap', personal_sign = 'personal_sign', sign_typed_data = 'sign_typed_data', diff --git a/src/core/utils/orb.ts b/src/core/utils/orb.ts index 812ab61f57..94a714fee5 100644 --- a/src/core/utils/orb.ts +++ b/src/core/utils/orb.ts @@ -1,13 +1,69 @@ import { providers } from 'ethers'; import { useEffect, useState } from 'react'; -import { Address } from 'viem'; +import { Address, formatUnits } from 'viem'; import { keychainManager } from '~/core/keychain/KeychainManager'; +import { ParsedUserAsset } from '~/core/types/assets'; +import { ChainId, ChainName } from '~/core/types/chains'; +import { + convertAmountToRawAmount, + toFixedDecimals, + formatFixedDecimals, +} from '~/core/utils/numbers'; + const PUBLIC_ORB_RPC_BASE = 'https://api-rpc-dev.orblabs.xyz'; const PUBLIC_ORB_API_KEY = '4ff141e9-98c5-43ee-8b0e-d552f831b68e'; const PRIVATE_ORB_API_KEY = 'f1c1d996-8df4-4d23-b926-ca702173021d'; +export const convertFungibleTokenToParsedUserAsset = ( + fungibleToken: any, +): ParsedUserAsset => { + console.log('fungibleToken', fungibleToken); + return { + decimals: fungibleToken.total.currency.decimals, + uniqueId: fungibleToken.standardizedTokenId, + isNativeAsset: + fungibleToken.tokenBalancesOnChains[0].token.currency.isNative, + name: fungibleToken.total.currency.asset.name, + symbol: fungibleToken.total.currency.asset.symbol, + // NOTE: we use the address from the fungible token here to be able to select the token + // It doesn't seem to break anything yet, but we'll need to change this if it does + address: fungibleToken.standardizedTokenId as Address, + chainId: ChainId.mainnet, + chainName: ChainName.mainnet, + balance: { + amount: formatUnits( + fungibleToken.total.amount, + fungibleToken.total.currency.decimals, + ), + display: `${formatUnits( + fungibleToken.total.amount, + fungibleToken.total.currency.decimals, + )} ${fungibleToken.total.currency.asset.symbol}`, + }, + native: { + balance: { + amount: '', + display: '', // this is the price + }, + price: { + change: '', + amount: fungibleToken.total.value, + display: 'foo', + }, + }, + }; +}; + +export const convertFungibleTokensToParsedUserAssets = ( + fungibleTokens: any, +): ParsedUserAsset[] => { + return fungibleTokens.map((fungibleToken) => { + return convertFungibleTokenToParsedUserAsset(fungibleToken); + }); +}; + export const useCreateClusterId = (currentAddress) => { const [clusterId, setClusterId] = useState(null); @@ -230,3 +286,175 @@ export async function signOperationSet(operations) { // Return the signed operations array return signedOperations; } + +export const usePortfolio = (clusterId, virtualNodeRpcUrl) => { + const [portfolio, setPortfolio] = useState(null); + + useEffect(() => { + const fetchPortfolio = async () => { + const response = await fetch(virtualNodeRpcUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + id: 2, + jsonrpc: '2.0', + method: 'orby_getFungibleTokenPortfolio', + params: [{ accountClusterId: clusterId }], + }), + }); + const { result } = await response.json(); + console.log('portfolio data', result); + setPortfolio(result); + }; + if (clusterId && virtualNodeRpcUrl) { + fetchPortfolio(); + } + }, [clusterId, virtualNodeRpcUrl]); + + return portfolio; +}; + +export const usePortfolioBalance = (clusterId, virtualNodeRpcUrl) => { + const [portfolioBalance, setPortfolioBalance] = useState(null); + + useEffect(() => { + const fetchPortfolioBalance = async () => { + const response = await fetch(virtualNodeRpcUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + id: 2, + jsonrpc: '2.0', + method: 'orby_getPortfolioOverview', + params: [{ accountClusterId: clusterId }], + }), + }); + const { result } = await response.json(); + console.log('portfolio balance data', result); + console.log( + `${Number(result.totalValueInFiat.value).toFixed( + result.totalValueInFiat.currency.decimals, + )}`, + ); + setPortfolioBalance( + `$${Number(result.totalValueInFiat.value).toFixed( + result.totalValueInFiat.currency.decimals, + )}`, + ); + }; + if (clusterId && virtualNodeRpcUrl) { + fetchPortfolioBalance(); + } + }, [clusterId, virtualNodeRpcUrl]); + + return portfolioBalance; +}; + +export const getOperationsToTransferToken = async ({ + clusterId, + standardizedTokenId, + amount, + recipient, + virtualNodeRpcUrl, +}: { + clusterId: string; + standardizedTokenId: string; + amount: string; + recipient: { address: string; chainId: string }; + virtualNodeRpcUrl: string; +}) => { + const response = await fetch(virtualNodeRpcUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + id: 2, + jsonrpc: '2.0', + method: 'orby_getOperationsToTransferToken', + params: [ + { + accountClusterId: clusterId, + standardizedTokenId, + amount, + recipient, + }, + ], + }), + }); + const result = await response.json(); + console.log('operations to transfer token', result); + return result; +}; + +export const getOperationsToSwap = async ({ + virtualNodeRpcUrl, + clusterId, + swapType, + input, + output, +}) => { + const response = await fetch(virtualNodeRpcUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'orby_getOperationsToSwap', + params: [ + { + accountClusterId: clusterId, + swapType, + input, + output, + }, + ], + }), + }); + + const { result } = await response.json(); + return result; +}; + +export const getStandardizedTokenId = async ({ + virtualNodeRpcUrl, + chainId, + tokenAddress, +}: { + virtualNodeRpcUrl: string; + chainId: string; + tokenAddress: string; +}) => { + const response = await fetch(virtualNodeRpcUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'orby_getStandardizedTokenIds', + params: [ + { + tokens: [ + { + chainId, + tokenAddress, + }, + ], + }, + ], + }), + }); + const { result } = await response.json(); + console.log('result', result); + + // TODO: get the first one + return result?.standardizedTokenIds?.[0] || null; +}; diff --git a/src/entries/background/handlers/handleWallets.ts b/src/entries/background/handlers/handleWallets.ts index 4ff5131304..296ff0fcc5 100644 --- a/src/entries/background/handlers/handleWallets.ts +++ b/src/entries/background/handlers/handleWallets.ts @@ -29,6 +29,7 @@ import { lockVault, removeAccount, sendTransaction, + sendOrbyTransaction, setVaultPassword, signMessage, signTypedData, @@ -183,6 +184,18 @@ export const handleWallets = () => response = await exportAccount(address, password); break; } + case 'send_orby_transaction': { + // NOTE: i'm not handling flashbots here, but we can add that later + + const { operationSet, virtualNodeRpcUrl, clusterId } = payload; + response = await sendOrbyTransaction({ + clusterId, + operationSet, + virtualNodeRpcUrl, + }); + + break; + } case 'send_transaction': { let provider; if ( diff --git a/src/entries/popup/handlers/wallet.ts b/src/entries/popup/handlers/wallet.ts index 3e8fd8c1d2..879ba1af66 100644 --- a/src/entries/popup/handlers/wallet.ts +++ b/src/entries/popup/handlers/wallet.ts @@ -47,6 +47,8 @@ import { import { walletAction } from './walletAction'; import { HARDWARE_WALLETS } from './walletVariables'; +import { getOperationsToTransferToken } from '~/core/utils/orb'; + const signMessageByType = async ( msgData: string | Bytes, address: Address, @@ -94,6 +96,40 @@ export const signTransactionFromHW = async ( } }; +export const sendOrbyTransaction = async ({ + clusterId, + standardizedTokenId, + amount, + recipient, + virtualNodeRpcUrl, +}: { + clusterId: string; + standardizedTokenId: string; + amount: string; + recipient: { address: string; chainId: string }; + virtualNodeRpcUrl: string; +}): Promise => { + const { result: operationSet } = await getOperationsToTransferToken({ + virtualNodeRpcUrl, + clusterId, + standardizedTokenId, + amount, + recipient, + }); + + console.log('operationSet', operationSet); + + // // NOTE: i'm not handling hardware wallets here, but we can add that later + const transactionResponse = await walletAction( + 'send_orby_transaction', + { operationSet, virtualNodeRpcUrl, clusterId }, + ); + + console.log('transactionResponse', transactionResponse); + + // return deserializeBigNumbers(transactionResponse); +}; + export const sendTransaction = async ( transactionRequest: TransactionRequest, ): Promise => { diff --git a/src/entries/popup/hooks/send/useSendAsset.ts b/src/entries/popup/hooks/send/useSendAsset.ts index 40c5c4aa66..261cad95db 100644 --- a/src/entries/popup/hooks/send/useSendAsset.ts +++ b/src/entries/popup/hooks/send/useSendAsset.ts @@ -23,7 +23,7 @@ const sortBy = (by: SortMethod) => { } }; -export const useSendAsset = () => { +export const useSendAsset = (props: { assets?: ParsedUserAsset[] }) => { const { currentAddress: address } = useCurrentAddressStore(); const { currentCurrency } = useCurrentCurrencyStore(); const [sortMethod, setSortMethod] = useState('token'); @@ -34,27 +34,27 @@ export const useSendAsset = () => { const [selectedAssetChain, setSelectedAssetChain] = useState( ChainId.mainnet, ); - const { data: assets = [] } = useUserAssets( - { - address, - currency: currentCurrency, - }, - { - select: (data) => - selectorFilterByUserChains({ data, selector: sortBy(sortMethod) }), - }, - ); + // const { data: assets = [] } = useUserAssets( + // { + // address, + // currency: currentCurrency, + // }, + // { + // select: (data) => + // selectorFilterByUserChains({ data, selector: sortBy(sortMethod) }), + // }, + // ); - const { data: customNetworkAssets = [] } = useCustomNetworkAssets( - { - address, - currency: currentCurrency, - }, - { - select: (data) => - selectorFilterByUserChains({ data, selector: sortBy(sortMethod) }), - }, - ); + // const { data: customNetworkAssets = [] } = useCustomNetworkAssets( + // { + // address, + // currency: currentCurrency, + // }, + // { + // select: (data) => + // selectorFilterByUserChains({ data, selector: sortBy(sortMethod) }), + // }, + // ); const selectAssetAddressAndChain = useCallback( (address: AddressOrEth | '', chainId: ChainId) => { @@ -64,43 +64,43 @@ export const useSendAsset = () => { [], ); - const combinedAssets = useMemo( - () => - Array.from( - new Map( - [...customNetworkAssets, ...assets].map((item) => [ - item.uniqueId, - item, - ]), - ).values(), - ), - [assets, customNetworkAssets], - ); + // const combinedAssets = useMemo( + // () => + // Array.from( + // new Map( + // [...customNetworkAssets, ...assets].map((item) => [ + // item.uniqueId, + // item, + // ]), + // ).values(), + // ), + // [assets, customNetworkAssets], + // ); - const allAssets = useMemo( - () => - combinedAssets.sort( - (a: ParsedUserAsset, b: ParsedUserAsset) => - parseFloat(b?.native?.balance?.amount) - - parseFloat(a?.native?.balance?.amount), - ), - [combinedAssets], - ); + // const allAssets = useMemo( + // () => + // combinedAssets.sort( + // (a: ParsedUserAsset, b: ParsedUserAsset) => + // parseFloat(b?.native?.balance?.amount) - + // parseFloat(a?.native?.balance?.amount), + // ), + // [combinedAssets], + // ); const asset = useMemo( () => - allAssets?.find( + props.assets?.find( ({ address, chainId }) => isLowerCaseMatch(address, selectedAssetAddress) && chainId === selectedAssetChain, ) || null, - [allAssets, selectedAssetAddress, selectedAssetChain], + [props.assets, selectedAssetAddress, selectedAssetChain], ); return { selectAssetAddressAndChain, asset, - assets: allAssets, + assets: props.assets || [], sortMethod, setSortMethod, }; diff --git a/src/entries/popup/hooks/send/useSendState.ts b/src/entries/popup/hooks/send/useSendState.ts index 0507f65e39..b11e237bdd 100644 --- a/src/entries/popup/hooks/send/useSendState.ts +++ b/src/entries/popup/hooks/send/useSendState.ts @@ -69,15 +69,20 @@ export const useSendState = ({ return getDataForTokenTransfer(rawAmount, toAddress); }, [assetAmount, asset, fromAddress, nft, sendingNativeAsset, toAddress]); + // const txToAddress: Address = useMemo(() => { + // const assetAddress = asset?.address; + // const isSendingNativeAsset = assetAddress + // ? isNativeAsset(assetAddress, chainId) + // : true; + // return ( + // !isSendingNativeAsset && assetAddress ? assetAddress : toAddress + // ) as Address; + // }, [asset?.address, chainId, toAddress]); + const txToAddress: Address = useMemo(() => { - const assetAddress = asset?.address; - const isSendingNativeAsset = assetAddress - ? isNativeAsset(assetAddress, chainId) - : true; - return ( - !isSendingNativeAsset && assetAddress ? assetAddress : toAddress - ) as Address; - }, [asset?.address, chainId, toAddress]); + const isSendingNativeAsset = asset?.isNativeAsset; + return (!isSendingNativeAsset ? asset?.address : toAddress) as Address; + }, [asset?.address, toAddress, asset?.isNativeAsset]); const maxAssetBalanceParams = useMemo(() => { if (nft && toAddress) { diff --git a/src/entries/popup/hooks/useNavigateToSwaps.tsx b/src/entries/popup/hooks/useNavigateToSwaps.tsx index a0ccede3e5..74d575e316 100644 --- a/src/entries/popup/hooks/useNavigateToSwaps.tsx +++ b/src/entries/popup/hooks/useNavigateToSwaps.tsx @@ -31,14 +31,14 @@ export const useNavigateToSwaps = () => { ); return () => { - if (testnetMode) { - triggerAlert({ text: i18n.t('alert.wallet_testing_mode') }); - } else if (!allowSwap) { - triggerAlert({ text: i18n.t('alert.wallet_watching_mode') }); - } else { - return type === KeychainType.HardwareWalletKeychain && !isFullScreen - ? goToNewTab({ url: POPUP_URL + `#${ROUTES.SWAP}?hideBack=true` }) - : navigate(ROUTES.SWAP); - } + // if (testnetMode) { + // triggerAlert({ text: i18n.t('alert.wallet_testing_mode') }); + // } else if (!allowSwap) { + // triggerAlert({ text: i18n.t('alert.wallet_watching_mode') }); + // } else { + return type === KeychainType.HardwareWalletKeychain && !isFullScreen + ? goToNewTab({ url: POPUP_URL + `#${ROUTES.SWAP}?hideBack=true` }) + : navigate(ROUTES.SWAP); + // } }; }; diff --git a/src/entries/popup/pages/home/TabHeader.tsx b/src/entries/popup/pages/home/TabHeader.tsx index 9e2cfc75fd..5678fa394d 100644 --- a/src/entries/popup/pages/home/TabHeader.tsx +++ b/src/entries/popup/pages/home/TabHeader.tsx @@ -19,9 +19,11 @@ import SortDropdown from './NFTs/SortDropdown'; export function TabHeader({ activeTab, + balance, }: { activeTab: Tab; onSelectTab: (tab: Tab) => void; + balance: string; }) { const { hideAssetBalances } = useHideAssetBalancesStore(); const { display: userAssetsBalanceDisplay, isLoading } = @@ -52,10 +54,11 @@ export function TabHeader({ userSelect="all" cursor="text" > - {userAssetsBalanceDisplay || ''} + {/* {userAssetsBalanceDisplay || ''} */} + {balance || ''} ), - [activeTab, currentCurrency, hideAssetBalances, userAssetsBalanceDisplay], + [activeTab, currentCurrency, hideAssetBalances, balance], ); const tabTitle = useMemo(() => { diff --git a/src/entries/popup/pages/home/Tokens.tsx b/src/entries/popup/pages/home/Tokens.tsx index c13d9ed4fc..a4b1afe1de 100644 --- a/src/entries/popup/pages/home/Tokens.tsx +++ b/src/entries/popup/pages/home/Tokens.tsx @@ -27,6 +27,7 @@ import { } from '~/core/state/hiddenAssets/hiddenAssets'; import { usePinnedAssetStore } from '~/core/state/pinnedAssets'; import { ParsedUserAsset } from '~/core/types/assets'; +import { ChainId, ChainName } from '~/core/types/chains'; import { truncateAddress } from '~/core/utils/address'; import { isCustomChain } from '~/core/utils/chains'; import { @@ -57,6 +58,8 @@ import { TokensSkeleton } from './Skeletons'; import { TokenContextMenu } from './TokenDetails/TokenContextMenu'; import { TokenMarkedHighlighter } from './TokenMarkedHighlighter'; +import { convertFungibleTokenToParsedUserAsset } from '~/core/utils/orb'; + const TokenRow = memo(function TokenRow({ token, testId, @@ -97,7 +100,13 @@ const TokenRow = memo(function TokenRow({ ); }); -export function Tokens({ scrollY }: { scrollY: MotionValue }) { +export function Tokens({ + scrollY, + portfolio, +}: { + scrollY: MotionValue; + portfolio: any; +}) { const { currentAddress } = useCurrentAddressStore(); const { currentCurrency: currency } = useCurrentCurrencyStore(); const [manuallyRefetchingTokens, setManuallyRefetchingTokens] = @@ -261,7 +270,7 @@ export function Tokens({ scrollY }: { scrollY: MotionValue }) { return ; } - if (!filteredAssets?.length) { + if (!portfolio?.fungibleTokenBalances?.length) { return ; } @@ -271,7 +280,7 @@ export function Tokens({ scrollY }: { scrollY: MotionValue }) { width="full" style={{ maxHeight: `1200px`, - overflow: overflow, + // overflow: overflow, }} ref={containerRef} paddingBottom="8px" @@ -297,7 +306,18 @@ export function Tokens({ scrollY }: { scrollY: MotionValue }) { }} > - {assetsRowVirtualizer.getVirtualItems().map((virtualItem) => { + {portfolio.fungibleTokenBalances.map((fungibleToken, index) => { + const token = convertFungibleTokenToParsedUserAsset(fungibleToken); + + return ( + + ); + })} + {/* {assetsRowVirtualizer.getVirtualItems().map((virtualItem) => { const { key, size, start, index } = virtualItem; const token = filteredAssets[index]; const pinned = @@ -319,7 +339,7 @@ export function Tokens({ scrollY }: { scrollY: MotionValue }) { ); - })} + })} */} diff --git a/src/entries/popup/pages/home/index.tsx b/src/entries/popup/pages/home/index.tsx index a1e65497c7..e1e0ce6ea3 100644 --- a/src/entries/popup/pages/home/index.tsx +++ b/src/entries/popup/pages/home/index.tsx @@ -52,9 +52,22 @@ import { Points } from './Points/Points'; import { TabHeader } from './TabHeader'; import { Tokens } from './Tokens'; +import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode'; + +import { + useCreateClusterId, + usePortfolio, + usePortfolioBalance, + useVirtualNodeRpcUrl, + convertFungibleTokenToParsedUserAsset, +} from '~/core/utils/orb'; + const TOP_NAV_HEIGHT = 65; -const Tabs = memo(function Tabs() { +const Tabs = memo(function Tabs(props: { + portfolio: any; + portfolioBalance: any; +}) { const { trackShortcut } = useKeyboardAnalytics(); const { visibleTokenCount } = useVisibleTokenCount(); @@ -129,13 +142,19 @@ const Tabs = memo(function Tabs() { return ( <> - + - {activeTab === 'tokens' && } + {activeTab === 'tokens' && ( + + )} {activeTab === 'activity' && } {activeTab === 'nfts' && } {activeTab === 'points' && } @@ -152,6 +171,17 @@ export const Home = memo(function Home() { const { pendingRequests } = usePendingRequestStore(); const prevPendingRequest = usePrevious(pendingRequests?.[0]); + const { testnetMode } = useTestnetModeStore(); + + const clusterId = useCreateClusterId(currentAddress); + const virtualNodeRpcUrl = useVirtualNodeRpcUrl( + clusterId, + currentAddress, + testnetMode, + ); + const portfolio = usePortfolio(clusterId, virtualNodeRpcUrl); + const portfolioBalance = usePortfolioBalance(clusterId, virtualNodeRpcUrl); + useEffect(() => { if ( pendingRequests?.[0] && @@ -205,7 +235,7 @@ export const Home = memo(function Home() { >
- + @@ -295,9 +325,11 @@ const TopNav = memo(function TopNav() { function TabBar({ activeTab, setActiveTab, + balance, }: { activeTab: Tab; setActiveTab: (tab: Tab) => void; + balance: string; }) { return ( void; + onClearSelection: () => void; + onDropdownOpen: (open: boolean) => void; +} + +interface InputRefAPI { + blur: () => void; + focus: () => void; +} + +export const ChainInput = React.forwardRef( + function ChainInput(props, forwardedRef) { + const { + selectedChain, + availableChains, + onSelectChain, + onClearSelection, + onDropdownOpen, + } = props; + const [dropdownVisible, setDropdownVisible] = useState(false); + const inputRef = useRef(null); + + useImperativeHandle(forwardedRef, () => ({ + blur: () => closeDropdown(), + focus: () => openDropdown(), + isFocused: () => inputRef.current === document.activeElement, + })); + + const openDropdown = useCallback(() => { + onDropdownOpen(true); + setDropdownVisible(true); + setTimeout(() => inputRef.current?.focus(), 300); + }, [onDropdownOpen]); + + const closeDropdown = useCallback(() => { + onDropdownOpen(false); + setDropdownVisible(false); + }, [onDropdownOpen]); + + const onDropdownAction = useCallback(() => { + dropdownVisible ? closeDropdown() : openDropdown(); + }, [dropdownVisible, openDropdown, closeDropdown]); + + const selectChainAndCloseDropdown = useCallback( + (chain: Chain) => { + onSelectChain(chain); + onDropdownAction(); + }, + [onDropdownAction, onSelectChain], + ); + + // useEffect(() => { + // if (!selectedChain) { + // openDropdown(); + // } + // }, [selectedChain, openDropdown]); + + const inputActionButton = ( + + ); + + const inputVisible = !selectedChain; + + return ( + <> + } + centerComponent={ + + + + + {inputVisible ? ( + + + + ) : ( + + + {selectedChain.name} + + + )} + + + + + } + dropdownComponent={ + + } + dropdownVisible={dropdownVisible} + rightComponent={ + selectedChain ? ( + + {inputActionButton} + + ) : ( + inputActionButton + ) + } + /> + + ); + }, +); + +const ChainList = ({ + chains, + selectChainAndCloseDropdown, +}: { + chains: Chain[]; + selectChainAndCloseDropdown: (chain: Chain) => void; +}) => { + return ( + + {chains.map((chain) => ( + selectChainAndCloseDropdown(chain)}> + + + + {chain.name} + + + + ))} + + ); +}; diff --git a/src/entries/popup/pages/send/ToAddressInput.tsx b/src/entries/popup/pages/send/ToAddressInput.tsx index 2dd5d72a0f..200e5ab0ba 100644 --- a/src/entries/popup/pages/send/ToAddressInput.tsx +++ b/src/entries/popup/pages/send/ToAddressInput.tsx @@ -382,7 +382,7 @@ export const ToAddressInput = React.forwardRef( return ( <> void; @@ -103,6 +135,8 @@ interface ChildInputAPI { } export function Send() { + const { testnetMode } = useTestnetModeStore(); + const { currentAddress } = useCurrentAddressStore(); const [waitingForDevice, setWaitingForDevice] = useState(false); const [showReviewSheet, setShowReviewSheet] = useState(false); const [contactSaveAction, setSaveContactAction] = useState<{ @@ -111,6 +145,10 @@ export function Send() { }>({ show: false, action: 'save' }); const [toAddressDropdownOpen, setToAddressDropdownOpen] = useState(false); + const chains = testnetMode ? TESTNET_CHAINS : MAINNET_CHAINS; + + const [chainId, setChainId] = useState(); + const navigate = useRainbowNavigate(); const { currentAddress: address } = useCurrentAddressStore(); @@ -137,13 +175,35 @@ export function Send() { const { connectedToHardhat, connectedToHardhatOp } = useConnectedToHardhatStore(); + const clusterId = useCreateClusterId(currentAddress); + const virtualNodeRpcUrl = useVirtualNodeRpcUrl( + clusterId, + currentAddress, + testnetMode, + ); + const portfolio = usePortfolio(clusterId, virtualNodeRpcUrl); + const portfolioBalance = usePortfolioBalance(clusterId, virtualNodeRpcUrl); + + console.log('portfolio in send', portfolio); + console.log('portfolioBalance in send', portfolioBalance); + + const orbyAssets = useMemo( + () => + portfolio + ? convertFungibleTokensToParsedUserAssets( + portfolio.fungibleTokenBalances, + ) + : [], + [portfolio], + ); + const { asset, selectAssetAddressAndChain, assets, setSortMethod, sortMethod, - } = useSendAsset(); + } = useSendAsset({ assets: orbyAssets }); const unhiddenAssets = useMemo( () => assets.filter((asset) => !isHidden(asset)), @@ -180,7 +240,7 @@ export function Send() { const { currentCurrency, maxAssetBalanceParams, - chainId, + // chainId, data, fromAddress, toAddress, @@ -246,7 +306,8 @@ export function Send() { ); const openReviewSheet = useCallback(() => { - if (readyForReview) { + // if (readyForReview) { + if (true) { setShowReviewSheet(true); } else { controls.start({ @@ -348,6 +409,24 @@ export function Send() { ], ); + if (asset && portfolio) { + console.log(asset?.isNativeAsset); + console.log('portfolio here', portfolio); + console.log('portfolio balances here', portfolio?.fungibleTokenBalances); + const recipientAddress = asset.isNativeAsset + ? toAddress + : portfolio.fungibleTokenBalances + .find( + (fungibleToken) => + fungibleToken.standardizedTokenId === asset.address, + ) + .tokenBalancesOnChains.find( + (tokenBalances) => tokenBalances.token.chainId === '84532', // base sepolia + )?.token.address; + + console.log('recipientAddress', recipientAddress); + } + const handleSend = useCallback( async (callback?: () => void) => { if (!config.send_enabled) return; @@ -360,20 +439,33 @@ export function Send() { setWaitingForDevice(true); } resetSendValues(); - const result = await sendTransaction({ - from: fromAddress, - to: txToAddress, - value, - chainId: activeChainId, - data, + // const result = await sendTransaction({ + // from: fromAddress, + // to: txToAddress, + // value, + // chainId: activeChainId, + // data, + // }); + const { result } = await sendOrbyTransaction({ + virtualNodeRpcUrl: virtualNodeRpcUrl!, + clusterId: clusterId!, + standardizedTokenId: asset.address, // NOTE: we're using the address field as the standardizedTokenId + amount: convertAmountToRawAmount(assetAmount, asset.decimals), + recipient: { + address: toAddress, + chainId: `EIP155-${chainId}`, + }, }); + + console.log('orbyTxResult', result); + if (result && asset) { - const transaction: NewTransaction = buildPendingTransaction(result); - addNewTransaction({ - address: fromAddress, - chainId: activeChainId, - transaction, - }); + // const transaction: NewTransaction = buildPendingTransaction(result); + // addNewTransaction({ + // address: fromAddress, + // chainId: activeChainId, + // transaction, + // }); callback?.(); navigate(ROUTES.HOME, { state: { tab: 'activity' }, @@ -689,6 +781,23 @@ export function Send() { /> + + c.id === chainId)} + onSelectChain={(chain) => { + setChainId(chain.id); + console.log('chain', chain); + }} + onDropdownOpen={() => { + console.log('onDropdownOpen'); + }} + onClearSelection={() => { + setChainId(undefined); + }} + /> + + void; + orbySwap: () => void; }; export const SwapReviewSheet = ({ @@ -175,6 +176,7 @@ export const SwapReviewSheet = ({ quote, flashbotsEnabled, hideSwapReview, + orbySwap, }: SwapReviewSheetProps) => { if (!quote || !assetToBuy || !assetToSell || (quote as QuoteError)?.error) return null; @@ -187,6 +189,7 @@ export const SwapReviewSheet = ({ quote={quote as Quote | CrosschainQuote} flashbotsEnabled={flashbotsEnabled} hideSwapReview={hideSwapReview} + orbySwap={orbySwap} /> ); }; @@ -199,6 +202,7 @@ type SwapReviewSheetWithQuoteProps = { quote: Quote | CrosschainQuote; flashbotsEnabled: boolean; hideSwapReview: () => void; + orbySwap: () => void; }; const SwapReviewSheetWithQuote = ({ @@ -209,6 +213,7 @@ const SwapReviewSheetWithQuote = ({ quote, flashbotsEnabled, hideSwapReview, + orbySwap, }: SwapReviewSheetWithQuoteProps) => { const navigate = useRainbowNavigate(); @@ -265,34 +270,36 @@ const SwapReviewSheetWithQuote = ({ const closeMoreDetails = useCallback(() => setShowDetails(false), []); const handleSwap = useCallback(async () => { - if (!enoughNativeAssetBalanceForGas) { - alert( - i18n.t('send.button_label.insufficient_native_asset_for_gas', { - symbol: nativeAsset?.symbol, - }), - ); - return; - } + // if (!enoughNativeAssetBalanceForGas) { + // alert( + // i18n.t('send.button_label.insufficient_native_asset_for_gas', { + // symbol: nativeAsset?.symbol, + // }), + // ); + // return; + // } setSendingSwap(true); - const swapExecutedSuccessfully = await onSwap({ - assetToSell, - assetToBuy, - quote, - degenMode: false, - }); + await orbySwap(); + // const swapExecutedSuccessfully = await onSwap({ + // assetToSell, + // assetToBuy, + // quote, + // degenMode: false, + // }); setSendingSwap(false); - if (swapExecutedSuccessfully) { - navigate(ROUTES.HOME, { state: { tab: 'tokens' } }); - } + // if (swapExecutedSuccessfully) { + // navigate(ROUTES.HOME, { state: { tab: 'tokens' } }); + // } }, [ - assetToBuy, - assetToSell, - enoughNativeAssetBalanceForGas, - nativeAsset?.symbol, - navigate, - quote, + // assetToBuy, + // assetToSell, + // enoughNativeAssetBalanceForGas, + // nativeAsset?.symbol, + // navigate, + // quote, + orbySwap, ]); const goBack = useCallback(() => { diff --git a/src/entries/popup/pages/swap/SwapTokenInput/TokenDropdown/TokenToSellDropdown.tsx b/src/entries/popup/pages/swap/SwapTokenInput/TokenDropdown/TokenToSellDropdown.tsx index 432f77d1b3..2c47b0e260 100644 --- a/src/entries/popup/pages/swap/SwapTokenInput/TokenDropdown/TokenToSellDropdown.tsx +++ b/src/entries/popup/pages/swap/SwapTokenInput/TokenDropdown/TokenToSellDropdown.tsx @@ -161,7 +161,8 @@ export const TokenToSellDropdown = ({ y: start, }} > - + {/* */} + ); })} diff --git a/src/entries/popup/pages/swap/SwapTokenInput/TokenRow/TokenToSellRow.tsx b/src/entries/popup/pages/swap/SwapTokenInput/TokenRow/TokenToSellRow.tsx index c9f0b0be5d..b69d7a27de 100644 --- a/src/entries/popup/pages/swap/SwapTokenInput/TokenRow/TokenToSellRow.tsx +++ b/src/entries/popup/pages/swap/SwapTokenInput/TokenRow/TokenToSellRow.tsx @@ -26,8 +26,8 @@ export type TokenToSellRowProps = { uniqueId: UniqueId; }; -export function TokenToSellRow({ uniqueId }: TokenToSellRowProps) { - const { data: asset } = useUserAsset(uniqueId); +export function TokenToSellRow({ asset }) { + // const { data: asset } = useUserAsset(uniqueId); const { hideAssetBalances } = useHideAssetBalancesStore(); const { currentCurrency } = useCurrentCurrencyStore(); @@ -102,7 +102,7 @@ export function TokenToSellRow({ uniqueId }: TokenToSellRowProps) { @@ -393,6 +409,27 @@ export function Swap({ bridge = false }: { bridge?: boolean }) { useState(false); const { isFirefox } = useBrowser(); + const { testnetMode } = useTestnetModeStore(); + const { currentAddress } = useCurrentAddressStore(); + + const clusterId = useCreateClusterId(currentAddress); + const virtualNodeRpcUrl = useVirtualNodeRpcUrl( + clusterId, + currentAddress, + testnetMode, + ); + const portfolio = usePortfolio(clusterId, virtualNodeRpcUrl); + const portfolioBalance = usePortfolioBalance(clusterId, virtualNodeRpcUrl); + + console.log('portfolio', portfolio); + console.log('portfolioBalance', portfolioBalance); + + const assetsToSell = portfolio + ? convertFungibleTokensToParsedUserAssets(portfolio.fungibleTokenBalances) + : []; + + console.log('assetsToSell', assetsToSell); + // translate based on the context, bridge or swap const translationContext = { Action: i18n.t(`swap._actions.${bridge ? 'Bridge' : 'Swap'}`), @@ -431,7 +468,7 @@ export function Swap({ bridge = false }: { bridge?: boolean }) { }, []); const { - assetsToSell, + // assetsToSell, assetToSellFilter, assetsToBuy, assetToBuyFilter, @@ -544,6 +581,8 @@ export function Swap({ bridge = false }: { bridge?: boolean }) { : slippage, }); + console.log('quote', quote); + const { assetToSellNativeDisplay, assetToBuyNativeDisplay } = useSwapNativeAmounts({ assetToBuy, @@ -711,6 +750,77 @@ export function Swap({ bridge = false }: { bridge?: boolean }) { const assetToBuyAccentColor = assetToBuy?.colors?.primary || assetToBuy?.colors?.fallback; + const [operationSet, setOperationSet] = useState([]); + + useEffect(() => { + const getSwapDetails = async () => { + console.log('in here'); + const outputStandardizedTokenId = await getStandardizedTokenId({ + virtualNodeRpcUrl, + chainId: `EIP155-${assetToBuy?.chainId}`, + tokenAddress: assetToBuy?.address as Address, + }); + + console.log('standardizedTokenId', outputStandardizedTokenId); + + if (outputStandardizedTokenId) { + const operationsToSwap = await getOperationsToSwap({ + virtualNodeRpcUrl, + clusterId, + swapType: 'EXACT_INPUT', + input: { + standardizedTokenId: assetToSell.address, + amount: Number( + convertAmountToRawAmount(assetToSellValue, assetToSell.decimals), + ), + }, + output: { + standardizedTokenId: outputStandardizedTokenId, + // amount: Number( + // convertAmountToRawAmount(assetToBuyValue, assetToBuy.decimals), + // ), + }, + }); + + console.log('operationsToSwap', operationsToSwap); + setOperationSet(operationsToSwap); + } + }; + + console.log('assetToBuy', assetToBuy); + console.log('assetToSell', assetToSell); + console.log('virtualNodeRpcUrl', virtualNodeRpcUrl); + if ( + assetToBuy && + assetToSell && + virtualNodeRpcUrl && + assetToBuyValue && + assetToSellValue + ) { + getSwapDetails(); + } + }, [ + assetToBuy, + assetToSell, + virtualNodeRpcUrl, + assetToBuyValue, + assetToSellValue, + ]); + + console.log('assetToSell', assetToSell); + + const orbySwap = useCallback(async () => { + console.log('orbySwap here'); + const signedOperationsResponse = await signOperationSet(operationSet); + const response = await sendSignedOperations({ + clusterId, + signedOperations: signedOperationsResponse, + virtualNodeRpcUrl, + }); + + return response; + }, [clusterId, operationSet, virtualNodeRpcUrl]); + return ( - - - ), - buttonAction: () => null, - status: 'loading', - }; - } + // if (isLoading) { + // return { + // buttonColor: 'surfaceSecondary', + // buttonLabelColor: 'labelQuaternary', + // buttonDisabled: true, + // buttonLabel: t('swap.actions.loading'), + // buttonIcon: ( + // + // + // + // ), + // buttonAction: () => null, + // status: 'loading', + // }; + // } - if (!quote) { - return { - buttonColor: 'surfaceSecondary', - buttonDisabled: true, - buttonLabel: t('swap.actions.enter_an_amount'), - buttonLabelColor: 'labelQuaternary', - buttonIcon: null, - buttonAction: () => null, - status: 'error', - }; - } + // if (!quote) { + // return { + // buttonColor: 'surfaceSecondary', + // buttonDisabled: true, + // buttonLabel: t('swap.actions.enter_an_amount'), + // buttonLabelColor: 'labelQuaternary', + // buttonIcon: null, + // buttonAction: () => null, + // status: 'error', + // }; + // } - if (!(quote as QuoteError).error) { - if (!enoughAssetsForSwap) { - return { - buttonColor: 'fillSecondary', - buttonDisabled: true, - buttonLabel: validationButtonLabel, - buttonLabelColor: 'label', - buttonIcon: null, - buttonAction: () => null, - status: 'ready', - }; - } + // if (!(quote as QuoteError).error) { + // if (!enoughAssetsForSwap) { + // return { + // buttonColor: 'fillSecondary', + // buttonDisabled: true, + // buttonLabel: validationButtonLabel, + // buttonLabelColor: 'label', + // buttonIcon: null, + // buttonAction: () => null, + // status: 'ready', + // }; + // } - if (isDegenModeEnabled) { - if (status === 'degen_swapping') { - return { - buttonColor: 'surfaceSecondary', - buttonDisabled: true, - buttonLabel: isHardwareWallet - ? t('swap.actions.waiting_signature') - : t('swap.actions.swapping'), - buttonLabelColor: 'labelQuaternary', - buttonIcon: ( - - - - ), - buttonAction: () => null, - status: 'ready', - }; - } + // if (isDegenModeEnabled) { + // if (status === 'degen_swapping') { + // return { + // buttonColor: 'surfaceSecondary', + // buttonDisabled: true, + // buttonLabel: isHardwareWallet + // ? t('swap.actions.waiting_signature') + // : t('swap.actions.swapping'), + // buttonLabelColor: 'labelQuaternary', + // buttonIcon: ( + // + // + // + // ), + // buttonAction: () => null, + // status: 'ready', + // }; + // } - return { - buttonColor: 'accent', - buttonDisabled: false, - buttonLabel: t('swap.actions.swap'), - buttonLabelColor: 'label', - buttonIcon: null, - buttonAction: async () => { - setStatus('degen_swapping'); - const swapExecutedSuccessfully = await onSwap({ - quote, - assetToSell, - assetToBuy, - degenMode: true, - }); - setStatus('idle'); - if (swapExecutedSuccessfully) { - navigate(ROUTES.HOME, { state: { tab: 'activity' } }); - } - }, - status: 'ready', - }; - } + // return { + // buttonColor: 'accent', + // buttonDisabled: false, + // buttonLabel: t('swap.actions.swap'), + // buttonLabelColor: 'label', + // buttonIcon: null, + // buttonAction: async () => { + // setStatus('degen_swapping'); + // const swapExecutedSuccessfully = await onSwap({ + // quote, + // assetToSell, + // assetToBuy, + // degenMode: true, + // }); + // setStatus('idle'); + // if (swapExecutedSuccessfully) { + // navigate(ROUTES.HOME, { state: { tab: 'activity' } }); + // } + // }, + // status: 'ready', + // }; + // } - return { - buttonColor: 'accent', - buttonDisabled: false, - buttonLabel: t('swap.actions.review'), - buttonLabelColor: 'label', - buttonIcon: ( - - ), - buttonAction: timeEstimate?.isLongWait - ? () => - showExplainerSheet({ - show: true, - header: { - icon: ( + return { + buttonColor: 'accent', + buttonDisabled: false, + buttonLabel: t('swap.actions.review'), + buttonLabelColor: 'label', + buttonIcon: ( + + ), + buttonAction: timeEstimate?.isLongWait + ? () => + showExplainerSheet({ + show: true, + header: { + icon: ( + - - - - - - - - - - + - ), - }, - title: t('swap.explainers.long_wait.title'), - description: [t('swap.explainers.long_wait.description')], - actionButton: { - label: t('swap.explainers.long_wait.action_label'), - variant: 'tinted', - labelColor: 'blue', - action: () => { - hideExplainerSheet(); - showSwapReviewSheet(); - }, + + + + + + + + + ), + }, + title: t('swap.explainers.long_wait.title'), + description: [t('swap.explainers.long_wait.description')], + actionButton: { + label: t('swap.explainers.long_wait.action_label'), + variant: 'tinted', + labelColor: 'blue', + action: () => { + hideExplainerSheet(); + showSwapReviewSheet(); }, - testId: 'swap-long-wait', - }) - : () => { - showSwapReviewSheet(); - }, - status: 'ready', - }; - } + }, + testId: 'swap-long-wait', + }) + : () => { + showSwapReviewSheet(); + }, + status: 'ready', + }; + // }; switch ((quote as QuoteError).error_code) { case 502: From e413d3e77fa1e9c1eeb8f4f3fa79ca6068f6fcd7 Mon Sep 17 00:00:00 2001 From: imti Date: Wed, 13 Nov 2024 16:18:07 -0800 Subject: [PATCH 06/15] feat: showing operations for swap and send --- src/core/utils/orb.ts | 2 +- src/entries/popup/handlers/wallet.ts | 20 +---- .../popup/hooks/send/useSendValidations.ts | 22 +++--- src/entries/popup/pages/send/ChainInput.tsx | 30 ++++++-- src/entries/popup/pages/send/ReviewSheet.tsx | 57 +++++++++++++- src/entries/popup/pages/send/index.tsx | 39 ++++++++-- .../swap/SwapReviewSheet/SwapReviewSheet.tsx | 74 ++++++++++++++++++- src/entries/popup/pages/swap/index.tsx | 4 +- 8 files changed, 200 insertions(+), 48 deletions(-) diff --git a/src/core/utils/orb.ts b/src/core/utils/orb.ts index 94a714fee5..acb5836bf0 100644 --- a/src/core/utils/orb.ts +++ b/src/core/utils/orb.ts @@ -386,7 +386,7 @@ export const getOperationsToTransferToken = async ({ ], }), }); - const result = await response.json(); + const { result } = await response.json(); console.log('operations to transfer token', result); return result; }; diff --git a/src/entries/popup/handlers/wallet.ts b/src/entries/popup/handlers/wallet.ts index 879ba1af66..9b33df0a9a 100644 --- a/src/entries/popup/handlers/wallet.ts +++ b/src/entries/popup/handlers/wallet.ts @@ -98,28 +98,14 @@ export const signTransactionFromHW = async ( export const sendOrbyTransaction = async ({ clusterId, - standardizedTokenId, - amount, - recipient, virtualNodeRpcUrl, + operationSet, }: { clusterId: string; - standardizedTokenId: string; - amount: string; - recipient: { address: string; chainId: string }; virtualNodeRpcUrl: string; + operationSet: any; }): Promise => { - const { result: operationSet } = await getOperationsToTransferToken({ - virtualNodeRpcUrl, - clusterId, - standardizedTokenId, - amount, - recipient, - }); - - console.log('operationSet', operationSet); - - // // NOTE: i'm not handling hardware wallets here, but we can add that later + // NOTE: i'm not handling hardware wallets here, but we can add that later const transactionResponse = await walletAction( 'send_orby_transaction', { operationSet, virtualNodeRpcUrl, clusterId }, diff --git a/src/entries/popup/hooks/send/useSendValidations.ts b/src/entries/popup/hooks/send/useSendValidations.ts index 16632ac167..e1a77f0db8 100644 --- a/src/entries/popup/hooks/send/useSendValidations.ts +++ b/src/entries/popup/hooks/send/useSendValidations.ts @@ -132,15 +132,15 @@ export const useSendValidations = ({ if (toAddressOrName === '') { return i18n.t('send.button_label.enter_address'); } - if (!enoughAssetBalance) - return i18n.t('send.button_label.insufficient_asset', { - symbol: asset?.symbol, - }); - if (!enoughNativeAssetForGas) - return i18n.t('send.button_label.insufficient_native_asset_for_gas', { - symbol: getChain({ chainId: asset?.chainId || ChainId.mainnet }) - .nativeCurrency.symbol, - }); + // if (!enoughAssetBalance) + // return i18n.t('send.button_label.insufficient_asset', { + // symbol: asset?.symbol, + // }); + // if (!enoughNativeAssetForGas) + // return i18n.t('send.button_label.insufficient_native_asset_for_gas', { + // symbol: getChain({ chainId: asset?.chainId || ChainId.mainnet }) + // .nativeCurrency.symbol, + // }); return i18n.t('send.button_label.review'); }, [ asset?.chainId, @@ -159,9 +159,7 @@ export const useSendValidations = ({ selectedGas?.gasFee?.amount && isValidToAddress && toAddressOrName !== '' && - (assetAmount || !!nft) && - enoughAssetBalance && - enoughNativeAssetForGas, + (assetAmount || !!nft), [ assetAmount, enoughAssetBalance, diff --git a/src/entries/popup/pages/send/ChainInput.tsx b/src/entries/popup/pages/send/ChainInput.tsx index 61110c6ed6..a0b37ebff7 100644 --- a/src/entries/popup/pages/send/ChainInput.tsx +++ b/src/entries/popup/pages/send/ChainInput.tsx @@ -96,7 +96,15 @@ export const ChainInput = React.forwardRef( zIndex={2} dropdownHeight={300} testId="chain-input" - leftComponent={} + leftComponent={ + + } centerComponent={ @@ -173,12 +181,22 @@ const ChainList = ({ selectChainAndCloseDropdown: (chain: Chain) => void; }) => { return ( - + {chains.map((chain) => ( - selectChainAndCloseDropdown(chain)}> - - - + selectChainAndCloseDropdown(chain)} + paddingBottom="8px" + > + + + {chain.name} diff --git a/src/entries/popup/pages/send/ReviewSheet.tsx b/src/entries/popup/pages/send/ReviewSheet.tsx index 63d1a7de59..9ecebd3bdf 100644 --- a/src/entries/popup/pages/send/ReviewSheet.tsx +++ b/src/entries/popup/pages/send/ReviewSheet.tsx @@ -6,7 +6,7 @@ import React, { useRef, useState, } from 'react'; -import { Address } from 'viem'; +import { Address, formatUnits } from 'viem'; import { i18n } from '~/core/languages'; import { chainsLabel } from '~/core/references/chains'; @@ -16,8 +16,10 @@ import { UniqueAsset } from '~/core/types/nfts'; import { truncateAddress } from '~/core/utils/address'; import { getBlockExplorerHostForChain, + getChain, isCustomChain, } from '~/core/utils/chains'; +import { handleSignificantDecimalsWithThreshold } from '~/core/utils/numbers'; import { isLowerCaseMatch } from '~/core/utils/strings'; import { getExplorerUrl, goToNewTab } from '~/core/utils/tabs'; import { wagmiConfig } from '~/core/wagmi'; @@ -253,6 +255,7 @@ export const ReviewSheet = ({ onCancel, onSend, onSaveContactAction, + operationSet, }: { show: boolean; toAddress: Address; @@ -269,6 +272,7 @@ export const ReviewSheet = ({ action: ContactAction; }> >; + operationSet: any; }) => { const { visibleOwnedWallets } = useWallets(); const [notSendingOnEthereumChecks, setNotSendingOnEthereumChecks] = @@ -354,6 +358,28 @@ export const ReviewSheet = ({ } }, [show]); + console.log('operationSet', operationSet); + const operations = + operationSet && operationSet.intents + ? operationSet.intents + .map((intent) => intent.intentOperations) + .flat() + ?.concat(operationSet.primaryOperation) + .filter( + (value) => + value !== undefined && + value !== null && + value.type === 'SUBMIT_INTENT', + ) + : []; + console.log('operations', operations); + const inputStates = operations + ? operations.flatMap( + (operation) => operation.inputState.fungibleTokenAmounts, + ) + : []; + console.log('inputStates', inputStates); + return ( <> @@ -542,6 +568,35 @@ export const ReviewSheet = ({ + + {inputStates.map((input, i) => ( + + + + + + Use{' '} + {handleSignificantDecimalsWithThreshold( + formatUnits( + input.amount, + input.token.currency.decimals, + ), + 2, + )}{' '} + {input.token.currency.asset.symbol} from{' '} + {getChain({ chainId: Number(input.token.chainId) }).name} + + + + + ))} + + {notSendingOnEthereum && !isToWalletOwner && ( diff --git a/src/entries/popup/pages/send/index.tsx b/src/entries/popup/pages/send/index.tsx index d5df0650e3..d149d4c098 100644 --- a/src/entries/popup/pages/send/index.tsx +++ b/src/entries/popup/pages/send/index.tsx @@ -109,6 +109,7 @@ import { usePortfolioBalance, useVirtualNodeRpcUrl, convertFungibleTokensToParsedUserAssets, + getOperationsToTransferToken, } from '~/core/utils/orb'; import { convertAmountToRawAmount } from '~/core/utils/numbers'; @@ -266,6 +267,8 @@ export function Send() { toAddressOrName, }); + console.log('readyForReview', readyForReview); + const controls = useAnimationControls(); const transactionRequestForGas: TransactionRequest = useMemo(() => { if (nft) { @@ -427,12 +430,38 @@ export function Send() { console.log('recipientAddress', recipientAddress); } + const [operationSet, setOperationSet] = useState(null); + + useEffect(() => { + const getSendDetails = async () => { + console.log('in here'); + + const operationsToSend = await getOperationsToTransferToken({ + virtualNodeRpcUrl: virtualNodeRpcUrl!, + clusterId: clusterId!, + standardizedTokenId: asset!.address, // NOTE: we're using the address field as the standardizedTokenId + amount: convertAmountToRawAmount(assetAmount, asset!.decimals), + recipient: { + address: toAddress!, + chainId: `EIP155-${chainId}`, + }, + }); + + console.log('operationsToSend', operationsToSend); + setOperationSet(operationsToSend); + }; + + if (clusterId && virtualNodeRpcUrl && asset && assetAmount && toAddress) { + getSendDetails(); + } + }, [asset, assetAmount, clusterId, toAddress, virtualNodeRpcUrl, chainId]); + const handleSend = useCallback( async (callback?: () => void) => { if (!config.send_enabled) return; try { - if (asset) { + if (asset && operationSet) { const { type } = await getWallet(fromAddress); // Change the label while we wait for confirmation if (type === 'HardwareWalletKeychain') { @@ -449,12 +478,7 @@ export function Send() { const { result } = await sendOrbyTransaction({ virtualNodeRpcUrl: virtualNodeRpcUrl!, clusterId: clusterId!, - standardizedTokenId: asset.address, // NOTE: we're using the address field as the standardizedTokenId - amount: convertAmountToRawAmount(assetAmount, asset.decimals), - recipient: { - address: toAddress, - chainId: `EIP155-${chainId}`, - }, + operationSet, }); console.log('orbyTxResult', result); @@ -717,6 +741,7 @@ export function Send() { /> void; orbySwap: () => void; + operationSet: any; }; export const SwapReviewSheet = ({ @@ -177,8 +182,15 @@ export const SwapReviewSheet = ({ flashbotsEnabled, hideSwapReview, orbySwap, + operationSet, }: SwapReviewSheetProps) => { - if (!quote || !assetToBuy || !assetToSell || (quote as QuoteError)?.error) + if ( + !quote || + !assetToBuy || + !assetToSell || + (quote as QuoteError)?.error || + !operationSet + ) return null; return ( ); }; @@ -203,6 +216,7 @@ type SwapReviewSheetWithQuoteProps = { flashbotsEnabled: boolean; hideSwapReview: () => void; orbySwap: () => void; + operationSet: any; }; const SwapReviewSheetWithQuote = ({ @@ -214,6 +228,7 @@ const SwapReviewSheetWithQuote = ({ flashbotsEnabled, hideSwapReview, orbySwap, + operationSet, }: SwapReviewSheetWithQuoteProps) => { const navigate = useRainbowNavigate(); @@ -269,6 +284,28 @@ const SwapReviewSheetWithQuote = ({ const openMoreDetails = useCallback(() => setShowDetails(true), []); const closeMoreDetails = useCallback(() => setShowDetails(false), []); + console.log('operationSet', operationSet); + const operations = + operationSet && operationSet.intents + ? operationSet.intents + .map((intent) => intent.intentOperations) + .flat() + ?.concat(operationSet.primaryOperation) + .filter( + (value) => + value !== undefined && + value !== null && + value.type === 'SUBMIT_INTENT', + ) + : []; + console.log('operations', operations); + const inputStates = operations + ? operations.flatMap( + (operation) => operation.inputState.fungibleTokenAmounts, + ) + : []; + console.log('inputStates', inputStates); + const handleSwap = useCallback(async () => { // if (!enoughNativeAssetBalanceForGas) { // alert( @@ -462,6 +499,37 @@ const SwapReviewSheetWithQuote = ({ paddingBottom="20px" > + + {inputStates.map((input, i) => ( + + + + + + Use{' '} + {handleSignificantDecimalsWithThreshold( + formatUnits( + input.amount, + input.token.currency.decimals, + ), + 2, + )}{' '} + {input.token.currency.asset.symbol} from{' '} + { + getChain({ chainId: Number(input.token.chainId) }) + .name + } + + + + + ))} + diff --git a/src/entries/popup/pages/home/index.tsx b/src/entries/popup/pages/home/index.tsx index e1e0ce6ea3..38dd8dd1a1 100644 --- a/src/entries/popup/pages/home/index.tsx +++ b/src/entries/popup/pages/home/index.tsx @@ -238,7 +238,7 @@ export const Home = memo(function Home() { - + {/* */} {currentHomeSheet} From a4e8f1f26d9703d0386fee2170e332bae595ce98 Mon Sep 17 00:00:00 2001 From: imti Date: Thu, 14 Nov 2024 13:48:38 -0800 Subject: [PATCH 09/15] chore: show token images --- src/core/utils/orb.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/utils/orb.ts b/src/core/utils/orb.ts index acb5836bf0..fa8e5c20b5 100644 --- a/src/core/utils/orb.ts +++ b/src/core/utils/orb.ts @@ -53,6 +53,7 @@ export const convertFungibleTokenToParsedUserAsset = ( display: 'foo', }, }, + icon_url: fungibleToken.total.currency.logoUrl, }; }; From f7b8f0a49879f057db7af606e256be54bfa3a6bd Mon Sep 17 00:00:00 2001 From: felimadu Date: Wed, 11 Dec 2024 23:46:00 -0500 Subject: [PATCH 10/15] playground --- .gitignore | 4 +- README.md | 10 +- lavamoat/build-webpack/policy.json | 38 +- package.json | 6 +- src/core/keychain/IKeychain.ts | 3 + src/core/keychain/KeychainManager.ts | 12 +- src/core/keychain/RainbowSigner.ts | 34 +- src/core/keychain/index.ts | 24 +- .../keychainTypes/hardwareWalletKeychain.ts | 5 + src/core/keychain/keychainTypes/hdKeychain.ts | 9 + .../keychain/keychainTypes/keyPairKeychain.ts | 7 + .../keychainTypes/readOnlyKeychain.ts | 5 + src/core/providers/proxy.ts | 24 +- src/core/raps/utils.ts | 1 + src/core/references/assets.ts | 10 + .../resources/transactions/transaction.ts | 35 +- src/core/state/rainbowChains/index.ts | 32 + src/core/types/assets.ts | 4 + src/core/utils/numbers.ts | 7 +- src/core/utils/orb.ts | 569 +++-------- src/core/wagmi/index.ts | 28 +- .../background/handlers/handleWallets.ts | 13 - src/entries/background/index.ts | 6 + src/entries/popup/App.tsx | 65 +- .../popup/components/CoinIcon/CoinIcon.tsx | 60 +- .../popup/components/CoinRow/CoinRow.tsx | 20 +- src/entries/popup/components/Tabs/TabBar.tsx | 24 +- .../TransactionFee/GasTokenMenu.tsx | 202 ++++ .../TransactionFee/TransactionFee.tsx | 124 ++- src/entries/popup/handlers/wallet.ts | 24 - .../useApproveAppRequestValidations.ts | 3 - src/entries/popup/hooks/send/useSendAsset.ts | 128 ++- .../popup/hooks/send/useSendValidations.ts | 79 +- src/entries/popup/hooks/swap/useSwapAssets.ts | 54 +- .../popup/hooks/swap/useSwapValidations.ts | 44 +- src/entries/popup/hooks/useAppSession.ts | 47 +- src/entries/popup/hooks/useAuth.tsx | 4 + .../popup/hooks/useConnectAppSessions.ts | 110 +++ src/entries/popup/hooks/useGas.ts | 45 + .../popup/hooks/useInfiniteTransactionList.ts | 99 +- .../popup/hooks/useUserAssetsBalance.ts | 21 +- .../pages/home/Activity/ActivitiesList.tsx | 6 +- src/entries/popup/pages/home/Header.tsx | 4 +- src/entries/popup/pages/home/TabHeader.tsx | 8 +- src/entries/popup/pages/home/Tokens.tsx | 298 +++--- src/entries/popup/pages/home/index.tsx | 42 +- .../pages/messages/ApproveAppRequest.tsx | 1 + .../SendTransactionActions.tsx | 2 - .../SendTransaction/SendTransactionsInfo.tsx | 42 +- .../pages/messages/SendTransaction/index.tsx | 280 +++--- .../messages/SignMessage/SignMessageInfo.tsx | 41 +- .../pages/messages/SignMessage/index.tsx | 186 ++-- .../popup/pages/messages/useHasEnoughGas.ts | 1 - src/entries/popup/pages/send/ChainInput.tsx | 72 +- src/entries/popup/pages/send/ReviewSheet.tsx | 108 +-- .../popup/pages/send/SendTokenInput.tsx | 24 +- src/entries/popup/pages/send/index.tsx | 437 +++++---- .../swap/SwapReviewSheet/SwapReviewSheet.tsx | 237 +++-- src/entries/popup/pages/swap/index.tsx | 236 +++-- .../popup/pages/swap/useSwapButton.tsx | 353 ++++--- static/allowlist.json | 3 + static/json/languages/en_US.json | 2 + static/manifest.json | 32 +- tsconfig.json | 14 +- yarn.lock | 913 +++++++++++++++++- 65 files changed, 3308 insertions(+), 2073 deletions(-) create mode 100644 src/entries/popup/components/TransactionFee/GasTokenMenu.tsx create mode 100644 src/entries/popup/hooks/useConnectAppSessions.ts diff --git a/.gitignore b/.gitignore index 26143e8b7f..e21ba3d6e3 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,6 @@ rainbowbx.xpi .idea/** -static/data \ No newline at end of file +static/data + +.tool-versions \ No newline at end of file diff --git a/README.md b/README.md index dc82825506..124a2c0c7e 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,11 @@ Built for speed. Built for power. Built for you. Rainbow is a fun, simple, and secure Ethereum wallet that makes managing your assets a joy. Great for newcomers and power users alike, Rainbow allows you to be in total control of your crypto. You own your assets directly thanks to the power of cryptography and the Ethereum blockchain, and Rainbow makes managing all of your wallets and keys a breeze. ### Features + - Auto-discovers tokens and assets - Supports Layer 2 chains like Arbitrum, Optimism, Base, Polygon, Avalanche, & Zora right out-of-the-box - Built-in Send, Bridge, and Swap to power all of your DeFi needs -- Keyboard shortcuts for pros to switch wallets like 1 2 3 +- Keyboard shortcuts for pros to switch wallets like 1 2 3 - Search and navigate your wallets with ⌘K or Ctrl-K for the Magic Menu - Watch wallets and interact with dApps in Impersonation mode @@ -37,16 +38,19 @@ Safari is [coming soon](https://rainbowdotme.typeform.com/to/iT919yeN) ## Security architecture Rainbow is one of the first extensions to use the new Manifest v3 extension standard. This comes with some important security benefits: + - **Runtime isolation**: Remotely hosted code is no longer allowed; an extension can only execute JavaScript that is included within its package. - **Network firewall**: Content security policy (CSP) allows us to define which domains the extension can interact with, similar to a "firewall". This means that if at any point the extension is compromised, it will not be able to communicate with any domain that is not explicitly allowed in the CSP, preventing any kind of data exfiltration. The v3 standard also improves the overall reliability of Rainbow: + - **Performance**: lighter CPU and memory footprint - the extension consumes resources only when active thanks to service workers. You can compare how quickly the extension loads compared to others. - **Reliable hardware wallets**: The extension can directly access web technologies like WebUSB and HID that make the integration with hardware wallets much simpler and more secure. Additionally, we're using some well known tools engineered by the MetaMask team: - - [@lavamoat/allow-scripts](https://github.com/LavaMoat/LavaMoat/tree/main/packages/allow-scripts) and [@lavamoat/preinstall-always-fail](https://github.com/LavaMoat/LavaMoat/tree/main/packages/preinstall-always-fail) are used to disable or allow dependency lifecycle scripts (eg. "postinstall"), a common build-time vulnerability - - [lavamoat](https://github.com/LavaMoat/lavamoat) aka LavaMoat Node is a NodeJS runtime that protects our build process, which aims to reduce the risk of malicious code in the dependency graph, commonly known as "software supply chain attacks" + +- [@lavamoat/allow-scripts](https://github.com/LavaMoat/LavaMoat/tree/main/packages/allow-scripts) and [@lavamoat/preinstall-always-fail](https://github.com/LavaMoat/LavaMoat/tree/main/packages/preinstall-always-fail) are used to disable or allow dependency lifecycle scripts (eg. "postinstall"), a common build-time vulnerability +- [lavamoat](https://github.com/LavaMoat/lavamoat) aka LavaMoat Node is a NodeJS runtime that protects our build process, which aims to reduce the risk of malicious code in the dependency graph, commonly known as "software supply chain attacks" - [browser-passworder](https://github.com/MetaMask/browser-passworder) is our shared encryption library used to encrypt a user's keychain while at rest > NOTE: We don't rely on LavaMoat at runtime because of the performance overhead and the benefits we already receive from Manifest v3, but we may consider it in the future. diff --git a/lavamoat/build-webpack/policy.json b/lavamoat/build-webpack/policy.json index 19440ec708..b58bedc4ed 100644 --- a/lavamoat/build-webpack/policy.json +++ b/lavamoat/build-webpack/policy.json @@ -1,5 +1,20 @@ { "resources": { + "@orb-labs/orby-react>tailwindcss>sucrase>@jridgewell/gen-mapping": { + "globals": { + "define": true + }, + "packages": { + "@orb-labs/orby-react>tailwindcss>sucrase>@jridgewell/gen-mapping>@jridgewell/set-array": true, + "jest>@jest/core>@jest/reporters>@jridgewell/trace-mapping": true, + "jest>@jest/core>@jest/reporters>@jridgewell/trace-mapping>@jridgewell/sourcemap-codec": true + } + }, + "@orb-labs/orby-react>tailwindcss>sucrase>@jridgewell/gen-mapping>@jridgewell/set-array": { + "globals": { + "define": true + } + }, "@testing-library/react>@testing-library/dom>@babel/code-frame": { "globals": { "console.warn": true, @@ -559,8 +574,8 @@ "define": true }, "packages": { - "jest>@jest/core>@jest/reporters>@jridgewell/trace-mapping>@jridgewell/sourcemap-codec": true, - "jest>@jest/core>jest-snapshot>@babel/generator>@jridgewell/gen-mapping>@jridgewell/set-array": true + "@orb-labs/orby-react>tailwindcss>sucrase>@jridgewell/gen-mapping>@jridgewell/set-array": true, + "jest>@jest/core>@jest/reporters>@jridgewell/trace-mapping>@jridgewell/sourcemap-codec": true } }, "eslint-config-rainbow>eslint-import-resolver-babel-module>@babel/core>@babel/helper-compilation-targets": { @@ -1094,26 +1109,11 @@ "console.error": true }, "packages": { - "jest>@jest/core>jest-snapshot>@babel/generator>@jridgewell/gen-mapping": true, + "@orb-labs/orby-react>tailwindcss>sucrase>@jridgewell/gen-mapping": true, "jest>@jest/core>jest-snapshot>@babel/generator>jsesc": true, "jest>@jest/core>jest-snapshot>@babel/types": true } }, - "jest>@jest/core>jest-snapshot>@babel/generator>@jridgewell/gen-mapping": { - "globals": { - "define": true - }, - "packages": { - "jest>@jest/core>@jest/reporters>@jridgewell/trace-mapping": true, - "jest>@jest/core>@jest/reporters>@jridgewell/trace-mapping>@jridgewell/sourcemap-codec": true, - "jest>@jest/core>jest-snapshot>@babel/generator>@jridgewell/gen-mapping>@jridgewell/set-array": true - } - }, - "jest>@jest/core>jest-snapshot>@babel/generator>@jridgewell/gen-mapping>@jridgewell/set-array": { - "globals": { - "define": true - } - }, "jest>@jest/core>jest-snapshot>@babel/generator>jsesc": { "globals": { "Buffer.isBuffer": true @@ -1173,7 +1173,7 @@ "console.warn": true }, "packages": { - "jest>@jest/core>jest-snapshot>@babel/generator>@jridgewell/gen-mapping": true, + "@orb-labs/orby-react>tailwindcss>sucrase>@jridgewell/gen-mapping": true, "jest>@jest/core>jest-snapshot>@babel/generator>jsesc": true, "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/generator>@jridgewell/trace-mapping": true, "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/types": true diff --git a/package.json b/package.json index 04ce2c5123..dc44cd6b31 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "update-manifest": "node scripts/update-manifest.js", "update-manifest:prod": "node scripts/update-manifest.js prod", "// Generates a build using webpack secured by Lavamoat": "", - "build": "yarn devmode:off && yarn build:webpack", + "build": "yarn devmode:off && yarn build:webpack && yarn copy-webpage", "// Generates modules via Webpack (without LavaMoat protected modules)": "", "build:webpack": "lavamoat ./scripts/webpack.js --policy ./lavamoat/build-webpack/policy.json --override ./lavamoat/build-webpack/policy-override.json", "// Generates LavaMoat secured modules from the Webpack build": "", @@ -22,6 +22,7 @@ "playground:ds": "PLAYGROUND=ds webpack --config ./webpack.config.dev.js --watch", "lint": "eslint --cache --max-warnings 0", "typecheck": "tsc --noEmit", + "copy-webpage": "cp ./node_modules/@orb-labs/orby-core-mini/dist/webpage.min.js ./build/webpage.js", "// Fetch backend networks": "", "fetch:networks": "node scripts/networks.js", "// Design System scripts": "", @@ -100,6 +101,7 @@ "@ledgerhq/hw-transport-webhid": "6.29.3", "@metamask/browser-passworder": "4.1.0", "@metamask/eth-sig-util": "7.0.1", + "@orb-labs/orby-react": "0.0.22", "@radix-ui/react-accordion": "1.1.2", "@radix-ui/react-context-menu": "2.1.1", "@radix-ui/react-dropdown-menu": "2.0.1", @@ -337,4 +339,4 @@ "wagmi>@wagmi/connectors>@metamask/sdk>eciesjs>secp256k1": false } } -} \ No newline at end of file +} diff --git a/src/core/keychain/IKeychain.ts b/src/core/keychain/IKeychain.ts index 32723e6b16..c2d77b2a8a 100644 --- a/src/core/keychain/IKeychain.ts +++ b/src/core/keychain/IKeychain.ts @@ -3,6 +3,8 @@ import { Mnemonic } from '@ethersproject/hdnode'; import { Wallet } from '@ethersproject/wallet'; import { Address } from 'viem'; +import { RainbowSigner } from './RainbowSigner'; + export type PrivateKey = string; export type TWallet = Omit & { @@ -18,6 +20,7 @@ export interface IKeychain { addAccountAtIndex(index: number, address: Address): Promise
; getAccounts(): Promise>; getSigner(address: Address): Signer; + getRainbowSigner(): RainbowSigner; exportAccount(address: Address): Promise; exportKeychain(address: Address): Promise; removeAccount(address: Address): Promise; diff --git a/src/core/keychain/KeychainManager.ts b/src/core/keychain/KeychainManager.ts index ce364f7d8f..8f1cae73e5 100644 --- a/src/core/keychain/KeychainManager.ts +++ b/src/core/keychain/KeychainManager.ts @@ -14,6 +14,7 @@ import { LocalStorage, SessionStorage } from '../storage'; import { KeychainType } from '../types/keychainTypes'; import { isLowerCaseMatch } from '../utils/strings'; +import { RainbowSigner } from './RainbowSigner'; import { HardwareWalletKeychain, SerializedHardwareWalletKeychain, @@ -536,18 +537,13 @@ class KeychainManager { for (let i = 0; i < this.state.keychains.length; i++) { const keychain = this.state.keychains[i]; const accounts = await keychain.getAccounts(); - console.log('address', address); - console.log('accounts', accounts); - console.log( - 'if check', - accounts.map((a) => a.toLowerCase()).includes(address.toLowerCase()), - ); if ( accounts.map((a) => a.toLowerCase()).includes(address.toLowerCase()) ) { return keychain; } } + throw new Error('No keychain found for account'); } @@ -555,6 +551,10 @@ class KeychainManager { const keychain = await this.getKeychain(address); return keychain.getSigner(address); } + + getRainbowSigner = async (): Promise => { + return keychainManager.getRainbowSigner(); + }; } export const keychainManager = new KeychainManager(); diff --git a/src/core/keychain/RainbowSigner.ts b/src/core/keychain/RainbowSigner.ts index e09d147c9c..9972652b77 100644 --- a/src/core/keychain/RainbowSigner.ts +++ b/src/core/keychain/RainbowSigner.ts @@ -5,16 +5,17 @@ import { TransactionFactory, } from '@ethereumjs/tx'; import { TransactionRequest } from '@ethersproject/abstract-provider'; -import { Signer } from '@ethersproject/abstract-signer'; +import { + Signer, + TypedDataDomain, + TypedDataField, +} from '@ethersproject/abstract-signer'; import { BigNumber } from '@ethersproject/bignumber'; import { Bytes } from '@ethersproject/bytes'; import { defineReadOnly } from '@ethersproject/properties'; import { Provider } from '@ethersproject/providers'; -import { - personalSign, - signTypedData, - SignTypedDataVersion, -} from '@metamask/eth-sig-util'; +import { Wallet } from '@ethersproject/wallet'; +import { personalSign } from '@metamask/eth-sig-util'; import { bytesToHex } from 'ethereum-cryptography/utils'; import { Address } from 'viem'; @@ -53,15 +54,20 @@ export class RainbowSigner extends Signer { return signature; } - async signTypedData(typedData: any): Promise { + async signTypedData( + domain: TypedDataDomain, + types: Record>, + value: Record, + ): Promise { const pkey = this.#getPrivateKeyBuffer(); - const signature = signTypedData({ - privateKey: pkey, - data: typedData, - version: SignTypedDataVersion.V4, - }); - - return signature; + const wallet = new Wallet(pkey); + return wallet._signTypedData(domain, types, value); + + // const signature = signTypedData({ + // privateKey: pkey, + // data: typedData, + // version: SignTypedDataVersion.V4, + // }); } async signTransaction(transaction: TransactionRequest): Promise { diff --git a/src/core/keychain/index.ts b/src/core/keychain/index.ts index fccc375e15..700a43893e 100644 --- a/src/core/keychain/index.ts +++ b/src/core/keychain/index.ts @@ -31,10 +31,9 @@ import { import { addHexPrefix } from '../utils/hex'; import { keychainManager } from './KeychainManager'; +import { RainbowSigner } from './RainbowSigner'; import { SerializedKeypairKeychain } from './keychainTypes/keyPairKeychain'; -import { signOperationSet, sendSignedOperations } from '~/core/utils/orb'; - interface TypedDataTypes { EIP712Domain: MessageTypeProperty[]; [additionalProperties: string]: MessageTypeProperty[]; @@ -223,6 +222,10 @@ export const getSigner = async (address: Address): Promise => { return keychainManager.getSigner(address); }; +export const getRainbowSigner = async (): Promise => { + return keychainManager.getRainbowSigner(); +}; + export const exportKeychain = async ( address: Address, password: string, @@ -237,23 +240,6 @@ export const exportAccount = async ( return keychainManager.exportAccount(address, password); }; -export const sendOrbyTransaction = async ({ - clusterId, - operationSet, - virtualNodeRpcUrl, -}): Promise => { - const signedOperationsResponse = await signOperationSet(operationSet); - console.log('signed operation set', signedOperationsResponse); - const response = await sendSignedOperations({ - clusterId, - virtualNodeRpcUrl, - signedOperations: signedOperationsResponse, - }); - console.log('sendSignedOperations response', response); - - return response; -}; - export const sendTransaction = async ( txPayload: TransactionRequest, provider: Provider, diff --git a/src/core/keychain/keychainTypes/hardwareWalletKeychain.ts b/src/core/keychain/keychainTypes/hardwareWalletKeychain.ts index a8c047190b..c2db421c16 100644 --- a/src/core/keychain/keychainTypes/hardwareWalletKeychain.ts +++ b/src/core/keychain/keychainTypes/hardwareWalletKeychain.ts @@ -8,6 +8,7 @@ import { getProvider } from '~/core/wagmi/clientToProvider'; import { HWSigner } from '../HWSigner'; import { IKeychain, PrivateKey } from '../IKeychain'; +import { RainbowSigner } from '../RainbowSigner'; import { getHDPathForVendorAndType } from '../hdPath'; export interface SerializedHardwareWalletKeychain { @@ -89,6 +90,10 @@ export class HardwareWalletKeychain implements IKeychain { ); } + getRainbowSigner(): RainbowSigner { + throw new Error('Method not implemented.'); + } + getPath(address: Address): string { const wallet = privates .get(this) diff --git a/src/core/keychain/keychainTypes/hdKeychain.ts b/src/core/keychain/keychainTypes/hdKeychain.ts index 1749ea5294..eb75996e32 100644 --- a/src/core/keychain/keychainTypes/hdKeychain.ts +++ b/src/core/keychain/keychainTypes/hdKeychain.ts @@ -122,6 +122,15 @@ export class HdKeychain implements IKeychain { return new RainbowSigner(provider, wallet.privateKey, wallet.address); } + getRainbowSigner(): RainbowSigner { + const _privates = privates.get(this)!; + + const provider = getProvider({ chainId: mainnet.id }); + const wallet = _privates!.getWalletForAddress(address) as TWallet; + if (!wallet) throw new Error('Account not found'); + return new RainbowSigner(provider, wallet.privateKey, wallet.address); + } + async serialize(): Promise { const _privates = privates.get(this)!; if (!_privates.mnemonic) throw new Error('No mnemonic'); diff --git a/src/core/keychain/keychainTypes/keyPairKeychain.ts b/src/core/keychain/keychainTypes/keyPairKeychain.ts index 366f8dd18e..f4fdf3d235 100644 --- a/src/core/keychain/keychainTypes/keyPairKeychain.ts +++ b/src/core/keychain/keychainTypes/keyPairKeychain.ts @@ -38,6 +38,13 @@ export class KeyPairKeychain implements IKeychain { return new RainbowSigner(provider, wallet.privateKey, wallet.address); } + getRainbowSigner(): RainbowSigner { + const provider = getProvider({ chainId: mainnet.id }); + const wallet = privates.get(this).wallets[0] as TWallet; + if (!wallet) throw new Error('Account not found'); + return new RainbowSigner(provider, wallet.privateKey, wallet.address); + } + async serialize(): Promise { return { privateKey: (privates.get(this).wallets[0] as Wallet) diff --git a/src/core/keychain/keychainTypes/readOnlyKeychain.ts b/src/core/keychain/keychainTypes/readOnlyKeychain.ts index 31e515d410..030ef39aac 100644 --- a/src/core/keychain/keychainTypes/readOnlyKeychain.ts +++ b/src/core/keychain/keychainTypes/readOnlyKeychain.ts @@ -9,6 +9,7 @@ import { KeychainType } from '~/core/types/keychainTypes'; import { logger } from '~/logger'; import { IKeychain, PrivateKey } from '../IKeychain'; +import { RainbowSigner } from '../RainbowSigner'; export interface SerializedReadOnlyKeychain { type: KeychainType.ReadOnlyKeychain; @@ -30,6 +31,10 @@ export class ReadOnlyKeychain implements IKeychain { throw new Error('Method not implemented.'); } + getRainbowSigner(): RainbowSigner { + throw new Error('Method not implemented.'); + } + addAccountAtIndex(index: number, address: Address): Promise
{ throw new Error('Method not implemented.'); } diff --git a/src/core/providers/proxy.ts b/src/core/providers/proxy.ts index 21fc1c701c..4fff1e8e64 100644 --- a/src/core/providers/proxy.ts +++ b/src/core/providers/proxy.ts @@ -1,5 +1,3 @@ -import { oldDefaultRPC } from '~/core/references/chains'; - import { ChainId } from '../types/chains'; const getHost = (endpoint: string) => { @@ -19,25 +17,5 @@ const isRainbowEndpoint = (endpoint: string) => export const proxyRpcEndpoint = (endpoint: string, chainId: ChainId) => { // NOTE: you'll need your .env file to have the correct RPC URLs - console.log('chainId', chainId); - console.log('using this rpc: ', oldDefaultRPC[chainId]); - return ( - oldDefaultRPC[chainId] || - 'https://ethereum-holesky.core.chainstack.com/3869a6437a482a0d980d76b40cba3d72' - ); - - // if ( - // endpoint && - // endpoint !== 'http://127.0.0.1:8545' && - // endpoint !== 'http://localhost:8545' && - // !endpoint.includes('http://10.') && - // !endpoint.includes('http://192.168') && - // !endpoint.match(/http:\/\/172.(1[6-9]|2[0-9]|3[0-1])./) && - // !isRainbowEndpoint(endpoint) - // ) { - // return `${process.env.RPC_PROXY_BASE_URL}/${chainId}/${ - // process.env.RPC_PROXY_API_KEY - // }?custom_rpc=${encodeURIComponent(endpoint)}`; - // } - // return endpoint; + return endpoint; }; diff --git a/src/core/raps/utils.ts b/src/core/raps/utils.ts index a31799ffd1..1607bab2bd 100644 --- a/src/core/raps/utils.ts +++ b/src/core/raps/utils.ts @@ -299,6 +299,7 @@ export const populateSwap = async ({ ...(methodArgs ?? []), params, ); + return swapTransaction; } catch (e) { return null; diff --git a/src/core/references/assets.ts b/src/core/references/assets.ts index 5e4a30d7e5..557d84c246 100644 --- a/src/core/references/assets.ts +++ b/src/core/references/assets.ts @@ -1,8 +1,10 @@ import { + arbitrum, arbitrumNova, aurora, auroraTestnet, avalanche, + base, blast, blastSepolia, bob, @@ -37,6 +39,7 @@ import { linea, lineaSepolia, lyra, + mainnet, manta, mantaSepoliaTestnet, mantle, @@ -49,9 +52,11 @@ import { moonbeam, opBNB, opBNBTestnet, + optimism, palm, palmTestnet, pgn, + polygon, polygonZkEvm, polygonZkEvmCardona, polygonZkEvmTestnet, @@ -76,10 +81,12 @@ import { import { ChainId } from '../types/chains'; export const customChainIdsToAssetNames: Record = { + [arbitrum.id]: 'arbitrum', [arbitrumNova.id]: 'arbitrumnova', [aurora.id]: 'aurora', [auroraTestnet.id]: 'auroratestnet', [avalanche.id]: 'avalanchex', + [base.id]: 'base', [blast.id]: 'blast', [blastSepolia.id]: 'blastsepolia', [bob.id]: 'bob', @@ -124,6 +131,7 @@ export const customChainIdsToAssetNames: Record = { [lineaSepolia.id]: 'lineasepolia', [ChainId.loot]: 'loot', [lyra.id]: 'lyra', + [mainnet.id]: 'ethereum', [manta.id]: 'manta', [mantaSepoliaTestnet.id]: 'mantasepolia', [mantle.id]: 'mantle', @@ -140,9 +148,11 @@ export const customChainIdsToAssetNames: Record = { 7701: 'nativecantotestnet', [opBNB.id]: 'opbnb', [opBNBTestnet.id]: 'opbnbtestnet', + [optimism.id]: 'optimism', [palm.id]: 'palm', [palmTestnet.id]: 'palmtestnet', [pgn.id]: 'pgn', + [polygon.id]: 'polygon', [polygonZkEvm.id]: 'polygonzkevm', [polygonZkEvmCardona.id]: 'polygonzkevmcardona', [polygonZkEvmTestnet.id]: 'polygonzkevmtestnet', diff --git a/src/core/resources/transactions/transaction.ts b/src/core/resources/transactions/transaction.ts index c025d5e3be..9f91d58643 100644 --- a/src/core/resources/transactions/transaction.ts +++ b/src/core/resources/transactions/transaction.ts @@ -4,7 +4,6 @@ import { QueryClient, useQuery, useQueryClient } from '@tanstack/react-query'; import { Address, Hash } from 'viem'; import { i18n } from '~/core/languages'; -import { addysHttp } from '~/core/network/addys'; import { QueryFunctionResult, createQueryKey } from '~/core/react-query'; import { SupportedCurrencyKey } from '~/core/references'; import { supportedTransactionsChainIds } from '~/core/references/chains'; @@ -19,12 +18,7 @@ import { } from '~/core/state'; import { customNetworkTransactionsStore } from '~/core/state/transactions/customNetworkTransactions'; import { ChainId } from '~/core/types/chains'; -import { - RainbowTransaction, - TransactionApiResponse, - TxHash, -} from '~/core/types/transactions'; -import { parseTransaction } from '~/core/utils/transactions'; +import { RainbowTransaction, TxHash } from '~/core/types/transactions'; import { getProvider } from '~/core/wagmi/clientToProvider'; import { useUserChains } from '~/entries/popup/hooks/useUserChains'; import { RainbowError, logger } from '~/logger'; @@ -44,7 +38,6 @@ const searchInLocalPendingTransactions = (userAddress: Address, hash: Hash) => { export const fetchTransaction = async ({ hash, address, - currency, chainId, }: { hash: TxHash; @@ -61,27 +54,13 @@ export const fetchTransaction = async ({ } try { - const response = await addysHttp.get<{ - payload: { transaction: TransactionApiResponse }; - meta: { status: string }; - }>(`/${chainId}/${address}/transactions/${hash}`, { - params: { currency: currency.toLowerCase() }, + const providerTx = await fetchTransactionDataFromProvider({ + chainId, + hash, + account: address, }); - const tx = response.data.payload.transaction; - if (response.data.meta.status === 'pending') { - const localPendingTx = searchInLocalPendingTransactions(address, hash); - if (localPendingTx) return localPendingTx; - - const providerTx = await fetchTransactionDataFromProvider({ - chainId, - hash, - account: address, - }); - return providerTx; - } - const parsedTx = parseTransaction({ tx, currency, chainId }); - if (!parsedTx) throw new Error('Failed to parse transaction'); - return parsedTx; + + return providerTx; } catch (e) { // if it's a pending tx BE may be in another mempool and it will return 404, // which throws and gets caught here, so we check if we got it in localstorage diff --git a/src/core/state/rainbowChains/index.ts b/src/core/state/rainbowChains/index.ts index e71466d365..8ab2a7fd01 100644 --- a/src/core/state/rainbowChains/index.ts +++ b/src/core/state/rainbowChains/index.ts @@ -33,6 +33,7 @@ export interface RainbowChainsState { chainId: ChainId; }) => void; removeCustomRPC: ({ rpcUrl }: { rpcUrl: string }) => void; + addAllCustomRPC: (rpcs: { rpcUrl: string; chainId: ChainId }[]) => boolean; } export const rainbowChainsStore = createStore( @@ -67,6 +68,37 @@ export const rainbowChainsStore = createStore( return false; } }, + + addAllCustomRPC: (rpcs: { rpcUrl: string; chainId: ChainId }[]) => { + const rainbowChains = get().rainbowChains; + rpcs.forEach(({ chainId, rpcUrl }) => { + const rainbowChain = rainbowChains[chainId] || { + chains: [], + activeRpcUrl: '', + }; + const currentRpcs = rainbowChain.chains.map( + (chain) => chain.rpcUrls.default.http[0], + ); + + if (!currentRpcs.includes(rpcUrl)) { + rainbowChain.chains.push({ + ...rainbowChain.chains[0], + rpcUrls: { + ...rainbowChain.chains[0].rpcUrls, + default: { + ...rainbowChain.chains[0].rpcUrls.default, + http: [rpcUrl], + }, + }, + } as Chain); + rainbowChain.activeRpcUrl = rpcUrl; + rainbowChains[chainId] = rainbowChain; + } + }); + + set({ rainbowChains }); + return true; + }, updateCustomRPC: ({ chain }) => { const rainbowChains = get().rainbowChains; const rainbowChain = rainbowChains[chain.id]; diff --git a/src/core/types/assets.ts b/src/core/types/assets.ts index 9fdfdc5628..40081432e1 100644 --- a/src/core/types/assets.ts +++ b/src/core/types/assets.ts @@ -41,12 +41,16 @@ export interface ParsedAsset { networks: { [id in ChainId]?: { bridgeable: boolean } }; }; transferable?: boolean; + relatedChainIds?: string[]; + relatedAssets?: ParsedUserAsset[]; + standardizedTokenId?: string; } export interface ParsedUserAsset extends ParsedAsset { balance: { amount: string; display: string; + displayOnchain: string; }; native: { balance: { diff --git a/src/core/utils/numbers.ts b/src/core/utils/numbers.ts index 8d4a91671b..b88a2cc32a 100644 --- a/src/core/utils/numbers.ts +++ b/src/core/utils/numbers.ts @@ -461,8 +461,11 @@ export const convertAmountToNativeDisplayWithThreshold = ( export const convertRawAmountToDecimalFormat = ( value: BigNumberish, decimals = 18, + significantDigits = 4, ): string => - new BigNumber(value).dividedBy(new BigNumber(10).pow(decimals)).toFixed(); + new BigNumber(value) + .dividedBy(new BigNumber(10).pow(decimals)) + .toPrecision(significantDigits); /** * @desc convert from decimal format to raw amount @@ -471,7 +474,7 @@ export const convertDecimalFormatToRawAmount = ( value: string, decimals = 18, ): string => - new BigNumber(value).multipliedBy(new BigNumber(10).pow(decimals)).toFixed(0); + new BigNumber(value).multipliedBy(new BigNumber(10).pow(decimals)).toFixed(); export const fromWei = (number: BigNumberish): string => convertRawAmountToDecimalFormat(number, 18); diff --git a/src/core/utils/orb.ts b/src/core/utils/orb.ts index fa8e5c20b5..5f0c66dcea 100644 --- a/src/core/utils/orb.ts +++ b/src/core/utils/orb.ts @@ -1,46 +1,61 @@ +import { TypedDataField } from '@ethersproject/abstract-signer'; +import { + FungibleTokenAmount, + OnchainOperation, + OperationDataFormat, + SignedOperation, + StandardizedBalance, +} from '@orb-labs/orby-core'; +import BigNumber from 'bignumber.js'; import { providers } from 'ethers'; -import { useEffect, useState } from 'react'; -import { Address, formatUnits } from 'viem'; - -import { keychainManager } from '~/core/keychain/KeychainManager'; +import _ from 'lodash'; +import { Address, TypedDataDomain, formatUnits } from 'viem'; import { ParsedUserAsset } from '~/core/types/assets'; -import { ChainId, ChainName } from '~/core/types/chains'; -import { - convertAmountToRawAmount, - toFixedDecimals, - formatFixedDecimals, -} from '~/core/utils/numbers'; +import { ChainName } from '~/core/types/chains'; +import { convertRawAmountToDecimalFormat } from '~/core/utils/numbers'; -const PUBLIC_ORB_RPC_BASE = 'https://api-rpc-dev.orblabs.xyz'; -const PUBLIC_ORB_API_KEY = '4ff141e9-98c5-43ee-8b0e-d552f831b68e'; -const PRIVATE_ORB_API_KEY = 'f1c1d996-8df4-4d23-b926-ca702173021d'; +import { keychainManager } from '../keychain/KeychainManager'; -export const convertFungibleTokenToParsedUserAsset = ( - fungibleToken: any, +export const convertStandardizedBalanceToParsedUserAsset = ( + balance: StandardizedBalance, ): ParsedUserAsset => { - console.log('fungibleToken', fungibleToken); + const relatedChainIds = balance.tokenBalancesOnChains.map((balance) => + balance.token.chainId.toString(), + ); + + const relatedAssets = convertTokenBalancesOnChainsToParsedUserAssets( + balance.tokenBalancesOnChains, + balance, + ); + + const tokenAmount = _.sample(balance.tokenBalancesOnChains); + return { - decimals: fungibleToken.total.currency.decimals, - uniqueId: fungibleToken.standardizedTokenId, - isNativeAsset: - fungibleToken.tokenBalancesOnChains[0].token.currency.isNative, - name: fungibleToken.total.currency.asset.name, - symbol: fungibleToken.total.currency.asset.symbol, + decimals: balance.total.currency.decimals, + uniqueId: balance.standardizedTokenId, + standardizedTokenId: balance.standardizedTokenId, + isNativeAsset: balance.tokenBalancesOnChains[0].token.isNative, + name: balance.total.currency.name, + symbol: balance.total.currency.symbol, // NOTE: we use the address from the fungible token here to be able to select the token // It doesn't seem to break anything yet, but we'll need to change this if it does - address: fungibleToken.standardizedTokenId as Address, - chainId: ChainId.mainnet, + address: tokenAmount?.token.address as Address, + chainId: Number(tokenAmount?.token.chainId), chainName: ChainName.mainnet, balance: { amount: formatUnits( - fungibleToken.total.amount, - fungibleToken.total.currency.decimals, + balance.total.toRawAmount(), + balance.total.currency.decimals, ), - display: `${formatUnits( - fungibleToken.total.amount, - fungibleToken.total.currency.decimals, - )} ${fungibleToken.total.currency.asset.symbol}`, + display: `${convertRawAmountToDecimalFormat( + new BigNumber(balance.total.toRawAmount()?.toString()), + balance.total.currency.decimals, + )} ${balance.total.currency.symbol}`, + displayOnchain: `${convertRawAmountToDecimalFormat( + new BigNumber(balance.total.toRawAmount()?.toString()), + balance.total.currency.decimals, + )} ${balance.total.currency.symbol}`, }, native: { balance: { @@ -49,413 +64,131 @@ export const convertFungibleTokenToParsedUserAsset = ( }, price: { change: '', - amount: fungibleToken.total.value, + amount: Number(balance.total.toRawAmount()), display: 'foo', }, }, - icon_url: fungibleToken.total.currency.logoUrl, + icon_url: balance.total.currency.logoUrl, + relatedChainIds, + relatedAssets, }; }; -export const convertFungibleTokensToParsedUserAssets = ( - fungibleTokens: any, +export const convertStandardizedBalanceToParsedUserAssets = ( + balances: StandardizedBalance[], ): ParsedUserAsset[] => { - return fungibleTokens.map((fungibleToken) => { - return convertFungibleTokenToParsedUserAsset(fungibleToken); - }); -}; - -export const useCreateClusterId = (currentAddress) => { - const [clusterId, setClusterId] = useState(null); - - useEffect(() => { - const createClusterId = async (address) => { - const accounts = [ - { - address, - vmType: 'EVM', - accountType: 'EOA', - }, - ]; - const response = await fetch( - `${PUBLIC_ORB_RPC_BASE}/${PRIVATE_ORB_API_KEY}`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - id: 1, - jsonrpc: '2.0', - method: 'orby_createAccountCluster', - params: [{ accounts }], - }), - }, - ); - const { result } = await response.json(); - console.log('cluster data', result); - setClusterId(result.accountClusterId); - }; - createClusterId(currentAddress); - }, [currentAddress]); - - return clusterId; -}; - -export const useVirtualNodeRpcUrl = ( - clusterId, - currentAddress, - testnetMode, -) => { - const [virtualNodeRpcUrl, setVirtualNodeRpcUrl] = useState( - null, + return balances.map((balance) => + convertStandardizedBalanceToParsedUserAsset(balance), ); - - useEffect(() => { - const fetchVirtualNodeRpcUrl = async () => { - const response = await fetch( - `${PUBLIC_ORB_RPC_BASE}/${PRIVATE_ORB_API_KEY}`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - id: 2, - jsonrpc: '2.0', - method: 'orby_getVirtualNodeRpcUrl', - params: [ - { - accountClusterId: clusterId, - entrypointAccountAddress: currentAddress, - chainId: testnetMode ? `EIP155-11155420` : `EIP155-8453`, - }, - ], - }), - }, - ); - const { result } = await response.json(); - console.log('virtual node rpc url', result); - setVirtualNodeRpcUrl(result.virtualNodeRpcUrl); - }; - if (clusterId && currentAddress) { - fetchVirtualNodeRpcUrl(); - } - }, [clusterId, currentAddress]); - - return virtualNodeRpcUrl; }; -export const getOperationsToExecuteTransaction = async ({ - virtualNodeRpcUrl, - request, -}: { - virtualNodeRpcUrl: string; - request: { - to: string; - data: string; - value: string; - }; -}) => { - const response = await fetch(virtualNodeRpcUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - jsonrpc: '2.0', - id: 1, - method: 'orby_getOperationsToExecuteTransaction', - params: [{ ...request }], - }), - }); - - const { result } = await response.json(); - console.log('getOperationsToExecuteTransaction result', result); - return result; -}; - -export const getOperationsToSignTypedData = async ({ - clusterId, - virtualNodeRpcUrl, - to, - data, -}) => { - const response = await fetch(virtualNodeRpcUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', +export const convertFungibleTokenAmountToParsedUserAsset = ( + balance: FungibleTokenAmount, + standardizedBalance: StandardizedBalance, +): ParsedUserAsset => { + return { + decimals: balance.token.decimals, + standardizedTokenId: standardizedBalance.standardizedTokenId, + uniqueId: balance.token.identifier(), + isNativeAsset: balance.token.isNative, + name: balance.token.name, + symbol: balance.token.symbol, + // NOTE: we use the address from the fungible token here to be able to select the token + // It doesn't seem to break anything yet, but we'll need to change this if it does + address: balance.token.address as Address, + chainId: Number(balance.token.chainId), + chainName: ChainName.mainnet, + balance: { + amount: formatUnits( + standardizedBalance.total.toRawAmount(), + standardizedBalance.total.currency.decimals, + ), + display: `${convertRawAmountToDecimalFormat( + new BigNumber(standardizedBalance.total.toRawAmount()?.toString()), + standardizedBalance.total.currency.decimals, + )} ${balance.token.symbol}`, + displayOnchain: `${convertRawAmountToDecimalFormat( + new BigNumber(balance.toRawAmount()?.toString()), + balance.token.decimals, + )} ${balance.token.symbol}`, }, - body: JSON.stringify({ - id: 2, - jsonrpc: '2.0', - method: 'orby_getOperationsToSignTypedData', - params: [{ to, data, accountClusterId: clusterId }], - }), - }); - - const { result } = await response.json(); - console.log('getOperationsToSignTypedData result', result); - return result; -}; - -export const sendSignedOperations = async ({ - clusterId, - signedOperations, - virtualNodeRpcUrl, -}) => { - const response = await fetch(virtualNodeRpcUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', + native: { + balance: { + amount: '', + display: '', // this is the price + }, + price: { + change: '', + amount: Number(standardizedBalance.total.toRawAmount()), + display: 'foo', + }, }, - body: JSON.stringify({ - id: 2, - jsonrpc: '2.0', - method: 'orby_sendSignedOperations', - params: [{ accountClusterId: clusterId, signedOperations }], - }), - }); - const { result } = await response.json(); - console.log('sendSignedOperations result', result); - return result; -}; - -// Function that signs an operation set. -export async function signOperationSet(operations) { - const signedOperations = []; - - // Loop through and sign all the operations - for (let i = 0; i < operations.length; i++) { - console.log('operations[i]', operations[i]); - // Set the provider and wallet instances for each operation - const provider = new providers.JsonRpcProvider(operations[i].txRpcUrl); - const signer = await keychainManager.getSigner( - operations[i].from as Address, - ); - const wallet = signer.connect(provider); - - console.log('provider', provider); - console.log('signer', signer); - console.log('wallet', wallet); - - let signedOperation; - - // Sign transactions or typed data - if (operations[i].format == 'TRANSACTION') { - const txData = { - from: operations[i].from, - to: operations[i].to, - value: operations[i].value, - data: operations[i].data, - nonce: operations[i].nonce, - gasLimit: operations[i].gasLimit, - // TODO: make note of this, add this to Monday, remind Felix of this - // gasPrice: operations[i].gasPrice, - maxFeePerGas: operations[i].maxFeePerGas, - maxPriorityFeePerGas: operations[i].maxPriorityFeePerGas, - }; - - console.log('txData', txData); - - const tx = await wallet.populateTransaction(txData); - console.log('tx', tx); - const signedTx = await wallet.signTransaction(tx); - console.log('signedTx', signedTx); - signedOperation = { type: operations[i].type, signature: signedTx }; - } else if (operations[i].format == 'TYPED_DATA') { - const parsedData = JSON.parse(operations[i].data); - - const signature = await wallet.signTypedData( - parsedData.domain, - parsedData.types, - parsedData.message, - ); - - console.log('signature', signature); - - signedOperation = { - type: operations[i].type, - signature, - data: operations[i].data, - }; - } - // append transaction to the signed operations array - signedOperations.push(signedOperation); - } - console.log('signedOperations: ', signedOperations); - // Return the signed operations array - return signedOperations; -} - -export const usePortfolio = (clusterId, virtualNodeRpcUrl) => { - const [portfolio, setPortfolio] = useState(null); - - useEffect(() => { - const fetchPortfolio = async () => { - const response = await fetch(virtualNodeRpcUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - id: 2, - jsonrpc: '2.0', - method: 'orby_getFungibleTokenPortfolio', - params: [{ accountClusterId: clusterId }], - }), - }); - const { result } = await response.json(); - console.log('portfolio data', result); - setPortfolio(result); - }; - if (clusterId && virtualNodeRpcUrl) { - fetchPortfolio(); - } - }, [clusterId, virtualNodeRpcUrl]); - - return portfolio; + icon_url: standardizedBalance.total.currency.logoUrl, + }; }; -export const usePortfolioBalance = (clusterId, virtualNodeRpcUrl) => { - const [portfolioBalance, setPortfolioBalance] = useState(null); - - useEffect(() => { - const fetchPortfolioBalance = async () => { - const response = await fetch(virtualNodeRpcUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - id: 2, - jsonrpc: '2.0', - method: 'orby_getPortfolioOverview', - params: [{ accountClusterId: clusterId }], - }), - }); - const { result } = await response.json(); - console.log('portfolio balance data', result); - console.log( - `${Number(result.totalValueInFiat.value).toFixed( - result.totalValueInFiat.currency.decimals, - )}`, - ); - setPortfolioBalance( - `$${Number(result.totalValueInFiat.value).toFixed( - result.totalValueInFiat.currency.decimals, - )}`, +export const convertFungibleTokenAmountsToParsedUserAssets = ( + balances: StandardizedBalance[], +): ParsedUserAsset[] => { + return balances + .map((standardizedBalance) => { + return convertTokenBalancesOnChainsToParsedUserAssets( + standardizedBalance.tokenBalancesOnChains, + standardizedBalance, ); - }; - if (clusterId && virtualNodeRpcUrl) { - fetchPortfolioBalance(); - } - }, [clusterId, virtualNodeRpcUrl]); - - return portfolioBalance; + }) + .flat(); }; -export const getOperationsToTransferToken = async ({ - clusterId, - standardizedTokenId, - amount, - recipient, - virtualNodeRpcUrl, -}: { - clusterId: string; - standardizedTokenId: string; - amount: string; - recipient: { address: string; chainId: string }; - virtualNodeRpcUrl: string; -}) => { - const response = await fetch(virtualNodeRpcUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - id: 2, - jsonrpc: '2.0', - method: 'orby_getOperationsToTransferToken', - params: [ - { - accountClusterId: clusterId, - standardizedTokenId, - amount, - recipient, - }, - ], - }), - }); - const { result } = await response.json(); - console.log('operations to transfer token', result); - return result; +export const convertTokenBalancesOnChainsToParsedUserAssets = ( + tokenBalancesOnChains: FungibleTokenAmount[], + standardizedBalance: StandardizedBalance, +): ParsedUserAsset[] => { + return tokenBalancesOnChains.map((balance) => + convertFungibleTokenAmountToParsedUserAsset(balance, standardizedBalance), + ); }; -export const getOperationsToSwap = async ({ - virtualNodeRpcUrl, - clusterId, - swapType, - input, - output, -}) => { - const response = await fetch(virtualNodeRpcUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - jsonrpc: '2.0', - id: 1, - method: 'orby_getOperationsToSwap', - params: [ - { - accountClusterId: clusterId, - swapType, - input, - output, - }, - ], - }), - }); +export async function signOperation( + operation: OnchainOperation, +): Promise { + const provider = new providers.JsonRpcProvider(operation.txRpcUrl); + const signer = await keychainManager.getSigner(operation.from as Address); + const wallet = signer.connect(provider); + + if (operation.format == OperationDataFormat.TRANSACTION) { + const txData = { + from: operation.from, + to: operation.to, + value: operation.value, + data: operation.data, + nonce: operation.nonce, + gasLimit: operation.gasLimit, + chainId: operation.chainId ? Number(operation.chainId) : undefined, + // TODO: make note of this, add this to Monday, remind Felix of this + // gasPrice: operations[i].gasPrice, + maxFeePerGas: operation.maxFeePerGas, + maxPriorityFeePerGas: operation.maxPriorityFeePerGas, + }; - const { result } = await response.json(); - return result; -}; + // eslint-disable-next-line no-await-in-loop + const tx = await wallet.populateTransaction(txData); + const signedTx = await signer.signTransaction(tx); + return { type: operation.type, signature: signedTx }; + } else { + const parsedData = JSON.parse(operation.data) as { + domain: TypedDataDomain; + types: Record>; + message: Record; + }; -export const getStandardizedTokenId = async ({ - virtualNodeRpcUrl, - chainId, - tokenAddress, -}: { - virtualNodeRpcUrl: string; - chainId: string; - tokenAddress: string; -}) => { - const response = await fetch(virtualNodeRpcUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - jsonrpc: '2.0', - id: 1, - method: 'orby_getStandardizedTokenIds', - params: [ - { - tokens: [ - { - chainId, - tokenAddress, - }, - ], - }, - ], - }), - }); - const { result } = await response.json(); - console.log('result', result); + // @ts-ignore + const signature = await wallet.signTypedData( + parsedData.domain, + parsedData.types, + parsedData.message, + ); - // TODO: get the first one - return result?.standardizedTokenIds?.[0] || null; -}; + return { type: operation.type, signature, data: operation.data }; + } +} diff --git a/src/core/wagmi/index.ts b/src/core/wagmi/index.ts index 9ca02fca3e..7ca33a77c4 100644 --- a/src/core/wagmi/index.ts +++ b/src/core/wagmi/index.ts @@ -1,10 +1,12 @@ -import { useEffect } from 'react'; +import { useOrby } from '@orb-labs/orby-react'; +import { useEffect, useMemo } from 'react'; import { Chain, HttpTransport, Transport, http } from 'viem'; import { createConfig } from 'wagmi'; import { useRainbowChains } from '~/entries/popup/hooks/useRainbowChains'; import { SUPPORTED_CHAINS } from '../references/chains'; +import { useRainbowChainsStore } from '../state'; import { handleRpcUrl } from './clientRpc'; @@ -42,10 +44,28 @@ const updateWagmiConfig = (chains: Chain[]) => { const WagmiConfigUpdater = () => { const { rainbowChains: chains } = useRainbowChains(); - useEffect(() => { - updateWagmiConfig(chains); - }, [chains]); + const { addAllCustomRPC } = useRainbowChainsStore(); + const { getVirtualNodeRpcUrlsForSupportedChains } = useOrby(); + const virtualNodeUrls = useMemo( + () => getVirtualNodeRpcUrlsForSupportedChains(), + [getVirtualNodeRpcUrlsForSupportedChains], + ); + + useEffect((): void => { + const rpcs = virtualNodeUrls?.map((virtualNodeUrl) => { + return { + rpcUrl: virtualNodeUrl.virtualNodeRpcUrl!, + chainId: Number(virtualNodeUrl.chainId), + }; + }); + + if (rpcs) { + addAllCustomRPC(rpcs); + } + }, [addAllCustomRPC, virtualNodeUrls]); + + updateWagmiConfig(chains); return null; }; diff --git a/src/entries/background/handlers/handleWallets.ts b/src/entries/background/handlers/handleWallets.ts index 296ff0fcc5..4ff5131304 100644 --- a/src/entries/background/handlers/handleWallets.ts +++ b/src/entries/background/handlers/handleWallets.ts @@ -29,7 +29,6 @@ import { lockVault, removeAccount, sendTransaction, - sendOrbyTransaction, setVaultPassword, signMessage, signTypedData, @@ -184,18 +183,6 @@ export const handleWallets = () => response = await exportAccount(address, password); break; } - case 'send_orby_transaction': { - // NOTE: i'm not handling flashbots here, but we can add that later - - const { operationSet, virtualNodeRpcUrl, clusterId } = payload; - response = await sendOrbyTransaction({ - clusterId, - operationSet, - virtualNodeRpcUrl, - }); - - break; - } case 'send_transaction': { let provider; if ( diff --git a/src/entries/background/index.ts b/src/entries/background/index.ts index 7096533383..62d582abd9 100644 --- a/src/entries/background/index.ts +++ b/src/entries/background/index.ts @@ -1,3 +1,4 @@ +import { unifyBalancesOnApps } from '@orb-labs/orby-core-mini'; import { uuid4 } from '@sentry/utils'; import { initFCM } from '~/core/firebase/fcm'; @@ -22,6 +23,11 @@ initializeSentry('background'); const popupMessenger = initializeMessenger({ connect: 'popup' }); const inpageMessenger = initializeMessenger({ connect: 'inpage' }); +unifyBalancesOnApps( + '/', + `${process.env.ORBY_BASE_URL}/${process.env.ORBY_PRIVATE_API_KEY}`, +); + handleInstallExtension(); handleProviderRequest({ popupMessenger, inpageMessenger }); handleTabAndWindowUpdates(); diff --git a/src/entries/popup/App.tsx b/src/entries/popup/App.tsx index a365733b15..2b4b29854d 100644 --- a/src/entries/popup/App.tsx +++ b/src/entries/popup/App.tsx @@ -1,7 +1,10 @@ +import { Account, AccountType, VMType } from '@orb-labs/orby-core'; +import { OrbyProvider } from '@orb-labs/orby-react'; import { QueryClientProvider } from '@tanstack/react-query'; import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'; import { isEqual } from 'lodash'; import * as React from 'react'; +import { useMemo } from 'react'; import { WagmiProvider } from 'wagmi'; import { analytics } from '~/analytics'; @@ -13,7 +16,11 @@ import config from '~/core/firebase/remoteConfig'; import { initializeMessenger } from '~/core/messengers'; import { persistOptions, queryClient } from '~/core/react-query'; import { initializeSentry, setSentryUser } from '~/core/sentry'; -import { useCurrentLanguageStore, useDeviceIdStore } from '~/core/state'; +import { + useCurrentAddressStore, + useCurrentLanguageStore, + useDeviceIdStore, +} from '~/core/state'; import { useCurrentThemeStore } from '~/core/state/currentSettings/currentTheme'; import { POPUP_DIMENSIONS } from '~/core/utils/dimensions'; import { WagmiConfigUpdater, wagmiConfig } from '~/core/wagmi'; @@ -74,10 +81,6 @@ export function App() { lazyLoad: true, }); - // if (process.env.IS_DEV !== 'true') { - // document.addEventListener('contextmenu', (e) => e.preventDefault()); - // } - // prevent trackpad double tap zoom const app = document.getElementById('app'); app?.addEventListener('wheel', (e) => { @@ -95,6 +98,24 @@ export function App() { const { currentTheme } = useCurrentThemeStore(); const isFullScreen = useIsFullScreen(); + const { currentAddress } = useCurrentAddressStore(); + + const orbyConfig = useMemo(() => { + return { + instancePrivateAPIKey: process.env.ORBY_PRIVATE_API_KEY as string, + instancePublicAPIKey: process.env.ORBY_PUBLIC_API_KEY as string, + appName: 'Rainbow', + accounts: [ + new Account( + currentAddress?.toLowerCase(), + AccountType.EOA, + VMType.EVM, + undefined, + ), + ], + }; + }, [currentAddress]); + return ( <> @@ -104,22 +125,24 @@ export function App() { > - - - - - - - - + + + + + + + + + + diff --git a/src/entries/popup/components/CoinIcon/CoinIcon.tsx b/src/entries/popup/components/CoinIcon/CoinIcon.tsx index c047704e65..5ae1b76b5e 100644 --- a/src/entries/popup/components/CoinIcon/CoinIcon.tsx +++ b/src/entries/popup/components/CoinIcon/CoinIcon.tsx @@ -1,5 +1,6 @@ +import { AddressZero } from '@ethersproject/constants'; import { upperCase } from 'lodash'; -import React, { ReactNode } from 'react'; +import React, { ReactNode, useMemo } from 'react'; import EthIcon from 'static/assets/ethIcon.png'; import { ETH_ADDRESS } from '~/core/references'; @@ -12,6 +13,7 @@ import { import { ChainId } from '~/core/types/chains'; import { UniqueAsset } from '~/core/types/nfts'; import { SearchAsset } from '~/core/types/search'; +import { getCustomChainIconUrl } from '~/core/utils/assets'; import { AccentColorProvider, Box, Symbol } from '~/design-system'; import { BoxStyles } from '~/design-system/styles/core.css'; import { colors as emojiColors } from '~/entries/popup/utils/emojiAvatarBackgroundColors'; @@ -38,6 +40,7 @@ export function CoinIcon({ badgePositionBottom = 0, badgePositionLeft = -6, badgeSize = '16', + isParent, }: { asset?: | ParsedAsset @@ -51,6 +54,7 @@ export function CoinIcon({ badgePositionBottom?: number; badgePositionLeft?: number; badgeSize?: ChainIconProps['size']; + isParent: boolean; }) { const mainnetAddress = asset?.mainnetAddress; const address = asset?.address; @@ -60,6 +64,16 @@ export function CoinIcon({ (asset as ParsedAsset)?.standard === 'erc-721' || (asset as ParsedAsset)?.standard === 'erc-1155'; + const url = useMemo(() => { + if (!asset) { + return undefined; + } + + return isParent + ? asset?.icon_url + : getCustomChainIconUrl(asset.chainId!, AddressZero); + }, [asset, isParent]); + return asset ? ( @@ -123,14 +137,9 @@ function ShadowWrapper({ } function CoinIconWrapper({ - chainId, children, shadowColor, size, - badge = true, - badgePositionBottom, - badgePositionLeft, - badgeSize, borderRadius, }: { chainId: ChainId; @@ -152,7 +161,7 @@ function CoinIconWrapper({ > {children} - {badge && chainId !== ChainId.mainnet && ( + {/* {badge && chainId !== ChainId.mainnet && ( - )} + )} */} ); } +export function ChainIcon({ + size, + url, + fallbackText, +}: { + size: number; + url?: string; + fallbackText?: string; +}) { + if (url) { + return ( + + ); + } + return ( + + ); +} + const nftRadiusBySize = { 14: '4px', 16: '4px', @@ -297,8 +331,6 @@ export const NFTIcon = ({ export const ContractIcon = ({ size, iconUrl, - badge, - chainId, }: { iconUrl?: string; size: keyof typeof nftRadiusBySize; @@ -335,11 +367,11 @@ export const ContractIcon = ({ width={size} height={size} /> - {badge && chainId && chainId !== ChainId.mainnet && ( + {/* {badge && chainId && chainId !== ChainId.mainnet && ( - )} + )} */} ); }; @@ -378,6 +410,7 @@ export function TwoCoinsIcon({ size={underSize} fallbackText={under.symbol} badge={false} + isParent={false} /> @@ -398,6 +431,7 @@ export function TwoCoinsIcon({ size={overSize} fallbackText={over.symbol} badge={false} + isParent={false} /> diff --git a/src/entries/popup/components/CoinRow/CoinRow.tsx b/src/entries/popup/components/CoinRow/CoinRow.tsx index 084ef5b009..263f7e80f6 100644 --- a/src/entries/popup/components/CoinRow/CoinRow.tsx +++ b/src/entries/popup/components/CoinRow/CoinRow.tsx @@ -25,12 +25,16 @@ export function CoinRow({ topRow, bottomRow, testId, + size, + isParent, }: { asset?: ParsedAsset | ParsedUserAsset; fallbackText?: string; topRow: ReactNode; bottomRow: ReactNode; testId?: string; + size: number; + isParent: boolean; }) { return ( @@ -40,8 +44,22 @@ export function CoinRow({ - + + {/* + {!isParent && ( + + + {asset?.chainName} + + + )} + */} {topRow} diff --git a/src/entries/popup/components/Tabs/TabBar.tsx b/src/entries/popup/components/Tabs/TabBar.tsx index 2615fbb4c9..f9bdcca88e 100644 --- a/src/entries/popup/components/Tabs/TabBar.tsx +++ b/src/entries/popup/components/Tabs/TabBar.tsx @@ -17,10 +17,6 @@ import ActivityIcon from './TabIcons/Activity'; import ActivitySelected from './TabIcons/ActivitySelected'; import HomeIcon from './TabIcons/Home'; import HomeSelected from './TabIcons/HomeSelected'; -import NFTsIcon from './TabIcons/NFTs'; -import NFTsSelected from './TabIcons/NFTsSelected'; -import PointsIcon from './TabIcons/Points'; -import PointsSelected from './TabIcons/PointsSelected'; export type Tab = (typeof TABS)[number]; @@ -58,16 +54,16 @@ const tabConfig: TabConfigType[] = [ SelectedIcon: ActivitySelected, name: 'activity', }, - { - Icon: NFTsIcon, - SelectedIcon: NFTsSelected, - name: 'nfts', - }, - { - Icon: PointsIcon, - SelectedIcon: PointsSelected, - name: 'points', - }, + // { + // Icon: NFTsIcon, + // SelectedIcon: NFTsSelected, + // name: 'nfts', + // }, + // { + // Icon: PointsIcon, + // SelectedIcon: PointsSelected, + // name: 'points', + // }, ]; export const TabBar = memo(function TabBar() { diff --git a/src/entries/popup/components/TransactionFee/GasTokenMenu.tsx b/src/entries/popup/components/TransactionFee/GasTokenMenu.tsx new file mode 100644 index 0000000000..de285aceb2 --- /dev/null +++ b/src/entries/popup/components/TransactionFee/GasTokenMenu.tsx @@ -0,0 +1,202 @@ +import React, { useImperativeHandle, useRef } from 'react'; + +import { AddressOrEth } from '~/core/types/assets'; +import { ChainId } from '~/core/types/chains'; +import { Box, Inline, Symbol, Text } from '~/design-system'; +import { Space } from '~/design-system/styles/designTokens'; + +import { GasTokenInput } from '../../pages/send'; +import { simulateClick } from '../../utils/simulateClick'; +import { CoinIcon } from '../CoinIcon/CoinIcon'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItemIndicator, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '../DropdownMenu/DropdownMenu'; + +const gasTokens: GasTokenInput[] = [ + { + name: 'no gas abstraction', + standardizedTokenId: undefined, + isDefault: true, + }, + { + name: 'USDC', + standardizedTokenId: 'sttkn_1e58ac683b9e4d28b1b4193f69c49d12', + isDefault: false, + url: 'https://cryptologos.cc/logos/usd-coin-usdc-logo.svg', + }, + { + name: 'WETH', + standardizedTokenId: 'sttkn_fe26388df9394694ad49acb77547e334', + isDefault: false, + url: 'https://raw.githubusercontent.com/rainbow-me/assets/master/blockchains/base/assets/0x4200000000000000000000000000000000000006/logo.png', + }, +]; + +export const SwitchGasTokenMenuSelector = ({ + selectedGasToken, +}: { + selectedGasToken?: GasTokenInput; + chainId: ChainId; +}) => { + return ( + <> + {gasTokens.map((gasToken, i) => { + return ( + + + + {!gasToken.isDefault && ( + + )} + + {gasToken.name} + + + + + + + + ); + })} + + ); +}; + +interface SwitchGasTokenMenuProps { + selectedGasToken?: GasTokenInput; + onGasTokenChanged?: (gasToken?: GasTokenInput) => void; + accentColor?: string | 'accent'; + editable?: boolean; + plainTriggerBorder?: boolean; + dropdownContentMarginRight?: Space; + chainId: ChainId; +} + +export const SwitchGasTokenMenu = React.forwardRef< + { open: () => void }, + SwitchGasTokenMenuProps +>(function SwitchGasTokenMenu( + { + dropdownContentMarginRight, + selectedGasToken, + editable = true, + accentColor, + onGasTokenChanged, + chainId, + }: SwitchGasTokenMenuProps, + forwardedRef, +) { + const triggerRef = useRef(null); + + useImperativeHandle(forwardedRef, () => ({ + open: () => { + simulateClick(triggerRef?.current); + }, + })); + + const menuTrigger = ( + + + {!selectedGasToken?.isDefault && ( + + )} + + {selectedGasToken?.name} + + + {editable ? ( + + ) : null} + + + ); + if (!editable) return menuTrigger; + return ( + + + {menuTrigger} + + + Gas Token + + { + onGasTokenChanged?.( + gasTokens.find( + (gasToken) => gasToken?.standardizedTokenId == gasTokenId, + ), + ); + }} + > + + + + + ); +}); diff --git a/src/entries/popup/components/TransactionFee/TransactionFee.tsx b/src/entries/popup/components/TransactionFee/TransactionFee.tsx index 5bfc248721..198e7fa68f 100644 --- a/src/entries/popup/components/TransactionFee/TransactionFee.tsx +++ b/src/entries/popup/components/TransactionFee/TransactionFee.tsx @@ -15,17 +15,7 @@ import { GasFeeParamsBySpeed, GasSpeed, } from '~/core/types/gas'; -import { - Box, - Column, - Columns, - Inline, - Row, - Rows, - Symbol, - Text, -} from '~/design-system'; -import { Lens } from '~/design-system/components/Lens/Lens'; +import { Box, Column, Columns, Inline, Row, Rows, Text } from '~/design-system'; import { TextOverflow } from '~/design-system/components/TextOverflow/TextOverflow'; import { Space } from '~/design-system/styles/designTokens'; @@ -37,11 +27,10 @@ import { } from '../../hooks/useGas'; import useKeyboardAnalytics from '../../hooks/useKeyboardAnalytics'; import { useKeyboardShortcut } from '../../hooks/useKeyboardShortcut'; -import { ChainBadge } from '../ChainBadge/ChainBadge'; -import { CursorTooltip } from '../Tooltip/CursorTooltip'; +import { GasTokenInput } from '../../pages/send'; import { CustomGasSheet } from './CustomGasSheet'; -import { SwitchTransactionSpeedMenu } from './TransactionSpeedsMenu'; +import { SwitchGasTokenMenu } from './GasTokenMenu'; type FeeProps = { chainId: ChainId; @@ -65,10 +54,12 @@ type FeeProps = { setCustomMaxBaseFee: (maxBaseFee?: string) => void; setCustomMaxPriorityFee: (maxPriorityFee?: string) => void; setCustomGasPrice: (gasPrice?: string) => void; + selectedGasToken?: GasTokenInput; + setSelectedGasToken?: (gasToken?: GasTokenInput) => void; + aggregateFee?: string; }; function Fee({ - accentColor, analyticsEvents, baseFeeTrend, chainId, @@ -76,7 +67,6 @@ function Fee({ currentBaseFee, gasFeeParamsBySpeed, isLoading, - plainTriggerBorder, selectedSpeed, flashbotsEnabled, speedMenuMarginRight, @@ -85,6 +75,9 @@ function Fee({ setCustomMaxBaseFee, setCustomMaxPriorityFee, setCustomGasPrice, + selectedGasToken, + setSelectedGasToken, + aggregateFee, }: FeeProps) { const { trackShortcut } = useKeyboardAnalytics(); const [showCustomGasSheet, setShowCustomGasSheet] = useState(false); @@ -104,31 +97,34 @@ function Fee({ [], ); - const onSpeedChanged = useCallback( - (speed: GasSpeed) => { - if (speed === GasSpeed.CUSTOM) { - openCustomGasSheet(); - } else { - setSelectedSpeed(speed); - } - analyticsEvents?.transactionSpeedSwitched && - analytics.track(analyticsEvents?.transactionSpeedSwitched, { speed }); - }, - [ - analyticsEvents?.transactionSpeedSwitched, - openCustomGasSheet, - setSelectedSpeed, - ], - ); + // const onSpeedChanged = useCallback( + // (speed: GasSpeed) => { + // if (speed === GasSpeed.CUSTOM) { + // openCustomGasSheet(); + // } else { + // setSelectedSpeed(speed); + // } + // analyticsEvents?.transactionSpeedSwitched && + // analytics.track(analyticsEvents?.transactionSpeedSwitched, { speed }); + // }, + // [ + // analyticsEvents?.transactionSpeedSwitched, + // openCustomGasSheet, + // setSelectedSpeed, + // ], + // ); - const onSpeedOpenChange = useCallback( - (isOpen: boolean) => { - isOpen && - analyticsEvents?.transactionSpeedClicked && - analytics.track(analyticsEvents?.transactionSpeedClicked); - }, - [analyticsEvents?.transactionSpeedClicked], - ); + // const onSpeedOpenChange = useCallback( + // (isOpen: boolean) => { + // isOpen && + // analyticsEvents?.transactionSpeedClicked && + // analytics.track(analyticsEvents?.transactionSpeedClicked); + // }, + // [analyticsEvents?.transactionSpeedClicked], + // ); + + // console.log('in tx fee', selectedGasToken); + // console.log('in tx fee', setSelectedGasToken); useKeyboardShortcut({ handler: (e: KeyboardEvent) => { @@ -179,16 +175,9 @@ function Fee({ - - - - {isLoading - ? '~' - : `${ - gasFeeParamsForSelectedSpeed?.gasFee.display || '~' - }`} + ${aggregateFee} @@ -211,7 +200,15 @@ function Fee({ - + {/* - + */} @@ -272,6 +269,9 @@ type TransactionFeeProps = { transactionSpeedSwitched: keyof EventProperties; transactionSpeedClicked: keyof EventProperties; }; + selectedGasToken?: GasTokenInput; + setSelectedGasToken?: (gasToken?: GasTokenInput) => void; + aggregateFee?: string; }; export function TransactionFee({ @@ -284,6 +284,9 @@ export function TransactionFee({ plainTriggerBorder, analyticsEvents, flashbotsEnabled, + selectedGasToken, + setSelectedGasToken, + aggregateFee, }: TransactionFeeProps) { const { defaultTxSpeed } = useDefaultTxSpeed({ chainId }); const { @@ -323,6 +326,9 @@ export function TransactionFee({ baseFeeTrend={baseFeeTrend} flashbotsEnabled={!!flashbotsEnabled} feeType={feeType} + selectedGasToken={selectedGasToken} + setSelectedGasToken={setSelectedGasToken} + aggregateFee={aggregateFee} /> ); } @@ -339,6 +345,9 @@ type SwapFeeProps = { flashbotsEnabled?: boolean; speedMenuMarginRight?: Space; quoteServiceTime?: number; + selectedGasToken: GasTokenInput; + setSelectedGasToken: (gasToken?: GasTokenInput) => void; + aggregateFee: string; }; export function SwapFee({ @@ -353,6 +362,9 @@ export function SwapFee({ flashbotsEnabled, speedMenuMarginRight, quoteServiceTime, + selectedGasToken, + setSelectedGasToken, + aggregateFee, }: SwapFeeProps) { const { defaultTxSpeed } = useDefaultTxSpeed({ chainId }); const { @@ -394,6 +406,9 @@ export function SwapFee({ flashbotsEnabled={!!flashbotsEnabled} speedMenuMarginRight={speedMenuMarginRight} feeType={feeType} + selectedGasToken={selectedGasToken} + setSelectedGasToken={setSelectedGasToken} + aggregateFee={aggregateFee} /> ); } @@ -414,6 +429,9 @@ type ApprovalFeeProps = { transactionSpeedClicked: keyof EventProperties; }; assetType: 'erc20' | 'nft'; + selectedGasToken: GasTokenInput; + setSelectedGasToken: (gasToken?: GasTokenInput) => void; + aggregateFee: string; }; export function ApprovalFee({ @@ -428,6 +446,9 @@ export function ApprovalFee({ analyticsEvents, flashbotsEnabled, assetType, + selectedGasToken, + setSelectedGasToken, + aggregateFee, }: ApprovalFeeProps) { const { defaultTxSpeed } = useDefaultTxSpeed({ chainId }); const { @@ -468,6 +489,9 @@ export function ApprovalFee({ baseFeeTrend={baseFeeTrend} flashbotsEnabled={!!flashbotsEnabled} feeType={feeType} + selectedGasToken={selectedGasToken} + setSelectedGasToken={setSelectedGasToken} + aggregateFee={aggregateFee} /> ); } diff --git a/src/entries/popup/handlers/wallet.ts b/src/entries/popup/handlers/wallet.ts index 9b33df0a9a..b02ed58710 100644 --- a/src/entries/popup/handlers/wallet.ts +++ b/src/entries/popup/handlers/wallet.ts @@ -47,8 +47,6 @@ import { import { walletAction } from './walletAction'; import { HARDWARE_WALLETS } from './walletVariables'; -import { getOperationsToTransferToken } from '~/core/utils/orb'; - const signMessageByType = async ( msgData: string | Bytes, address: Address, @@ -96,26 +94,6 @@ export const signTransactionFromHW = async ( } }; -export const sendOrbyTransaction = async ({ - clusterId, - virtualNodeRpcUrl, - operationSet, -}: { - clusterId: string; - virtualNodeRpcUrl: string; - operationSet: any; -}): Promise => { - // NOTE: i'm not handling hardware wallets here, but we can add that later - const transactionResponse = await walletAction( - 'send_orby_transaction', - { operationSet, virtualNodeRpcUrl, clusterId }, - ); - - console.log('transactionResponse', transactionResponse); - - // return deserializeBigNumbers(transactionResponse); -}; - export const sendTransaction = async ( transactionRequest: TransactionRequest, ): Promise => { @@ -128,8 +106,6 @@ export const sendTransaction = async ( provider, }); - console.log('selectedGas', selectedGas); - const nonce = transactionRequest.nonce ?? (await getNextNonce({ diff --git a/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts b/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts index d19bd41778..e7cc3c614f 100644 --- a/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts +++ b/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts @@ -7,8 +7,6 @@ import { useConnectedToHardhatStore } from '~/core/state/currentSettings/connect import { ChainId } from '~/core/types/chains'; import { chainIdToUse, getChain } from '~/core/utils/chains'; -import { useHasEnoughGas } from '../../pages/messages/useHasEnoughGas'; - export const useApproveAppRequestValidations = ({ session, dappStatus, @@ -19,7 +17,6 @@ export const useApproveAppRequestValidations = ({ const { connectedToHardhat, connectedToHardhatOp } = useConnectedToHardhatStore(); - // const enoughNativeAssetForGas = useHasEnoughGas(session); const enoughNativeAssetForGas = true; const buttonLabel = useMemo(() => { diff --git a/src/entries/popup/hooks/send/useSendAsset.ts b/src/entries/popup/hooks/send/useSendAsset.ts index 261cad95db..e680ee79c1 100644 --- a/src/entries/popup/hooks/send/useSendAsset.ts +++ b/src/entries/popup/hooks/send/useSendAsset.ts @@ -1,3 +1,4 @@ +import { usePortfolio } from '@orb-labs/orby-react'; import { useCallback, useMemo, useState } from 'react'; import { @@ -8,8 +9,13 @@ import { import { useUserAssets } from '~/core/resources/assets'; import { useCustomNetworkAssets } from '~/core/resources/assets/customNetworkAssets'; import { useCurrentAddressStore, useCurrentCurrencyStore } from '~/core/state'; +import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode'; import { AddressOrEth, ParsedUserAsset } from '~/core/types/assets'; import { ChainId } from '~/core/types/chains'; +import { + convertFungibleTokenAmountsToParsedUserAssets, + convertStandardizedBalanceToParsedUserAssets, +} from '~/core/utils/orb'; import { isLowerCaseMatch } from '~/core/utils/strings'; export type SortMethod = 'token' | 'chain'; @@ -23,7 +29,7 @@ const sortBy = (by: SortMethod) => { } }; -export const useSendAsset = (props: { assets?: ParsedUserAsset[] }) => { +export const useSendAsset = () => { const { currentAddress: address } = useCurrentAddressStore(); const { currentCurrency } = useCurrentCurrencyStore(); const [sortMethod, setSortMethod] = useState('token'); @@ -31,30 +37,31 @@ export const useSendAsset = (props: { assets?: ParsedUserAsset[] }) => { const [selectedAssetAddress, setSelectedAssetAddress] = useState< AddressOrEth | '' >(''); - const [selectedAssetChain, setSelectedAssetChain] = useState( - ChainId.mainnet, + const [selectedAssetChain, setSelectedAssetChain] = useState< + ChainId | undefined + >(undefined); + + const { data: assets = [] } = useUserAssets( + { + address, + currency: currentCurrency, + }, + { + select: (data) => + selectorFilterByUserChains({ data, selector: sortBy(sortMethod) }), + }, + ); + + const { data: customNetworkAssets = [] } = useCustomNetworkAssets( + { + address, + currency: currentCurrency, + }, + { + select: (data) => + selectorFilterByUserChains({ data, selector: sortBy(sortMethod) }), + }, ); - // const { data: assets = [] } = useUserAssets( - // { - // address, - // currency: currentCurrency, - // }, - // { - // select: (data) => - // selectorFilterByUserChains({ data, selector: sortBy(sortMethod) }), - // }, - // ); - - // const { data: customNetworkAssets = [] } = useCustomNetworkAssets( - // { - // address, - // currency: currentCurrency, - // }, - // { - // select: (data) => - // selectorFilterByUserChains({ data, selector: sortBy(sortMethod) }), - // }, - // ); const selectAssetAddressAndChain = useCallback( (address: AddressOrEth | '', chainId: ChainId) => { @@ -64,44 +71,67 @@ export const useSendAsset = (props: { assets?: ParsedUserAsset[] }) => { [], ); - // const combinedAssets = useMemo( - // () => - // Array.from( - // new Map( - // [...customNetworkAssets, ...assets].map((item) => [ - // item.uniqueId, - // item, - // ]), - // ).values(), - // ), - // [assets, customNetworkAssets], - // ); - - // const allAssets = useMemo( - // () => - // combinedAssets.sort( - // (a: ParsedUserAsset, b: ParsedUserAsset) => - // parseFloat(b?.native?.balance?.amount) - - // parseFloat(a?.native?.balance?.amount), - // ), - // [combinedAssets], - // ); + let combinedAssets = useMemo( + () => + Array.from( + new Map( + [...customNetworkAssets, ...assets].map((item) => [ + item.uniqueId, + item, + ]), + ).values(), + ), + [assets, customNetworkAssets], + ); + + const { testnetMode } = useTestnetModeStore(); + const { portfolio } = usePortfolio(testnetMode); + + combinedAssets = useMemo(() => { + if (!portfolio) { + return []; + } + + return convertStandardizedBalanceToParsedUserAssets(portfolio); + }, [portfolio]); + + const allAssets = useMemo( + () => + combinedAssets.sort( + (a: ParsedUserAsset, b: ParsedUserAsset) => + parseFloat(b?.native?.balance?.amount) - + parseFloat(a?.native?.balance?.amount), + ), + [combinedAssets], + ); + + const flattenedAssets = useMemo(() => { + if (!portfolio) { + return []; + } + + return convertFungibleTokenAmountsToParsedUserAssets(portfolio); + }, [portfolio]); const asset = useMemo( () => - props.assets?.find( + flattenedAssets?.find( ({ address, chainId }) => isLowerCaseMatch(address, selectedAssetAddress) && chainId === selectedAssetChain, ) || null, - [props.assets, selectedAssetAddress, selectedAssetChain], + [flattenedAssets, selectedAssetAddress, selectedAssetChain], ); return { selectAssetAddressAndChain, + setSelectedAssetAddress, + setSelectedAssetChain, asset, - assets: props.assets || [], + assets: allAssets, sortMethod, setSortMethod, + portfolio, + chainId: selectedAssetChain, }; }; diff --git a/src/entries/popup/hooks/send/useSendValidations.ts b/src/entries/popup/hooks/send/useSendValidations.ts index e1a77f0db8..8a75510149 100644 --- a/src/entries/popup/hooks/send/useSendValidations.ts +++ b/src/entries/popup/hooks/send/useSendValidations.ts @@ -1,4 +1,5 @@ import { isValidAddress } from '@ethereumjs/util'; +import { CreateOperationsStatus, OperationSet } from '@orb-labs/orby-core'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { Address } from 'viem'; @@ -7,7 +8,6 @@ import { ParsedUserAsset } from '~/core/types/assets'; import { ChainId, chainNameToIdMapping } from '~/core/types/chains'; import { GasFeeLegacyParams, GasFeeParams } from '~/core/types/gas'; import { UniqueAsset } from '~/core/types/nfts'; -import { getChain } from '~/core/utils/chains'; import { toWei } from '~/core/utils/ethereum'; import { add, @@ -26,6 +26,8 @@ export const useSendValidations = ({ selectedGas, toAddress, toAddressOrName, + operationSet, + isLoading, }: { asset?: ParsedUserAsset | null; assetAmount?: string; @@ -33,6 +35,8 @@ export const useSendValidations = ({ selectedGas?: GasFeeParams | GasFeeLegacyParams; toAddress?: Address; toAddressOrName?: string; + operationSet?: OperationSet; + isLoading?: boolean; }) => { const [toAddressIsSmartContract, setToAddressIsSmartContract] = useState(false); @@ -120,56 +124,67 @@ export const useSendValidations = ({ }, [asset?.chainId, nft, toAddress]); const buttonLabel = useMemo(() => { + if (isLoading) { + return i18n.t('send.button_label.processing'); + } + if (!isValidToAddress && toAddressOrName !== '') return i18n.t('send.button_label.enter_valid_address'); if (!toAddress && !assetAmount && !nft) { return i18n.t('send.button_label.enter_address_and_amount'); } + if (!assetAmount && !nft) { return i18n.t('send.button_label.enter_amount'); } + if (toAddressOrName === '') { return i18n.t('send.button_label.enter_address'); } - // if (!enoughAssetBalance) - // return i18n.t('send.button_label.insufficient_asset', { - // symbol: asset?.symbol, - // }); - // if (!enoughNativeAssetForGas) - // return i18n.t('send.button_label.insufficient_native_asset_for_gas', { - // symbol: getChain({ chainId: asset?.chainId || ChainId.mainnet }) - // .nativeCurrency.symbol, - // }); - return i18n.t('send.button_label.review'); + + if (operationSet?.status == CreateOperationsStatus.INSUFFICIENT_FUNDS) { + return i18n.t('send.button_label.insufficient_asset', { + symbol: asset?.symbol, + }); + } + + if (operationSet?.status == CreateOperationsStatus.NO_EXECUTION_PATH) { + return i18n.t('send.button_label.no_execution_path'); + } + + if (operationSet?.status == CreateOperationsStatus.SUCCESS) { + return i18n.t('send.button_label.review'); + } + + return i18n.t('send.button_label.processing'); }, [ - asset?.chainId, - asset?.symbol, - assetAmount, - enoughAssetBalance, - enoughNativeAssetForGas, + operationSet, + isLoading, isValidToAddress, - nft, - toAddress, toAddressOrName, + toAddress, + assetAmount, + nft, + asset?.symbol, ]); - const readyForReview = useMemo( - () => - selectedGas?.gasFee?.amount && + const readyForReview = useMemo(() => { + return ( + operationSet?.status == CreateOperationsStatus.SUCCESS && isValidToAddress && toAddressOrName !== '' && - (assetAmount || !!nft), - [ - assetAmount, - enoughAssetBalance, - enoughNativeAssetForGas, - isValidToAddress, - nft, - selectedGas?.gasFee?.amount, - toAddressOrName, - ], - ); + (!!assetAmount || !!nft) && + !isLoading + ); + }, [ + assetAmount, + isValidToAddress, + nft, + toAddressOrName, + operationSet, + isLoading, + ]); return { enoughAssetBalance, diff --git a/src/entries/popup/hooks/swap/useSwapAssets.ts b/src/entries/popup/hooks/swap/useSwapAssets.ts index af55fd0080..c2766452fb 100644 --- a/src/entries/popup/hooks/swap/useSwapAssets.ts +++ b/src/entries/popup/hooks/swap/useSwapAssets.ts @@ -1,12 +1,11 @@ +import { usePortfolio } from '@orb-labs/orby-react'; import { useCallback, useMemo, useState } from 'react'; import { selectUserAssetsList } from '~/core/resources/_selectors'; -import { - selectUserAssetsListByChainId, - selectorFilterByUserChains, -} from '~/core/resources/_selectors/assets'; -import { useAssets, useUserAssets } from '~/core/resources/assets'; -import { useCurrentAddressStore, useCurrentCurrencyStore } from '~/core/state'; +import { selectUserAssetsListByChainId } from '~/core/resources/_selectors/assets'; +import { useAssets } from '~/core/resources/assets'; +import { useCurrentCurrencyStore } from '~/core/state'; +import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode'; import { usePopupInstanceStore } from '~/core/state/popupInstances'; import { ParsedSearchAsset } from '~/core/types/assets'; import { ChainId } from '~/core/types/chains'; @@ -16,13 +15,14 @@ import { isSameAssetInDiffChains, parseSearchAsset, } from '~/core/utils/assets'; +import { convertStandardizedBalanceToParsedUserAssets } from '~/core/utils/orb'; import { SortMethod } from '../send/useSendAsset'; import { useDebounce } from '../useDebounce'; import usePrevious from '../usePrevious'; import { useSearchCurrencyLists } from '../useSearchCurrencyLists'; -const sortBy = (by: SortMethod) => { +export const sortBy = (by: SortMethod) => { switch (by) { case 'token': return selectUserAssetsList; @@ -32,7 +32,7 @@ const sortBy = (by: SortMethod) => { }; export const useSwapAssets = ({ bridge }: { bridge: boolean }) => { - const { currentAddress } = useCurrentAddressStore(); + // const { currentAddress } = useCurrentAddressStore(); const { currentCurrency } = useCurrentCurrencyStore(); const [assetToSell, setAssetToSellState] = useState< @@ -58,19 +58,15 @@ export const useSwapAssets = ({ bridge }: { bridge: boolean }) => { const { saveSwapTokenToBuy, saveSwapTokenToSell } = usePopupInstanceStore(); - const { data: userAssets = [] } = useUserAssets( - { - address: currentAddress, - currency: currentCurrency, - }, - { - select: (data) => - selectorFilterByUserChains({ - data, - selector: sortBy(sortMethod), - }), - }, - ); + const { testnetMode } = useTestnetModeStore(); + const { portfolio } = usePortfolio(testnetMode); + const userAssets = useMemo(() => { + if (!portfolio) { + return []; + } + + return convertStandardizedBalanceToParsedUserAssets(portfolio); + }, [portfolio]); const filteredAssetsToSell = useMemo(() => { return debouncedAssetToSellFilter @@ -154,10 +150,24 @@ export const useSwapAssets = ({ bridge }: { bridge: boolean }) => { const setAssetToBuy = useCallback( (asset: ParsedSearchAsset | null) => { + if (assetToSell && asset && assetToSell?.chainId != asset?.chainId) { + const relatedAssets = (assetToSell as ParsedSearchAsset)?.relatedAssets; + const newAssetToSell = relatedAssets?.find( + (relatedAsset) => relatedAsset.chainId == asset?.chainId, + ); + + if (newAssetToSell) { + setAssetToSellState({ + ...newAssetToSell, + relatedAssets, + } as ParsedSearchAsset); + } + } + saveSwapTokenToBuy({ token: asset }); setAssetToBuyState(asset); }, - [saveSwapTokenToBuy], + [assetToSell, saveSwapTokenToBuy], ); const setAssetToSell = useCallback( diff --git a/src/entries/popup/hooks/swap/useSwapValidations.ts b/src/entries/popup/hooks/swap/useSwapValidations.ts index 1f28b1beec..7b5c314f8d 100644 --- a/src/entries/popup/hooks/swap/useSwapValidations.ts +++ b/src/entries/popup/hooks/swap/useSwapValidations.ts @@ -1,3 +1,4 @@ +import { CreateOperationsStatus, OperationSet } from '@orb-labs/orby-core'; import { useMemo } from 'react'; import { i18n } from '~/core/languages'; @@ -7,28 +8,25 @@ import { GasFeeLegacyParams, GasFeeParams } from '~/core/types/gas'; import { getChain } from '~/core/utils/chains'; import { toWei } from '~/core/utils/ethereum'; import { - add, convertAmountToRawAmount, lessOrEqualThan, - lessThan, } from '~/core/utils/numbers'; -import { getNetworkNativeAssetUniqueId } from '../useNativeAssetForNetwork'; -import { useUserAsset } from '../useUserAsset'; - export const useSwapValidations = ({ assetToSell, assetToSellValue, selectedGas, + operationSet, }: { assetToSell?: ParsedSearchAsset | null; assetToSellValue?: string; selectedGas?: GasFeeParams | GasFeeLegacyParams; + operationSet?: OperationSet | null; }) => { - const nativeAssetUniqueId = getNetworkNativeAssetUniqueId({ - chainId: assetToSell?.chainId, - }); - const { data: userNativeAsset } = useUserAsset(nativeAssetUniqueId || ''); + // const nativeAssetUniqueId = getNetworkNativeAssetUniqueId({ + // chainId: assetToSell?.chainId, + // }); + // const { data: userNativeAsset } = useUserAsset(nativeAssetUniqueId || ''); const enoughAssetBalance = useMemo(() => { if (assetToSellValue) { @@ -54,22 +52,18 @@ export const useSwapValidations = ({ }, [assetToSell, assetToSellValue]); const enoughNativeAssetBalanceForGas = useMemo(() => { - if (assetToSell?.isNativeAsset) { - return lessOrEqualThan( - add(toWei(assetToSellValue || '0'), selectedGas?.gasFee?.amount || '0'), - toWei(userNativeAsset?.balance?.amount || '0'), - ); - } - return lessThan( - selectedGas?.gasFee?.amount || '0', - toWei(userNativeAsset?.balance?.amount || '0'), - ); - }, [ - assetToSell?.isNativeAsset, - assetToSellValue, - userNativeAsset?.balance?.amount, - selectedGas?.gasFee?.amount, - ]); + return operationSet?.status != CreateOperationsStatus.INSUFFICIENT_FUNDS; + // if (assetToSell?.isNativeAsset) { + // return lessOrEqualThan( + // add(toWei(assetToSellValue || '0'), selectedGas?.gasFee?.amount || '0'), + // toWei(userNativeAsset?.balance?.amount || '0'), + // ); + // } + // return lessThan( + // selectedGas?.gasFee?.amount || '0', + // toWei(userNativeAsset?.balance?.amount || '0'), + // ); + }, [operationSet]); const buttonLabel = useMemo(() => { if (!enoughAssetBalance) diff --git a/src/entries/popup/hooks/useAppSession.ts b/src/entries/popup/hooks/useAppSession.ts index f1e7d58141..3141933a2a 100644 --- a/src/entries/popup/hooks/useAppSession.ts +++ b/src/entries/popup/hooks/useAppSession.ts @@ -1,3 +1,6 @@ +import { Account, AccountType, VMType } from '@orb-labs/orby-core'; +import { removeConnectedAppSession } from '@orb-labs/orby-core-mini'; +import { connectAppSession, useOrby } from '@orb-labs/orby-react'; import * as React from 'react'; import { Address } from 'viem'; @@ -21,6 +24,8 @@ export function useAppSession({ host = '' }: { host?: string }) { getActiveSession, } = useAppSessionsStore(); + const { baseMainnetClient } = useOrby(); + const activeSession = getActiveSession({ host }); const clearAppHasInteractedWithNudgeSheet = useAppConnectionWalletSwitcherStore.use.clearAppHasInteractedWithNudgeSheet(); @@ -33,8 +38,17 @@ export function useAppSession({ host = '' }: { host?: string }) { `chainChanged:${host}`, appSessions[host].sessions[address], ); + + const account = new Account( + address?.toLowerCase(), + AccountType.EOA, + VMType.EVM, + undefined, + ); + + connectAppSession([account], host, baseMainnetClient); }, - [appSessions, host, storeUpdateActiveSession], + [appSessions, baseMainnetClient, host, storeUpdateActiveSession], ); const addSession = React.useCallback( @@ -57,8 +71,17 @@ export function useAppSession({ host = '' }: { host?: string }) { chainId: toHex(String(chainId)), }); } + + const account = new Account( + address?.toLowerCase(), + AccountType.EOA, + VMType.EVM, + undefined, + ); + + connectAppSession([account], host, baseMainnetClient); }, - [storeAddSession], + [baseMainnetClient, storeAddSession], ); const updateAppSessionChainId = React.useCallback( @@ -96,24 +119,30 @@ export function useAppSession({ host = '' }: { host?: string }) { ({ address, host }: { address: Address; host: string }) => { const newActiveSession = removeSession({ host, address }); if (newActiveSession) { + const account = new Account( + address?.toLowerCase(), + AccountType.EOA, + VMType.EVM, + undefined, + ); + + connectAppSession([account], host, baseMainnetClient); messenger.send(`accountsChanged:${host}`, newActiveSession?.address); messenger.send(`chainChanged:${host}`, newActiveSession?.chainId); } else { + removeConnectedAppSession(host); messenger.send(`disconnect:${host}`, []); - clearAppHasInteractedWithNudgeSheet({ - host: host, - }); + clearAppHasInteractedWithNudgeSheet({ host: host }); } }, - [clearAppHasInteractedWithNudgeSheet, removeSession], + [baseMainnetClient, clearAppHasInteractedWithNudgeSheet, removeSession], ); const disconnectAppSession = React.useCallback(() => { messenger.send(`disconnect:${host}`, null); removeAppSession({ host }); - clearAppHasInteractedWithNudgeSheet({ - host: host, - }); + clearAppHasInteractedWithNudgeSheet({ host: host }); + removeConnectedAppSession(host); }, [host, removeAppSession, clearAppHasInteractedWithNudgeSheet]); return { diff --git a/src/entries/popup/hooks/useAuth.tsx b/src/entries/popup/hooks/useAuth.tsx index c7af98c98d..fea0dc370f 100644 --- a/src/entries/popup/hooks/useAuth.tsx +++ b/src/entries/popup/hooks/useAuth.tsx @@ -14,6 +14,8 @@ import { SessionStorage } from '~/core/storage'; import * as wallet from '../handlers/wallet'; +import { useConnectAppSessions } from './useConnectAppSessions'; + const AuthContext = createContext({ status: 'NEW', updateStatus: () => Promise.resolve(), @@ -105,6 +107,8 @@ const useSessionStatus = () => { export function AuthProvider({ children }: { children: React.ReactNode }) { const { status, updateStatus, setStatus } = useSessionStatus(); + useConnectAppSessions(); + useEffect(() => { const listener = async (changes: { [key: string]: chrome.storage.StorageChange; diff --git a/src/entries/popup/hooks/useConnectAppSessions.ts b/src/entries/popup/hooks/useConnectAppSessions.ts new file mode 100644 index 0000000000..2e130875ef --- /dev/null +++ b/src/entries/popup/hooks/useConnectAppSessions.ts @@ -0,0 +1,110 @@ +import { Account, AccountType, VMType } from '@orb-labs/orby-core'; +import { bulkResetConnectedAppSessions } from '@orb-labs/orby-core-mini'; +import { useOrby } from '@orb-labs/orby-react'; +import { OrbyActions } from '@orb-labs/orby-viem-extension'; +import * as React from 'react'; +import { Client, HttpTransport, PublicRpcSchema } from 'viem'; + +import { useAppSessionsStore } from '~/core/state'; + +export function useConnectAppSessions() { + const { appSessions } = useAppSessionsStore(); + + const activeSessions = React.useMemo(() => { + return Array.from(Object.keys(appSessions)).map((host) => { + return { host, address: appSessions[host].activeSessionAddress }; + }); + }, [appSessions]); + + const { isLoading, isConnected } = useBulkConnectAppSessions(activeSessions); + return { isLoading, isConnected }; +} + +export function useBulkConnectAppSessions( + activeSessions: { host: string; address: `0x${string}` }[], +) { + const [isLoading, setIsLoading] = React.useState(false); + const [isConnected, setIsConnected] = React.useState(false); + const { baseMainnetClient } = useOrby(); + + // A ref to track the previous count value + const prevCountRef = React.useRef<{ host: string; address: `0x${string}` }[]>( + [], + ); + + React.useEffect(() => { + const resetConnectedAppSessions = async () => { + setIsLoading(true); + try { + if ( + activeSessions.length == 0 || + !baseMainnetClient || + prevCountRef.current.sort((a, b) => b.host.localeCompare(a.host)) == + activeSessions.sort((a, b) => b.host.localeCompare(a.host)) + ) { + return; + } + + const connected = await connectAppSessions( + activeSessions, + // @ts-ignore + baseMainnetClient, + ); + + prevCountRef.current = activeSessions.map((session) => ({ + ...session, + })); + + // Update the previous value after render + setIsConnected(connected); + } catch (error) { + console.error('Failed to reset connected app', error); + } finally { + setIsLoading(false); + } + }; + + resetConnectedAppSessions(); + }, [activeSessions, baseMainnetClient]); + + return { isLoading, isConnected }; +} + +export async function connectAppSessions( + activeSessions?: { host: string; address: string }[], + baseMainnetClient?: Client< + HttpTransport, + undefined, + undefined, + PublicRpcSchema, + OrbyActions + >, +) { + if (!activeSessions || !baseMainnetClient) { + return false; + } + + const promises = activeSessions?.map( + async ({ host, address }: { host: string; address: string }) => { + const account = new Account( + address?.toLowerCase(), + AccountType.EOA, + VMType.EVM, + undefined, + ); + + const accountCluster = await baseMainnetClient.createAccountCluster([ + account, + ]); + + return { + appUrl: host, + activeAccountClusterId: accountCluster.accountClusterId, + }; + }, + ); + + const sessions = await Promise.all(promises); + bulkResetConnectedAppSessions(sessions); + return true; +} diff --git a/src/entries/popup/hooks/useGas.ts b/src/entries/popup/hooks/useGas.ts index 9d09553d8d..638d08f6fa 100644 --- a/src/entries/popup/hooks/useGas.ts +++ b/src/entries/popup/hooks/useGas.ts @@ -253,6 +253,51 @@ const useGas = ({ const [selectedSpeed, setSelectedSpeed] = useState(defaultSpeed); + // There was an issue with gas for sending swaps and this change made it work. Infortunately, it let to an infinite re-render loop for the sending transactions. + // const gasFeeParamsBySpeed: + // | GasFeeParamsBySpeed + // | GasFeeLegacyParamsBySpeed + // | null = useMemo(() => { + // const newGasFeeParamsBySpeed = + // !isLoading && + // ((gasData as MeteorologyResponse)?.data?.currentBaseFee || + // (gasData as MeteorologyLegacyResponse)?.data?.legacy) + // ? parseGasFeeParamsBySpeed({ + // chainId, + // data: gasData as MeteorologyLegacyResponse | MeteorologyResponse, + // gasLimit: + // debouncedEstimatedGasLimit || + // getChainGasUnits(chainId).basic.tokenTransfer, + // nativeAsset: nativeAsset as ParsedAsset, + // currency: currentCurrency, + // optimismL1SecurityFee, + // flashbotsEnabled, + // additionalTime, + // }) + // : null; + // if ( + // customGasModified && + // newGasFeeParamsBySpeed && + // prevChainId === chainId + // ) { + // newGasFeeParamsBySpeed.custom = storeGasFeeParamsBySpeed.custom; + // } + // return newGasFeeParamsBySpeed; + // }, [ + // isLoading, + // gasData, + // nativeAsset, + // chainId, + // debouncedEstimatedGasLimit, + // currentCurrency, + // optimismL1SecurityFee, + // flashbotsEnabled, + // additionalTime, + // customGasModified, + // prevChainId, + // storeGasFeeParamsBySpeed.custom, + // ]); + const gasFeeParamsBySpeed: | GasFeeParamsBySpeed | GasFeeLegacyParamsBySpeed diff --git a/src/entries/popup/hooks/useInfiniteTransactionList.ts b/src/entries/popup/hooks/useInfiniteTransactionList.ts index 854a10d704..7f5ab798ba 100644 --- a/src/entries/popup/hooks/useInfiniteTransactionList.ts +++ b/src/entries/popup/hooks/useInfiniteTransactionList.ts @@ -1,3 +1,10 @@ +import { + ActivityStatus, + Category, + OperationStatus, + OperationType, +} from '@orb-labs/orby-core'; +import { useGetActivity } from '@orb-labs/orby-react'; import { useVirtualizer } from '@tanstack/react-virtual'; import { useCallback, useEffect, useMemo, useState } from 'react'; @@ -8,14 +15,12 @@ import { consolidatedTransactionsQueryKey, useConsolidatedTransactions, } from '~/core/resources/transactions/consolidatedTransactions'; -import { - useCurrentAddressStore, - useCurrentCurrencyStore, - usePendingTransactionsStore, -} from '~/core/state'; +import { fetchTransaction } from '~/core/resources/transactions/transaction'; +import { useCurrentAddressStore, useCurrentCurrencyStore } from '~/core/state'; import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode'; import { useCustomNetworkTransactionsStore } from '~/core/state/transactions/customNetworkTransactions'; import { RainbowTransaction } from '~/core/types/transactions'; +import { truncateAddress } from '~/core/utils/address'; import { useSupportedChains } from '~/core/utils/chains'; import useComponentWillUnmount from './useComponentWillUnmount'; @@ -28,18 +33,15 @@ interface UseInfiniteTransactionListParams { getScrollElement: () => HTMLDivElement | null; } -const stableEmptyPendingTransactionsArray: RainbowTransaction[] = []; - export const useInfiniteTransactionList = ({ getScrollElement, }: UseInfiniteTransactionListParams) => { const { currentAddress: address } = useCurrentAddressStore(); const { currentCurrency: currency } = useCurrentCurrencyStore(); - const pendingTransactions = usePendingTransactionsStore( - (s) => - s.pendingTransactions[address] || stableEmptyPendingTransactionsArray, - ); const [manuallyRefetching, setManuallyRefetching] = useState(false); + const [transactions, setTransactions] = useState< + RainbowTransaction[] | undefined + >(undefined); const customNetworkTransactions = useCustomNetworkTransactionsStore( (s) => s.customNetworkTransactions, @@ -75,13 +77,67 @@ export const useInfiniteTransactionList = ({ const pages = data?.pages; const cutoff = pages?.length ? pages[pages.length - 1]?.cutoff : null; - const transactions = useMemo( - () => pages?.flatMap((p) => p.transactions) || [], - [pages], - ); + + const { activity, isLoading } = useGetActivity(testnetMode); + + useEffect(() => { + const getActivity = async () => { + if (!activity) return; + + const promises = activity?.activities?.map(async (ac) => { + const final = ac.operationStatuses.find( + (status) => status.type == OperationType.FINAL_TRANSACTION, + ) as OperationStatus; + + if (!final) { + return; + } else if (!final.hash) { + return; + } else if (!final.chainId) { + return; + } else if ( + !['SUCCESSFUL', ActivityStatus.PENDING].includes(ac.overallStatus) + ) { + return; + } + + const transaction = await fetchTransaction({ + hash: final.hash as `0x${string}`, + address: address, + chainId: Number(final.chainId), + currency: currency, + }); + + let description = ''; + const formattedAddress = truncateAddress(transaction.to || '0x'); + if (ac.category == Category.SEND) { + description = `Send funds to ${formattedAddress}`; + } else if (ac.category == Category.RECEIVE) { + description = `Receive funds from ${formattedAddress}`; + } else if (ac.category == Category.SWAP) { + description = `Swap funds on ${formattedAddress}`; + } else if (ac.category == Category.REBALANCE) { + description = `Rebalance funds on ${formattedAddress}`; + } else if (ac.category == Category.BRIDGE) { + description = `Bridge funds on ${formattedAddress}`; + } else if (ac.category == Category.FUNCTION_CALL) { + description = `Calling contract ${formattedAddress}`; + } else { + description = 'Unknown'; + } + + return { ...transaction, description }; + }); + + const transactions = await Promise.all(promises); + setTransactions(transactions.filter((tx) => tx) as RainbowTransaction[]); + }; + + getActivity(); + }, [activity, address, currency]); const transactionsAfterCutoff = useMemo(() => { - const allTransactions = transactions.concat( + const allTransactions = (transactions ?? []).concat( currentAddressCustomNetworkTransactions, ); if (!cutoff) return allTransactions; @@ -98,11 +154,11 @@ export const useInfiniteTransactionList = ({ () => Object.entries( selectTransactionsByDate([ - ...pendingTransactions, + // ...pendingTransactions, ...transactionsAfterCutoff, ]), ).flat(2), - [pendingTransactions, transactionsAfterCutoff], + [transactionsAfterCutoff], ); const infiniteRowVirtualizer = useVirtualizer({ @@ -146,7 +202,7 @@ export const useInfiniteTransactionList = ({ const [lastRow] = [...rows].reverse(); if (!lastRow) return; if ( - lastRow.index >= transactions.length - 1 && + lastRow.index >= (transactions ?? []).length - 1 && hasNextPage && !isFetching && !isFetchingNextPage @@ -166,14 +222,13 @@ export const useInfiniteTransactionList = ({ fetchNextPage(); } }, [ - data?.pages?.length, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, - transactions.length, transactionsAfterCutoff.length, rows, + transactions, ]); const refetchTransactions = async () => { @@ -194,7 +249,7 @@ export const useInfiniteTransactionList = ({ return { error, fetchNextPage, - isFetching, + isFetching: isFetching || isLoading || !transactions, isFetchingNextPage, isInitialLoading, status, diff --git a/src/entries/popup/hooks/useUserAssetsBalance.ts b/src/entries/popup/hooks/useUserAssetsBalance.ts index 66a2c53881..95ae1cba90 100644 --- a/src/entries/popup/hooks/useUserAssetsBalance.ts +++ b/src/entries/popup/hooks/useUserAssetsBalance.ts @@ -1,3 +1,4 @@ +import { usePortfolioOverview } from '@orb-labs/orby-react'; import { useCallback } from 'react'; import { Address } from 'viem'; @@ -9,13 +10,18 @@ import { import { useUserAssets } from '~/core/resources/assets'; import { useCustomNetworkAssets } from '~/core/resources/assets/customNetworkAssets'; import { useCurrentAddressStore, useCurrentCurrencyStore } from '~/core/state'; +import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode'; import { computeUniqueIdForHiddenAsset, useHiddenAssetStore, } from '~/core/state/hiddenAssets/hiddenAssets'; import { ParsedUserAsset } from '~/core/types/assets'; import { ChainId } from '~/core/types/chains'; -import { add, convertAmountToNativeDisplay } from '~/core/utils/numbers'; +import { + add, + convertAmountToNativeDisplay, + convertRawAmountToDecimalFormat, +} from '~/core/utils/numbers'; export function useUserAssetsBalance(args?: { chain?: ChainId; @@ -76,10 +82,19 @@ export function useUserAssetsBalance(args?: { ? add(totalAssetsBalanceKnownNetworks, totalAssetsBalanceCustomNetworks) : undefined; + const { testnetMode } = useTestnetModeStore(); + const { portfolioOverview } = usePortfolioOverview(testnetMode); + return { amount: totalAssetsBalance, - display: totalAssetsBalance - ? convertAmountToNativeDisplay(totalAssetsBalance, currency || currentCurrency) + display: portfolioOverview + ? convertAmountToNativeDisplay( + convertRawAmountToDecimalFormat( + portfolioOverview?.totalValueInFiat?.toRawAmount()?.toString(), + portfolioOverview?.totalValueInFiat?.currency.decimals, + ), + currentCurrency, + ) : undefined, isLoading: knownNetworksIsLoading || customNetworksIsLoading, }; diff --git a/src/entries/popup/pages/home/Activity/ActivitiesList.tsx b/src/entries/popup/pages/home/Activity/ActivitiesList.tsx index 4a4cd792c4..67a2963c79 100644 --- a/src/entries/popup/pages/home/Activity/ActivitiesList.tsx +++ b/src/entries/popup/pages/home/Activity/ActivitiesList.tsx @@ -48,6 +48,7 @@ export function Activities() { isInitialLoading, isFetchingNextPage, isRefetching, + isFetching, transactions, virtualizer: activityRowVirtualizer, } = useInfiniteTransactionList({ @@ -107,7 +108,8 @@ export function Activities() { [isWatchingWallet, tokenApprovals], ); - if (isInitialLoading || isRefetching) return ; + if (isInitialLoading || isRefetching || isFetching) + return ; if (!transactions.length) return ; const rows = activityRowVirtualizer.getVirtualItems(); @@ -181,7 +183,7 @@ const ActivityDescription = ({ transaction: RainbowTransaction; }) => { const { type, to, asset } = transaction; - let description = transaction.description; + let description = transaction.description || 'felix'; let tag: string | undefined; if (type === 'contract_interaction' && to) { description = transaction.contract?.name || truncateAddress(to); diff --git a/src/entries/popup/pages/home/Header.tsx b/src/entries/popup/pages/home/Header.tsx index 70fd67e87a..b5e8d9a2f5 100644 --- a/src/entries/popup/pages/home/Header.tsx +++ b/src/entries/popup/pages/home/Header.tsx @@ -214,7 +214,7 @@ function ActionButtonsSection() { tooltipText={i18n.t('tooltip.copy_address')} /> - + /> */} void; - balance: string; }) { const { hideAssetBalances } = useHideAssetBalancesStore(); const { display: userAssetsBalanceDisplay, isLoading } = @@ -54,11 +52,11 @@ export function TabHeader({ userSelect="all" cursor="text" > - {/* {userAssetsBalanceDisplay || ''} */} - {balance || ''} + {userAssetsBalanceDisplay || ''} + {/* {balance || ''} */} ), - [activeTab, currentCurrency, hideAssetBalances, balance], + [activeTab, currentCurrency, hideAssetBalances, userAssetsBalanceDisplay], ); const tabTitle = useMemo(() => { diff --git a/src/entries/popup/pages/home/Tokens.tsx b/src/entries/popup/pages/home/Tokens.tsx index a4b1afe1de..b3c9cfc743 100644 --- a/src/entries/popup/pages/home/Tokens.tsx +++ b/src/entries/popup/pages/home/Tokens.tsx @@ -1,35 +1,25 @@ +import { AddressZero } from '@ethersproject/constants'; +import { usePortfolio } from '@orb-labs/orby-react'; import { useVirtualizer } from '@tanstack/react-virtual'; import { MotionValue, motion, useTransform } from 'framer-motion'; -import uniqBy from 'lodash/uniqBy'; import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Address } from 'viem'; import { i18n } from '~/core/languages'; import { supportedCurrencies } from '~/core/references'; import { shortcuts } from '~/core/references/shortcuts'; -import { selectUserAssetsList } from '~/core/resources/_selectors'; -import { - selectUserAssetsFilteringSmallBalancesList, - selectorFilterByUserChains, -} from '~/core/resources/_selectors/assets'; -import { useUserAssets } from '~/core/resources/assets'; -import { useCustomNetworkAssets } from '~/core/resources/assets/customNetworkAssets'; import { fetchProviderWidgetUrl } from '~/core/resources/f2c'; import { FiatProviderName } from '~/core/resources/f2c/types'; import { useCurrentAddressStore, useCurrentCurrencyStore } from '~/core/state'; import { useCurrentThemeStore } from '~/core/state/currentSettings/currentTheme'; import { useHideAssetBalancesStore } from '~/core/state/currentSettings/hideAssetBalances'; -import { useHideSmallBalancesStore } from '~/core/state/currentSettings/hideSmallBalances'; import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode'; -import { - computeUniqueIdForHiddenAsset, - useHiddenAssetStore, -} from '~/core/state/hiddenAssets/hiddenAssets'; import { usePinnedAssetStore } from '~/core/state/pinnedAssets'; import { ParsedUserAsset } from '~/core/types/assets'; -import { ChainId, ChainName } from '~/core/types/chains'; import { truncateAddress } from '~/core/utils/address'; +import { getCustomChainIconUrl } from '~/core/utils/assets'; import { isCustomChain } from '~/core/utils/chains'; +import { convertStandardizedBalanceToParsedUserAssets } from '~/core/utils/orb'; import { Box, Column, @@ -45,39 +35,34 @@ import { CoinRow } from '~/entries/popup/components/CoinRow/CoinRow'; import { Asterisks } from '../../components/Asterisks/Asterisks'; import { CoinbaseIcon } from '../../components/CoinbaseIcon/CoinbaseIcon'; +import ExternalImage from '../../components/ExternalImage/ExternalImage'; import { QuickPromo } from '../../components/QuickPromo/QuickPromo'; import useKeyboardAnalytics from '../../hooks/useKeyboardAnalytics'; import { useKeyboardShortcut } from '../../hooks/useKeyboardShortcut'; -import { useRainbowNavigate } from '../../hooks/useRainbowNavigate'; import { useSystemSpecificModifierKey } from '../../hooks/useSystemSpecificModifierKey'; -import { useTokenPressMouseEvents } from '../../hooks/useTokenPressMouseEvents'; import { useTokensShortcuts } from '../../hooks/useTokensShortcuts'; -import { ROUTES } from '../../urls'; import { TokensSkeleton } from './Skeletons'; import { TokenContextMenu } from './TokenDetails/TokenContextMenu'; import { TokenMarkedHighlighter } from './TokenMarkedHighlighter'; -import { convertFungibleTokenToParsedUserAsset } from '~/core/utils/orb'; - const TokenRow = memo(function TokenRow({ token, testId, + onClickAsset, }: { token: ParsedUserAsset; testId: string; + onClickAsset: (standardizedTokenId?: string) => void; }) { - const navigate = useRainbowNavigate(); const openDetails = () => { - navigate(ROUTES.TOKEN_DETAILS(token.uniqueId), { - state: { skipTransitionOnRoute: ROUTES.HOME }, - }); + onClickAsset(token.standardizedTokenId); }; - const { onMouseDown, onMouseUp, onMouseLeave } = useTokenPressMouseEvents({ - token, - onClick: openDetails, - }); + const isParent = useMemo( + () => token?.relatedAssets && token?.relatedAssets.length > 0, + [token], + ); return ( - - - + {isParent ? ( + + + + ) : ( + + + + )} ); }); -export function Tokens({ - scrollY, - portfolio, -}: { - scrollY: MotionValue; - portfolio: any; -}) { +export function Tokens({ scrollY }: { scrollY: MotionValue }) { const { currentAddress } = useCurrentAddressStore(); - const { currentCurrency: currency } = useCurrentCurrencyStore(); const [manuallyRefetchingTokens, setManuallyRefetchingTokens] = useState(false); - const { hideSmallBalances } = useHideSmallBalancesStore(); const { trackShortcut } = useKeyboardAnalytics(); const { modifierSymbol } = useSystemSpecificModifierKey(); const { pinned: pinnedStore } = usePinnedAssetStore(); - const { hidden } = useHiddenAssetStore(); + + const [combinedAssets, setCombinedAssets] = useState([]); + const [isInCombinedList, setIsInCombinedList] = useState< + Map + >(new Map()); const containerRef = useRef(null); const overflow = useTransform(scrollY, (p) => (p > 92 ? 'auto' : 'hidden')); - const isHidden = useCallback( - (asset: ParsedUserAsset) => { - return !!hidden[currentAddress]?.[computeUniqueIdForHiddenAsset(asset)]; - }, - [currentAddress, hidden], - ); - - const { - data: assets = [], - isFetching, - isPending, - refetch: refetchUserAssets, - } = useUserAssets( - { - address: currentAddress, - currency, - }, - { - select: (data) => - selectorFilterByUserChains({ - data, - selector: hideSmallBalances - ? selectUserAssetsFilteringSmallBalancesList - : selectUserAssetsList, - }), - }, - ); + const { testnetMode } = useTestnetModeStore(); + const { portfolio, isLoading } = usePortfolio(testnetMode); - const { - data: customNetworkAssets = [], - refetch: refetchCustomNetworkAssets, - } = useCustomNetworkAssets( - { - address: currentAddress, - currency, - }, - { - select: (data) => - selectorFilterByUserChains({ - data, - selector: hideSmallBalances - ? selectUserAssetsFilteringSmallBalancesList - : selectUserAssetsList, - }), - }, - ); + useEffect(() => { + if (!portfolio) { + return; + } - const isPinned = useCallback( - (assetUniqueId: string) => - !!pinnedStore[currentAddress]?.[assetUniqueId]?.pinned, - [currentAddress, pinnedStore], - ); + setCombinedAssets(convertStandardizedBalanceToParsedUserAssets(portfolio)); + }, [portfolio]); - const combinedAssets = useMemo( - () => - Array.from( - new Map( - [...customNetworkAssets, ...assets].map((item) => [ - item.uniqueId, - item, - ]), - ).values(), - ), - [assets, customNetworkAssets], - ); + console.log('isLoading', isLoading); - const unhiddenAssets = useMemo(() => { - return combinedAssets.filter((asset) => !isHidden(asset)); - }, [combinedAssets, isHidden]); - - const computeUniqueAssets = useCallback( - (assets: ParsedUserAsset[]) => { - const filteredAssets = assets.filter( - ({ uniqueId }) => !isPinned(uniqueId), - ); + const onCombineLists = useCallback( + (standardizedTokenId?: string) => { + if (!standardizedTokenId) { + return; + } - return uniqBy(filteredAssets, 'uniqueId').sort( - (a: ParsedUserAsset, b: ParsedUserAsset) => - parseFloat(b?.native?.balance?.amount) - - parseFloat(a?.native?.balance?.amount), + const index = combinedAssets.findIndex( + (asset) => asset.uniqueId == standardizedTokenId, ); - }, - [isPinned], - ); - - const computePinnedAssets = useCallback( - (assets: ParsedUserAsset[]) => { - const filteredAssets = assets.filter((asset) => isPinned(asset.uniqueId)); - const sortedAssets = filteredAssets.sort((a, b) => { - const pinnedFirstAsset = pinnedStore[currentAddress]?.[a.uniqueId]; - const pinnedSecondAsset = pinnedStore[currentAddress]?.[b.uniqueId]; + const parentAsset = combinedAssets[index]; - // This won't happen, but we'll just return to it's - // default sorted order just in case it will happen - if (!pinnedFirstAsset || !pinnedSecondAsset) return 0; + if (isInCombinedList.get(standardizedTokenId) == true) { + combinedAssets.splice(index + 1, parentAsset.relatedAssets!.length); + } else { + combinedAssets.splice(index + 1, 0, ...parentAsset.relatedAssets!); + } - return pinnedFirstAsset.createdAt - pinnedSecondAsset.createdAt; + setIsInCombinedList((prev) => { + prev.set( + standardizedTokenId, + !isInCombinedList.get(standardizedTokenId), + ); + return prev; }); - return sortedAssets; + setCombinedAssets([...combinedAssets]); }, - [currentAddress, pinnedStore, isPinned], - ); - - const filteredAssets = useMemo( - () => [ - ...computePinnedAssets(unhiddenAssets), - ...computeUniqueAssets(unhiddenAssets), - ], - [unhiddenAssets, computePinnedAssets, computeUniqueAssets], + [combinedAssets, isInCombinedList], ); const assetsRowVirtualizer = useVirtualizer({ - count: filteredAssets.length, + count: combinedAssets.length, getScrollElement: () => containerRef.current, estimateSize: () => 52, overscan: 10, paddingEnd: 64, paddingStart: 8, - getItemKey: (index) => filteredAssets[index].uniqueId, + // getItemKey: (index) => combinedAssets[index].uniqueId, }); useKeyboardShortcut({ @@ -252,7 +166,6 @@ export function Tokens({ type: 'tokens.refresh', }); setManuallyRefetchingTokens(true); - await Promise.all([refetchUserAssets(), refetchCustomNetworkAssets()]); setManuallyRefetchingTokens(false); } }, @@ -264,13 +177,13 @@ export function Tokens({ useEffect(() => { assetsRowVirtualizer?.measure(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [unhiddenAssets?.length]); + }, [combinedAssets?.length]); - if ((isFetching && isPending) || manuallyRefetchingTokens) { + if (isLoading || !portfolio || manuallyRefetchingTokens) { return ; } - if (!portfolio?.fungibleTokenBalances?.length) { + if (!combinedAssets?.length) { return ; } @@ -280,7 +193,7 @@ export function Tokens({ width="full" style={{ maxHeight: `1200px`, - // overflow: overflow, + overflow: overflow, }} ref={containerRef} paddingBottom="8px" @@ -306,20 +219,9 @@ export function Tokens({ }} > - {portfolio.fungibleTokenBalances.map((fungibleToken, index) => { - const token = convertFungibleTokenToParsedUserAsset(fungibleToken); - - return ( - - ); - })} - {/* {assetsRowVirtualizer.getVirtualItems().map((virtualItem) => { + {assetsRowVirtualizer.getVirtualItems().map((virtualItem) => { const { key, size, start, index } = virtualItem; - const token = filteredAssets[index]; + const token = combinedAssets[index]; const pinned = !!pinnedStore[currentAddress]?.[token.uniqueId]?.pinned; @@ -336,10 +238,14 @@ export function Tokens({ }} > {pinned && } - + ); - })} */} + })} @@ -360,10 +266,21 @@ export const AssetRow = memo(function AssetRow({ const { hideAssetBalances } = useHideAssetBalancesStore(); const { currentCurrency } = useCurrentCurrencyStore(); - const priceChange = asset?.native?.price?.change; - const priceChangeDisplay = priceChange?.length ? priceChange : '-'; - const priceChangeColor = - priceChangeDisplay[0] !== '-' ? 'green' : 'labelTertiary'; + const isParent = useMemo(() => { + if (!asset?.relatedAssets) { + return false; + } + + return asset.relatedAssets.length > 0; + }, [asset]); + + const size = useMemo(() => { + return isParent ? 36 : 24; + }, [isParent]); + + const display = useMemo(() => { + return isParent ? asset.balance.display : asset.balance.displayOnchain; + }, [asset, isParent]); const balanceDisplay = useMemo( () => @@ -376,10 +293,10 @@ export const AssetRow = memo(function AssetRow({ ) : ( - {asset?.balance?.display} + {display} ), - [asset?.balance?.display, asset?.symbol, hideAssetBalances], + [display, asset?.symbol, hideAssetBalances], ); const nativeBalanceDisplay = useMemo( @@ -425,6 +342,28 @@ export const AssetRow = memo(function AssetRow({ [name, nativeBalanceDisplay], ); + const chainsList = useMemo(() => { + const tokenChains = asset.relatedAssets?.map((token) => { + const src = getCustomChainIconUrl(token.chainId!, AddressZero); + return ( + + ); + }); + + return ( + + {tokenChains} + + ); + }, [asset]); + const bottomRow = useMemo( () => ( @@ -433,21 +372,10 @@ export const AssetRow = memo(function AssetRow({ {balanceDisplay} - - - - {priceChangeDisplay} - - - + {chainsList} ), - [balanceDisplay, priceChangeColor, priceChangeDisplay, uniqueId], + [balanceDisplay, chainsList, uniqueId], ); return ( @@ -456,6 +384,8 @@ export const AssetRow = memo(function AssetRow({ asset={asset} topRow={topRow} bottomRow={bottomRow} + size={size} + isParent={isParent} /> ); }); diff --git a/src/entries/popup/pages/home/index.tsx b/src/entries/popup/pages/home/index.tsx index 38dd8dd1a1..a1e65497c7 100644 --- a/src/entries/popup/pages/home/index.tsx +++ b/src/entries/popup/pages/home/index.tsx @@ -52,22 +52,9 @@ import { Points } from './Points/Points'; import { TabHeader } from './TabHeader'; import { Tokens } from './Tokens'; -import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode'; - -import { - useCreateClusterId, - usePortfolio, - usePortfolioBalance, - useVirtualNodeRpcUrl, - convertFungibleTokenToParsedUserAsset, -} from '~/core/utils/orb'; - const TOP_NAV_HEIGHT = 65; -const Tabs = memo(function Tabs(props: { - portfolio: any; - portfolioBalance: any; -}) { +const Tabs = memo(function Tabs() { const { trackShortcut } = useKeyboardAnalytics(); const { visibleTokenCount } = useVisibleTokenCount(); @@ -142,19 +129,13 @@ const Tabs = memo(function Tabs(props: { return ( <> - + - {activeTab === 'tokens' && ( - - )} + {activeTab === 'tokens' && } {activeTab === 'activity' && } {activeTab === 'nfts' && } {activeTab === 'points' && } @@ -171,17 +152,6 @@ export const Home = memo(function Home() { const { pendingRequests } = usePendingRequestStore(); const prevPendingRequest = usePrevious(pendingRequests?.[0]); - const { testnetMode } = useTestnetModeStore(); - - const clusterId = useCreateClusterId(currentAddress); - const virtualNodeRpcUrl = useVirtualNodeRpcUrl( - clusterId, - currentAddress, - testnetMode, - ); - const portfolio = usePortfolio(clusterId, virtualNodeRpcUrl); - const portfolioBalance = usePortfolioBalance(clusterId, virtualNodeRpcUrl); - useEffect(() => { if ( pendingRequests?.[0] && @@ -235,10 +205,10 @@ export const Home = memo(function Home() { >
- + - {/* */} + {currentHomeSheet} @@ -325,11 +295,9 @@ const TopNav = memo(function TopNav() { function TabBar({ activeTab, setActiveTab, - balance, }: { activeTab: Tab; setActiveTab: (tab: Tab) => void; - balance: string; }) { return ( void; }) => { const { testnetMode } = useTestnetModeStore(); + return ( void; - operations: any; + operationSet?: OperationSet; } const InfoRow = ({ @@ -170,23 +170,20 @@ const Overview = memo(function Overview({ }); const TransactionRoute = memo(function TransactionRoute({ - operations, + operationSet, }: { - operations: any; + operationSet?: OperationSet; }) { - console.log('operations', operations); - const inputStates = operations - ? operations.flatMap( - (operation) => operation.inputState.fungibleTokenAmounts, - ) - : []; - console.log('inputStates', inputStates); + const fungibleTokens = useMemo(() => { + return operationSet?.inputState?.getFungibleTokens(); + }, [operationSet]); + return ( Using Funds - {inputStates.map((input, i) => ( + {fungibleTokens?.map((input, i) => ( - Use {formatUnits(input.amount, input.token.currency.decimals)}{' '} - {input.token.currency.asset.symbol} from{' '} + Use {formatUnits(input.toRawAmount(), input.token.decimals)}{' '} + {input.token.symbol} from{' '} {getChain({ chainId: Number(input.token.chainId) }).name} @@ -332,14 +329,14 @@ function TransactionInfo({ dappMetadata, expanded, onExpand, - operations, + operationSet, }: { request: TransactionRequest; dappUrl: string; dappMetadata: DappMetadata | null; expanded: boolean; onExpand: VoidFunction; - operations: any; + operationSet?: OperationSet; }) { const { activeSession } = useAppSession({ host: dappMetadata?.appHost }); const chainId = activeSession?.chainId || ChainId.mainnet; @@ -391,7 +388,7 @@ function TransactionInfo({ /> - {operations && } + {operationSet && } {simulation && ( @@ -614,7 +611,7 @@ function InsuficientGasFunds({ export function SendTransactionInfo({ request, onRejectRequest, - operations, + operationSet, }: SendTransactionProps) { const dappUrl = request?.meta?.sender?.url || ''; const { data: dappMetadata } = useDappMetadata({ url: dappUrl }); @@ -627,8 +624,9 @@ export function SendTransactionInfo({ const isScamDapp = dappMetadata?.status === DAppStatus.Scam; - // const hasEnoughGas = useHasEnoughGas(activeSession); - const hasEnoughGas = true; + const hasEnoughGas = useMemo(() => { + return true; + }, []); return ( setExpanded((e) => !e)} - operations={operations} + operationSet={operationSet} /> ) : ( activeSession && ( diff --git a/src/entries/popup/pages/messages/SendTransaction/index.tsx b/src/entries/popup/pages/messages/SendTransaction/index.tsx index 0788d6c361..a30113adde 100644 --- a/src/entries/popup/pages/messages/SendTransaction/index.tsx +++ b/src/entries/popup/pages/messages/SendTransaction/index.tsx @@ -1,5 +1,10 @@ import { TransactionRequest } from '@ethersproject/abstract-provider'; -import { getAddress } from '@ethersproject/address'; +import { OperationStatus, OperationStatusType } from '@orb-labs/orby-core'; +import { + useGetOperationsToExecuteTransaction, + useOrby, +} from '@orb-labs/orby-react'; +import _ from 'lodash'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { Address } from 'viem'; @@ -9,15 +14,14 @@ import config from '~/core/firebase/remoteConfig'; import { i18n } from '~/core/languages'; import { chainsNativeAsset } from '~/core/references/chains'; import { useDappMetadata } from '~/core/resources/metadata/dapp'; -import { useFlashbotsEnabledStore, useGasStore } from '~/core/state'; +import { useFlashbotsEnabledStore } from '~/core/state'; import { useConnectedToHardhatStore } from '~/core/state/currentSettings/connectedToHardhat'; import { useFeatureFlagsStore } from '~/core/state/currentSettings/featureFlags'; import { ProviderRequestPayload } from '~/core/transports/providerRequestTransport'; import { ChainId } from '~/core/types/chains'; -import { NewTransaction, TxHash } from '~/core/types/transactions'; import { chainIdToUse } from '~/core/utils/chains'; import { POPUP_DIMENSIONS } from '~/core/utils/dimensions'; -import { addNewTransaction } from '~/core/utils/transactions'; +import { signOperation } from '~/core/utils/orb'; import { Bleed, Box, Separator, Stack } from '~/design-system'; import { triggerAlert } from '~/design-system/components/Alert/Alert'; import { TransactionFee } from '~/entries/popup/components/TransactionFee/TransactionFee'; @@ -28,19 +32,11 @@ import { useWallets } from '~/entries/popup/hooks/useWallets'; import { RainbowError, logger } from '~/logger'; import * as wallet from '../../../handlers/wallet'; +import { GasTokenInput } from '../../send'; import { AccountSigningWith } from '../AccountSigningWith'; import { SendTransactionActions } from './SendTransactionActions'; import { SendTransactionInfo } from './SendTransactionsInfo'; -import { - useCreateClusterId, - useVirtualNodeRpcUrl, - getOperationsToExecuteTransaction, - signOperationSet, - sendSignedOperations, -} from '~/core/utils/orb'; - -import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode'; interface ApproveRequestProps { approveRequest: (payload: unknown) => void; @@ -65,146 +61,127 @@ export function SendTransaction({ url: request?.meta?.sender?.url, }); const { activeSession } = useAppSession({ host: dappMetadata?.appHost }); - const selectedGas = useGasStore.use.selectedGas(); const selectedWallet = activeSession?.address || ''; const { connectedToHardhat, connectedToHardhatOp } = useConnectedToHardhatStore(); - const { asset, selectAssetAddressAndChain } = useSendAsset(); + const { selectAssetAddressAndChain } = useSendAsset(); const { watchedWallets } = useWallets(); const { featureFlags } = useFeatureFlagsStore(); - const { flashbotsEnabled } = useFlashbotsEnabledStore(); - const flashbotsEnabledGlobally = - config.flashbots_enabled && - flashbotsEnabled && - activeSession?.chainId === ChainId.mainnet; - - const { testnetMode } = useTestnetModeStore(); - - console.log('request', request); + const [selectedGasToken, setSelectedGasToken] = useState({ + name: 'no gas abstraction', + standardizedTokenId: undefined, + isDefault: true, + }); - const clusterId = useCreateClusterId(selectedWallet); - const virtualNodeRpcUrl = useVirtualNodeRpcUrl( - clusterId, - selectedWallet, - testnetMode, // testnet mode + const { flashbotsEnabled } = useFlashbotsEnabledStore(); + const flashbotsEnabledGlobally = useMemo(() => { + return ( + config.flashbots_enabled && + flashbotsEnabled && + activeSession?.chainId === ChainId.mainnet + ); + }, [activeSession?.chainId, flashbotsEnabled]); + + const txRequest = request?.params?.[0] as TransactionRequest; + + const { accountCluster, baseMainnetClient } = useOrby(); + const { operations, operationSet, virtualNode, aggregateFee, isLoading } = + useGetOperationsToExecuteTransaction( + activeSession?.address?.toLowerCase(), + activeSession?.chainId ? BigInt(activeSession.chainId) : undefined, + txRequest.to as string, + txRequest.data as string, + txRequest.value ? BigInt(txRequest.value.toString()) : undefined, + selectedGasToken.standardizedTokenId + ? { standardizedTokenId: selectedGasToken.standardizedTokenId } + : undefined, + ); + + const operationStatusesUpdated = useCallback( + ( + statusSummary: OperationStatusType, + finalTransactionStatus?: OperationStatus, + statuses?: OperationStatus[], + ) => { + if ( + operations && + activeSession && + statuses && + [OperationStatusType.SUCCESSFUL, OperationStatusType.PENDING].includes( + statusSummary, + ) + ) { + const hash = statuses[statuses.length - 1].hash; + const activeChainId = chainIdToUse( + connectedToHardhat, + connectedToHardhatOp, + activeSession.chainId, + ); + + approveRequest(hash); + + setWaitingForDevice(false); + analytics.track(event.dappPromptSendTransactionApproved, { + chainId: activeChainId, + dappURL: dappMetadata?.appHost || '', + dappName: dappMetadata?.appName, + }); + } + }, + [ + activeSession, + approveRequest, + connectedToHardhat, + connectedToHardhatOp, + dappMetadata?.appHost, + dappMetadata?.appName, + operations, + ], ); - console.log('clusterId', clusterId); - console.log('virtualNodeRpcUrl', virtualNodeRpcUrl); - - const [operations, setOperations] = useState(null); - - console.log('operations', operations); - - useEffect(() => { - console.log('in useEffect'); - const getOperations = async ({ virtualNodeRpcUrl, request }) => { - const operationSet = await getOperationsToExecuteTransaction({ - virtualNodeRpcUrl, - request, - }); - - console.log('operationSet', operationSet); - - const operations = operationSet.intents - .map((intent) => intent.intentOperations) - .flat() - ?.concat(operationSet.primaryOperation) - .filter((value) => value !== undefined && value !== null); - - console.log('operations before setting', operations); - - setOperations(operations); - }; - - if (clusterId && virtualNodeRpcUrl && request) { - const txRequest = request?.params?.[0] as TransactionRequest; - - const txData = { - value: txRequest.value || '0x0', - to: txRequest?.to ? (getAddress(txRequest?.to) as Address) : undefined, - data: txRequest.data ?? '0x', - }; - - console.log('before get operations'); - - getOperations({ virtualNodeRpcUrl, request: txData }); - } - }, [clusterId, virtualNodeRpcUrl, request]); - - // TODO: create hook for orby_getOperationsToExecuteTransaction here and display the operations - const onAcceptRequest = useCallback(async () => { if (!config.tx_requests_enabled) return; if (!selectedWallet || !activeSession) return; setLoading(true); try { - const txRequest = request?.params?.[0] as TransactionRequest; const { type } = await wallet.getWallet(selectedWallet); - console.log('txRequest', txRequest); - // Change the label while we wait for confirmation if (type === 'HardwareWalletKeychain') { setWaitingForDevice(true); } - const signedOperations = await signOperationSet(operations); - console.log('signedOperations', signedOperations); - const result = await sendSignedOperations({ - clusterId, - virtualNodeRpcUrl, - signedOperations, - }); + if (!accountCluster) { + approveRequest(null); + setWaitingForDevice(false); + return; + } else if (!accountCluster || !virtualNode || !operationSet) { + approveRequest(null); + setWaitingForDevice(false); + return; + } - console.log('result', result); + const { operationResponses, success } = + await virtualNode.sendOperationSet( + accountCluster.accountClusterId, + operationSet, + signOperation, + ); + + if (!success) { + approveRequest(null); + setWaitingForDevice(false); + return; + } - const activeChainId = chainIdToUse( - connectedToHardhat, - connectedToHardhatOp, - activeSession.chainId, + const ids = operationResponses + ?.map((op) => op.id) + .filter((id) => !_.isUndefined(id)); + baseMainnetClient?.subscribeToOperationStatuses( + ids, + operationStatusesUpdated, ); - // const txData = { - // from: selectedWallet, - // to: txRequest?.to ? (getAddress(txRequest?.to) as Address) : undefined, - // value: txRequest.value || '0x0', - // data: txRequest.data ?? '0x', - // chainId: activeChainId, - // }; - // const result = await wallet.sendTransaction(txData); - // console.log('result', result); - // if (result) { - // const transaction = { - // asset: asset || undefined, - // value: result.value.toString(), - // data: result.data, - // flashbots: flashbotsEnabledGlobally, - // from: txData.from, - // to: txData.to, - // hash: result.hash as TxHash, - // chainId: txData.chainId, - // nonce: result.nonce, - // status: 'pending', - // type: 'send', - // ...selectedGas.transactionGasParams, - // } satisfies NewTransaction; - - // addNewTransaction({ - // address: txData.from, - // chainId: txData.chainId, - // transaction, - // }); - const lastHash = - result.operationResponses[result.operationResponses.length - 1].hash; - approveRequest(lastHash); - setWaitingForDevice(false); - - analytics.track(event.dappPromptSendTransactionApproved, { - chainId: activeChainId, - dappURL: dappMetadata?.appHost || '', - dappName: dappMetadata?.appName, - }); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { showLedgerDisconnectedAlertIfNeeded(e); @@ -226,18 +203,12 @@ export function SendTransaction({ }, [ selectedWallet, activeSession, - request?.params, - connectedToHardhat, - connectedToHardhatOp, - // asset, - // flashbotsEnabledGlobally, - // selectedGas.transactionGasParams, + accountCluster, + virtualNode, + operationSet, + baseMainnetClient, + operationStatusesUpdated, approveRequest, - dappMetadata?.appHost, - dappMetadata?.appName, - clusterId, - operations, - virtualNodeRpcUrl, ]); const onRejectRequest = useCallback(() => { @@ -256,6 +227,15 @@ export function SendTransaction({ dappMetadata?.appName, ]); + const selectGasToken = useCallback( + (gasToken?: GasTokenInput) => { + if (gasToken) { + setSelectedGasToken(gasToken); + } + }, + [setSelectedGasToken], + ); + const isWatchingWallet = useMemo(() => { const watchedAddresses = watchedWallets?.map(({ address }) => address); return selectedWallet && watchedAddresses?.includes(selectedWallet); @@ -289,6 +269,14 @@ export function SendTransaction({ connectedToHardhatOp, ]); + const transactionRequest = useMemo(() => { + return request?.params?.[0] as TransactionRequest; + }, [request?.params]); + + const chainId = useMemo(() => { + return activeSession?.chainId || ChainId.mainnet; + }, [activeSession?.chainId]); + return ( @@ -313,18 +301,20 @@ export function SendTransaction({ transactionSpeedClicked: event.dappPromptSendTransactionSpeedClicked, }} - chainId={activeSession?.chainId || ChainId.mainnet} + chainId={chainId} address={activeSession?.address} - transactionRequest={request?.params?.[0] as TransactionRequest} - plainTriggerBorder + transactionRequest={transactionRequest} flashbotsEnabled={flashbotsEnabledGlobally} + selectedGasToken={selectedGasToken} + setSelectedGasToken={selectGasToken} + aggregateFee={aggregateFee} /> diff --git a/src/entries/popup/pages/messages/SignMessage/SignMessageInfo.tsx b/src/entries/popup/pages/messages/SignMessage/SignMessageInfo.tsx index bc6b60ddc8..f841cdebf5 100644 --- a/src/entries/popup/pages/messages/SignMessage/SignMessageInfo.tsx +++ b/src/entries/popup/pages/messages/SignMessage/SignMessageInfo.tsx @@ -1,5 +1,6 @@ +import { OnchainOperation, OperationSet } from '@orb-labs/orby-core'; import { AnimatePresence, motion } from 'framer-motion'; -import { useState, memo } from 'react'; +import { memo, useMemo, useState } from 'react'; import { formatUnits } from 'viem'; import { DAppStatus } from '~/core/graphql/__generated__/metadata'; @@ -27,7 +28,8 @@ import { interface SignMessageProps { request: ProviderRequestPayload; - operations: any; + operations?: OnchainOperation[]; + operationSet?: OperationSet; } function Overview({ @@ -92,23 +94,21 @@ function Overview({ } const TransactionRoute = memo(function TransactionRoute({ - operations, + operationSet, }: { - operations: any; + operations?: OnchainOperation[]; + operationSet?: OperationSet; }) { - console.log('operations', operations); - const inputStates = operations - ? operations.flatMap( - (operation) => operation.inputState.fungibleTokenAmounts, - ) - : []; - console.log('inputStates', inputStates); + const fungibleTokens = useMemo(() => { + return operationSet?.inputState?.getFungibleTokens(); + }, [operationSet]); + return ( Using Funds - {inputStates.map((input, i) => ( + {fungibleTokens?.map((input, i) => ( - Use {formatUnits(input.amount, input.token.currency.decimals)}{' '} - {input.token.currency.asset.symbol} from{' '} + Use {formatUnits(input.toRawAmount(), input.token.decimals)}{' '} + {input.token.symbol} from{' '} {getChain({ chainId: Number(input.token.chainId) }).name} @@ -129,7 +129,11 @@ const TransactionRoute = memo(function TransactionRoute({ ); }); -export const SignMessageInfo = ({ request, operations }: SignMessageProps) => { +export const SignMessageInfo = ({ + request, + operations, + operationSet, +}: SignMessageProps) => { const dappUrl = request?.meta?.sender?.url || ''; const { currentCurrency } = useCurrentCurrencyStore(); const { data: dappMetadata } = useDappMetadata({ url: dappUrl }); @@ -232,7 +236,12 @@ export const SignMessageInfo = ({ request, operations }: SignMessageProps) => { /> - {operations && } + {operations && ( + + )} diff --git a/src/entries/popup/pages/messages/SignMessage/index.tsx b/src/entries/popup/pages/messages/SignMessage/index.tsx index 8d491a139e..cfce45f13c 100644 --- a/src/entries/popup/pages/messages/SignMessage/index.tsx +++ b/src/entries/popup/pages/messages/SignMessage/index.tsx @@ -1,3 +1,6 @@ +import { OperationStatus, OperationStatusType } from '@orb-labs/orby-core'; +import { useGetOperationsToSignTypedData, useOrby } from '@orb-labs/orby-react'; +import _ from 'lodash'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { analytics } from '~/analytics'; @@ -8,6 +11,7 @@ import { useFeatureFlagsStore } from '~/core/state/currentSettings/featureFlags' import { ProviderRequestPayload } from '~/core/transports/providerRequestTransport'; import { RPCMethod } from '~/core/types/rpcMethods'; import { POPUP_DIMENSIONS } from '~/core/utils/dimensions'; +import { signOperation } from '~/core/utils/orb'; import { getSigningRequestDisplayDetails } from '~/core/utils/signMessages'; import { Bleed, Box, Stack } from '~/design-system'; import { triggerAlert } from '~/design-system/components/Alert/Alert'; @@ -21,14 +25,6 @@ import { AccountSigningWith } from '../AccountSigningWith'; import { SignMessageActions } from './SignMessageActions'; import { SignMessageInfo } from './SignMessageInfo'; -import { - signOperationSet, - useCreateClusterId, - sendSignedOperations, - useVirtualNodeRpcUrl, - getOperationsToSignTypedData, -} from '~/core/utils/orb'; -import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode'; interface ApproveRequestProps { approveRequest: (payload: unknown) => void; @@ -65,63 +61,56 @@ export function SignMessage({ const selectedWallet = activeSession?.address; - const { testnetMode } = useTestnetModeStore(); + const requestPayload = useMemo(() => { + return getSigningRequestDisplayDetails(request); + }, [request]); - // TODO: create hook for orby_getOperationsToSignTypedData here and display the operations + const { accountCluster, baseMainnetClient } = useOrby(); - const clusterId = useCreateClusterId(selectedWallet); - const virtualNodeRpcUrl = useVirtualNodeRpcUrl( - clusterId, - selectedWallet, - testnetMode, - ); - - console.log('clusterId', clusterId); - console.log('virtualNodeRpcUrl', virtualNodeRpcUrl); - - const [operations, setOperations] = useState(null); - - useEffect(() => { - console.log('in useEffect'); - const getOperations = async ({ - virtualNodeRpcUrl, - to, - data, - clusterId, - }) => { - const operationSet = await getOperationsToSignTypedData({ - to, - data, - clusterId, - virtualNodeRpcUrl, - }); - - console.log('operationSet', operationSet); - - const operations = operationSet.intents - .map((intent) => intent.intentOperations) - .flat() - ?.concat(operationSet.primaryOperation) - .filter((value) => value !== undefined && value !== null); - - console.log('operations before setting', operations); + const { operations, operationSet, virtualNode } = + useGetOperationsToSignTypedData( + request?.method == 'personal_sign' + ? '' + : JSON.stringify(requestPayload.msgData), + activeSession?.address?.toLowerCase(), + activeSession?.chainId ? BigInt(activeSession.chainId) : undefined, + ); - setOperations(operations); - }; - - if (clusterId && virtualNodeRpcUrl && request) { - console.log('before get operations'); + const operationStatusesUpdated = useCallback( + async ( + statusSummary: OperationStatusType, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _finalTransactionStatus?: OperationStatus, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _statuses?: OperationStatus[], + ) => { + if ( + requestPayload.address && + [OperationStatusType.SUCCESSFUL, OperationStatusType.PENDING].includes( + statusSummary, + ) + ) { + const result = await wallet.signTypedData( + requestPayload.msgData, + requestPayload.address, + ); - const requestPayload = getSigningRequestDisplayDetails(request); + analytics.track(event.dappPromptSignTypedDataApproved, { + dappURL: dappMetadata?.appHost || '', + dappName: dappMetadata?.appName, + }); - getOperations({ - clusterId, - virtualNodeRpcUrl, - to: requestPayload.address, - data: requestPayload.msgData, - }); - } - }, [clusterId, virtualNodeRpcUrl, request, selectedWallet]); + approveRequest(result); + } + }, + [ + approveRequest, + dappMetadata?.appHost, + dappMetadata?.appName, + requestPayload.address, + requestPayload.msgData, + ], + ); const onAcceptRequest = useCallback(async () => { const walletAction = getWalletActionMethod(request?.method); @@ -129,10 +118,8 @@ export function SignMessage({ if (!requestPayload.msgData || !requestPayload.address || !selectedWallet) return; const { type } = await wallet.getWallet(selectedWallet); - let result = null; setLoading(true); - let hash; try { // Change the label while we wait for confirmation if (type === 'HardwareWalletKeychain') { @@ -140,37 +127,50 @@ export function SignMessage({ } if (walletAction === 'personal_sign') { - result = await wallet.personalSign( + const result = await wallet.personalSign( requestPayload.msgData, requestPayload.address, ); + analytics.track(event.dappPromptSignMessageApproved, { dappURL: dappMetadata?.appHost || '', dappName: dappMetadata?.appName, }); - hash = result; - // TODO: use orby_sendSignedOperations + + approveRequest(result); } else if (walletAction === 'sign_typed_data') { - const signedOperations = await signOperationSet(operations); - console.log('signedOperations: ', signedOperations); - const result = await sendSignedOperations({ - clusterId, - signedOperations, - virtualNodeRpcUrl, - }); - console.log('result', result); - hash = result.hash; + if (!accountCluster || !virtualNode || !operationSet) { + console.error('Missing data for sign typed data'); + approveRequest(null); + return; + } - // result = await wallet.signTypedData( - // requestPayload.msgData, - // requestPayload.address, - // ); - analytics.track(event.dappPromptSignTypedDataApproved, { - dappURL: dappMetadata?.appHost || '', - dappName: dappMetadata?.appName, - }); + const { success, operationResponses } = + await virtualNode.sendOperationSet( + accountCluster.accountClusterId, + operationSet, + signOperation, + ); + + if (!success) { + console.error('Error sending operation set'); + approveRequest(null); + return; + } + + if (operationResponses && operationResponses.length === 0) { + console.error('No operation responses'); + operationStatusesUpdated(OperationStatusType.SUCCESSFUL); + } else { + const ids = operationResponses + ?.map((op) => op.id) + .filter((id) => !_.isUndefined(id)); + baseMainnetClient?.subscribeToOperationStatuses( + ids, + operationStatusesUpdated, + ); + } } - approveRequest(hash); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { showLedgerDisconnectedAlertIfNeeded(e); @@ -181,14 +181,16 @@ export function SignMessage({ setLoading(false); } }, [ - approveRequest, - dappMetadata?.appHost, - dappMetadata?.appName, request, selectedWallet, - clusterId, - virtualNodeRpcUrl, - operations, + dappMetadata?.appHost, + dappMetadata?.appName, + approveRequest, + accountCluster, + virtualNode, + operationSet, + baseMainnetClient, + operationStatusesUpdated, ]); const onRejectRequest = useCallback(() => { @@ -232,7 +234,11 @@ export function SignMessage({ flexDirection="column" style={{ height: POPUP_DIMENSIONS.height, overflow: 'hidden' }} > - + diff --git a/src/entries/popup/pages/messages/useHasEnoughGas.ts b/src/entries/popup/pages/messages/useHasEnoughGas.ts index 74cfd37af2..b83d76d57c 100644 --- a/src/entries/popup/pages/messages/useHasEnoughGas.ts +++ b/src/entries/popup/pages/messages/useHasEnoughGas.ts @@ -15,7 +15,6 @@ export const useHasEnoughGas = (session: ActiveSession) => { }); const selectedGas = useGasStore.use.selectedGas(); - console.log('nativeAsset', chainId, nativeAsset); return lessThan( selectedGas?.gasFee?.amount || '0', toWei(nativeAsset?.balance?.amount || '0'), diff --git a/src/entries/popup/pages/send/ChainInput.tsx b/src/entries/popup/pages/send/ChainInput.tsx index a0b37ebff7..b7523a8cc1 100644 --- a/src/entries/popup/pages/send/ChainInput.tsx +++ b/src/entries/popup/pages/send/ChainInput.tsx @@ -1,27 +1,26 @@ +import { BlockchainInformation } from '@orb-labs/orby-core'; +import { AnimatePresence, motion } from 'framer-motion'; import React, { useCallback, - useEffect, useImperativeHandle, - useMemo, useRef, useState, } from 'react'; -import { AnimatePresence, motion } from 'framer-motion'; -import { Box, Stack, Text, Inline, Symbol } from '~/design-system'; + +import backendNetworks from 'static/data/networks.json'; +import { Box, Inline, Stack, Text } from '~/design-system'; import { Input } from '~/design-system/components/Input/Input'; + +import { ChainIcon } from '../../components/CoinIcon/CoinIcon'; import { DropdownInputWrapper } from '../../components/DropdownInputWrapper/DropdownInputWrapper'; -import { InputActionButton } from './InputActionButton'; import { CursorTooltip } from '../../components/Tooltip/CursorTooltip'; -interface Chain { - id: number; - name: string; -} +import { InputActionButton } from './InputActionButton'; interface ChainInputProps { - selectedChain?: Chain; - availableChains: Chain[]; - onSelectChain: (chain: Chain) => void; + selectedChain?: BlockchainInformation; + availableChains?: BlockchainInformation[]; + onSelectChain: (chain: BlockchainInformation) => void; onClearSelection: () => void; onDropdownOpen: (open: boolean) => void; } @@ -31,6 +30,13 @@ interface InputRefAPI { focus: () => void; } +function getChainImage(chainId?: number) { + if (!chainId) return undefined; + + return backendNetworks.networks.find((n) => Number(n.id) === chainId)?.icons + .badgeURL; +} + export const ChainInput = React.forwardRef( function ChainInput(props, forwardedRef) { const { @@ -65,7 +71,7 @@ export const ChainInput = React.forwardRef( }, [dropdownVisible, openDropdown, closeDropdown]); const selectChainAndCloseDropdown = useCallback( - (chain: Chain) => { + (chain: BlockchainInformation) => { onSelectChain(chain); onDropdownAction(); }, @@ -88,8 +94,6 @@ export const ChainInput = React.forwardRef( /> ); - const inputVisible = !selectedChain; - return ( <> ( dropdownHeight={300} testId="chain-input" leftComponent={ - } centerComponent={ @@ -115,7 +121,7 @@ export const ChainInput = React.forwardRef( layout="position" > - {inputVisible ? ( + {!selectedChain ? ( ( > diff --git a/src/entries/popup/pages/swap/SwapTokenInput/TokenRow/TokenToSellRow.tsx b/src/entries/popup/pages/swap/SwapTokenInput/TokenRow/TokenToSellRow.tsx index b69d7a27de..52012288b6 100644 --- a/src/entries/popup/pages/swap/SwapTokenInput/TokenRow/TokenToSellRow.tsx +++ b/src/entries/popup/pages/swap/SwapTokenInput/TokenRow/TokenToSellRow.tsx @@ -18,7 +18,6 @@ import { Lens } from '~/design-system/components/Lens/Lens'; import { rowTransparentAccentHighlight } from '~/design-system/styles/rowTransparentAccentHighlight.css'; import { Asterisks } from '~/entries/popup/components/Asterisks/Asterisks'; import { CoinIcon } from '~/entries/popup/components/CoinIcon/CoinIcon'; -import { useUserAsset } from '~/entries/popup/hooks/useUserAsset'; import { RowHighlightWrapper } from './RowHighlightWrapper'; @@ -27,7 +26,6 @@ export type TokenToSellRowProps = { }; export function TokenToSellRow({ asset }) { - // const { data: asset } = useUserAsset(uniqueId); const { hideAssetBalances } = useHideAssetBalancesStore(); const { currentCurrency } = useCurrentCurrencyStore(); @@ -115,7 +113,7 @@ export function TokenToSellRow({ asset }) { - + {leftColumn} {rightColumn} From bed32b17d9cc2ee0a6b6f7830bee38f3c4fc3d22 Mon Sep 17 00:00:00 2001 From: felimadu Date: Fri, 13 Dec 2024 12:16:48 -0500 Subject: [PATCH 12/15] testing --- CHANGELOG.md | 1789 +---------------- README.md | 44 +- README_FIREFOX.md | 5 +- manifest/internal.json | 4 +- package.json | 2 +- .../TransactionFee/GasTokenMenu.tsx | 2 + .../components/TransactionRoute/index.tsx | 41 + .../useApproveAppRequestValidations.ts | 38 +- .../popup/hooks/useConnectAppSessions.ts | 95 +- .../SendTransactionActions.tsx | 26 +- .../SendTransaction/SendTransactionsInfo.tsx | 42 +- .../pages/messages/SendTransaction/index.tsx | 15 +- .../messages/SignMessage/SignMessageInfo.tsx | 51 +- .../pages/messages/SignMessage/index.tsx | 56 +- yarn.lock | 38 +- 15 files changed, 187 insertions(+), 2061 deletions(-) create mode 100644 src/entries/popup/components/TransactionRoute/index.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 17a753acb8..e7901e9912 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,1793 +21,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/) ### Testing -## [v1.5.32](https://github.com/rainbow-me/browser-extension/releases/tag/v1.5.32) +## [v0.0.1](https://github.com/orb-labs/playground-extension/releases/tag/v0.0.1) ### Added -- Degen Mode is here to make Swapping even faster. Turn it on in Swap Settings to skip the review steps #1652 - -### Changed - -- Automatically defaulting ETH as the output for Swaps, and using Max values to reduce clicks #1622 #1653 -- Swap values are now rounded to match the Rainbow App. Pasting an exact amount or Maxing will continue to function as expected #1656 -- Added support for Ham and Cronos chains in Custom Networks #1658 -- You'll now see Hardware Wallet identifiers for wallets in the Send flow #1659 - -### Fixed - -- Token balances will now update in realtime alongside relevant transactions #1649 -- Fixed an issue where Pending Transactions would sometimes disappear from Activity between submission and confirmation #1649 -- Fixed an issue where Rainbow Rewards claim confirmations were not properly rendered #1668 -- Fixed percentage difference calculation for Token charts #1660 -- Fixed an issue with keyboard navigation highlights for Rewards #1672 -- Now merging Private Keys and Wallet Groups where possible upon import #1435 -- Fixed an issue with the font for Tip highlights on Firefox #1661 - -### Internal - -- Reduced artifact noise by deprecating artifact comment links and leveraging persistent artifacts on workflow runs instead #1628 -- Added axios advisory to allowlist #1664 -- Upgraded to Swap SDK v23 #1662 -- Re-adding optional chaining for sellAsset and buyAsset on quote response, related to #1662 (#1673) -- Fix Wrapping and Unwrapping, related to #1662 (#1675) - -### Testing - -- Use beefy-runner-bx in the chrome-e2e-parallel suite #1665 - -## [v1.5.23](https://github.com/rainbow-me/browser-extension/releases/tag/v1.5.23) - -### Fixed - -- Resolved an issue with failing transactions when switching to an alternate RPC for a default network #1657 - -## [v1.5.21](https://github.com/rainbow-me/browser-extension/releases/tag/v1.5.21) - -### Added - -- Gas Speed defaults and Custom Gas settings are now available for all networks #1631 - -### Internal - -- Translations for ETH Rewards, Bridge, Approvals, and Contacts in CmdK, and Points leaderboard and referral changes #1651 - -### Changed - -- You can now more easily access Approvals and Bridging with shortcuts in the Cmd+K Magic Menu #1637 #1629 -- Transaction explorer links are now more accurate for each network, including support for RelayScan for bridges #1635 -- You can now highlight and copy text and numeric values for Tokens and Activity transactions #1644 - -### Fixed - -- Rainbow ETH Rewards claims are now properly labeled as Rainbow interactions in Activity Details #1633 -- Resolved an issue with accurate balance discovery during wallet import #1634 -- Resolved issues with RPC management for networks. You can now right-click on a custom RPC added for a particular network to remove it. Default RPCs provided by Rainbow remain persistent. #1647 -- Fixed an issue where dApps that requested the addition of an RPC could override your RPC selection. Your selection now remains persistent. #1647 -- Approval amounts are now rounded for contract approvals in Token Details #1648 -- Duplicate search results for Watched wallets and Contacts are now hidden in the Magic Menu search #1636 -- Total ETH Rewards earnings displaying in the Rewards Leaderboard are now rounded #1641 -- Resolved an issue where saved input for the Custom Token form in Network settings was not cleared after a successful token addition. #1639 -- Your weekly Points drop will now include line items for New Referrals and Referral Activity #1642 -- Resolved shortcut registration for Token Details to make shortcuts active only when menus are visible #1640 -- Amended Custom Network autofill metadata and the explorer for the PulseChain network #1638 - -### Internal - -- Properly attributing Bridges from Rainbow's ETH Rewards claim and bridge flow #1643 - -## [v1.5.14](https://github.com/rainbow-me/browser-extension/releases/tag/v1.5.14) - -### Changed - -- New networks are available in the Add Network flow, including Ronin, KavaEVM, Hedera, Merlin, zkLink, LightLink, Fusion, Bob, Karak, Core, and B² #1606 -- Testnet coverage has also expanded. Ensure that you have Developer Tools enabled to add testnets #1606 - -### Fixed - -- Improved performance, loading times, and frame drops for the Token and Activity lists for large wallets #1565 -- Improved NFT loading performance and paginated scroll #1540 -- More reliable gas estimates for supported networks #1603 -- Improved jittery Swap quote refreshes while adjusting input values and reviewing a swap #1625 -- Improved gas estimation padding for Swap quotes to reduce "out of gas" errors #1626 -- Reduced unecessary approvals for Wrappedd ETH unwrapping on L2s #1608 -- Resolved an issue with token selection scrolling in the Send flow #1630 -- No longer displaying route preferences for Bridges in Bridge Settings #1624 -- Rounding large numbers for Swap pairs while reviewing a swap #1612 -- Blocking touchpad double-tap zoom behavior when interacting with the extension #1621 -- Preventing errors on Firefox during interaction sounds when audio permissions are rejected #1620 - -### Internal - -- Upgraded `@sentry/browser` with the goal of mitigating unexpected 429 errors, and reduced sampling rate for prod builds to reduce performance unit usage #1617 -- Reduced requests for gas estimation when it's not used in transaction simulations #1618 -- Upgraded Firebase SDK to reduce Sentry errors related to `chrome.storage` clals in Ingonito mode upon initialization #1619 - -## [v1.5.8](https://github.com/rainbow-me/browser-extension/releases/tag/v1.5.8) - -### Fixed - -- Resolved an issue with the reliability of Rainbow's injected provider #1613 - -## [v1.5.7](https://github.com/rainbow-me/browser-extension/releases/tag/v1.5.7) - -### Internal - -- Reverted changes to provider for Coinbase Wallet window clash #1599 - -## [v1.5.6](https://github.com/rainbow-me/browser-extension/releases/tag/v1.5.6) - -### Changed - -- You can now search for even more tokens in the Magic Menu with Cmd+K. Just enter a token name or contract address to find it across all supported networks #1584 #1579 - -### Fixed - -- Improved rounding for ETH Rewards earnings #1600 -- Polished Rainbow ETH Rewards interface and styling #1605 -- Fixed an issue that caused a clash with Coinbase Wallet on dApps when both wallets are installed #1599 - -### Internal - -- Amended `rewards_enabled` default after ETH Rewards launch #1601 - -## [v1.5.3](https://github.com/rainbow-me/browser-extension/releases/tag/v1.5.3) - -### Fixed - -- Improved rounding for ETH Rewards earnings during Claims #1598 - -## [v1.5.2](https://github.com/rainbow-me/browser-extension/releases/tag/v1.5.2) - -### Added - -- Rainbow Points now earn you ETH Rewards. Use Ethereum, earn Rewards. Claim your ETH each week on Tuesday for free on Optimism, Base, and Zora chains. #1576 #1595 #1591 #1592 #1593 - -### Fixed - -- Resolved an issue with shortcuts not be unregistered in Token Details, clashing with keyboard shortcuts to copy text #1580 - -### Internal - -- Analytics coverage for ETH Rewards #1596 -- Added a missing state migration for chains after the refactor #1581 -- Migrated ENS endpoints, adopted static GraphQL generated clients in the codebase #1588 - -## [v1.4.131](https://github.com/rainbow-me/browser-extension/releases/tag/v1.4.131) - -### Changed - -- dApps can now leverage the `wallet_revokePermissions` RPC call to disconnect from Rainbow programatically. When disconnecting from a dApp, the wallet will now remember your preference on the next visit. #1575 - -### Fixed - -- Improved gas estimations for Custom Networks #1578 - -### Internal - -- Now fetching chain information from the backend at build-time to reduce constants that need maintenance #1564 -- Internal build configurations #1587 #1582 #1577 -- Bumping `ws` to resolve CI resolution #1583 - -## [v1.4.130](https://github.com/rainbow-me/browser-extension/releases/tag/v1.4.130) - -### Fixed - -- Resolved issue with bridge route discovery for certain native assets #1570 -- Improved metadata for Approvals to properly list Contract Addresses that have been approved #1573 -- Resolved `@grpc/grpc-js` and `braces` audit #1574 - -### Internal - -- Reducing the number of internal builds with a new commit check #1557 -- ENV cleanup, deprecated unused ENV keys #1552 - -## [v1.4.122](https://github.com/rainbow-me/browser-extension/releases/tag/v1.4.122) - -### Changed - -- You can now search for a Token Contract Address in Swaps to more quickly find a token #1561 - -### Fixed - -- Resolved gas estimation issues for Blast and Degen chain #1559 #1566 -- Fixed the `To` field in Activity Details for a transaction to better reflect a transaction recipient #1567 -- Resolved `NaN` balances displayed on the Tokens tab while reloading balances #1562 -- Fixed text overflows for alternative languages on the Tip Banner on the Tokens tab #1563 - -### Internal - -- Consolidated chains info to prepare for new Chains Metadata endpoint #1554 - -## [v1.4.111](https://github.com/rainbow-me/browser-extension/releases/tag/v1.4.111) - -### Changed - -- You can now search for Token Contract Addresses in the Magic Menu with Cmd+K to easily find and get an asset #1547 - -### Fixed - -- Resolved accuracy of Ethereum Sepolia and additional testnet gas estimates #1546 -- Fixed a crash for some browsers that attempted to translate the Onboarding flow #1548 - -### Internal - -- Translations for Magic Menu, Contacts, Report NFTs, and Speed up and Cancel Txs #1550 -- Upgraded Wagmi and Viem to v2, and included React Query #1541 - -## [v1.4.97](https://github.com/rainbow-me/browser-extension/releases/tag/v1.4.97) - -### Added - -- You can now add, remove, and manage Contacts in the Magic Menu with Cmd+K #1526 #1539 #1542 #1543 -- Instant bridging routes are now available in Rainbow for many core tokens and networks via the Relay Protocol #1531 - -### Changed - -- Search result priority ordering improvements in the Magic Menu for commands, tokens, and more #1543 -- Improved translation coverage for the Magic Menu #1537 - -### Fixed - -- Resolved an issue where pending transactions could visually overlap in Activity #1532 -- Blocking auto-translation attempts in some browsers while interacting with your Secret Recovery Phrase #1544 -- Fixed an issue with `ESC` keyboard dismissals for the Token Details card #1521 -- Fixed an issue for some dApps like Etherscan that required triggering a connection request upon switching networks #1536 -- Resolved interaction consistency with right-click menus and action menus in NFT Details #1425 - -### Internal - -- Optimized home screen renders with wrapper component for listeners #1535 -- Optimized Zustand selector use to improve performance #1513 - -## [v1.4.84](https://github.com/rainbow-me/browser-extension/releases/tag/v1.4.84) - -### Fixed - -- Fixed missing gas token names on the Transaction Previews for Degen Chain and additional L2 and L3 networks - -### Removed - -- Removed deprecated Polygon Mumbai network #1477 - -### Internal - -- Temporarily altered Rudderstack flush behavior to mitigate event drops #1533 - -## [v1.4.81](https://github.com/rainbow-me/browser-extension/releases/tag/v1.4.81) - -### Changed - -- More reliable and accurate slippage threshold estimates are now available for Swaps #1502 -- You can now navigate the Points dashboard and Weekly Drops with your Keyboard #1522 - -### Fixed - -- Resolved issues for users that experienced a missing RPC and failed transactions on Degen Chain L3 #1518 -- Fixed an issue where Speed up and Cancel transaction would not appear in the Activity list #1413 -- Improved Ledger connection reliability to reduce signing timeouts #1494 -- Resolved a UI display issue when Signing transactions for Hardware Wallets #1524 -- Fixed text overflows for long wallet names on dApp Connection prompts and Transaction Previews #1525 #1507 -- Improved performance when launching Activity Details for a transaction #1519 -- Fixed shortcuts handling on the Backup Reminders and added `esc` dismissal #1454 - -### Internal - -- Translations for new Settings flows, Transactions warnings, and more #1516 -- Replaced `migrate` util with `persistOptions` for better state versioning #1523 -- Improved Firefox release workflow #1515 - -## [v1.4.73](https://github.com/rainbow-me/browser-extension/releases/tag/v1.4.73) - -### Changed - -- Added autofill support for Redstone and Rootstock networks in the Add Network flow #1508 - -### Fixed - -- Resolved duplicate instances of tokens like `DEGEN` when Rainbow introduces rich metadata for a new network #1499 -- Resolved a white flash on launch for users in dark mode #1509 -- Improved quote price estimates for Swaps on Degen Chain #1512 - -## [v1.4.67](https://github.com/rainbow-me/browser-extension/releases/tag/v1.4.67) - -### Changed - -- Broadened coverage of our network explainers as you interact with assets across alternative L1s, and Ethereum L2s and L3s #1490 - -### Fixed - -- Resolved an issue with missing prices for some token pairs in our Swap interface #1505 - -### Internal - -- Bumped `@solana/web3.js` version to `1.90.2` to resolve CI audit issue #1503 - -## [v1.4.65](https://github.com/rainbow-me/browser-extension/releases/tag/v1.4.65) - -### Added - -- Your POAP collection is now available in the NFT Gallery #1469 -- Degen Chain is now fully supported in Rainbow. Send, Swap, and interact with Degen dApps with Rainbow #1476 - -### Changed - -- You'll now see USD or preferable currency estimates for each Transaction Preview, so that you can at a glance understand the real cost of a transaction #1481 -- Enabled Transaction Previews and simulation for Contract deployments #1485 -- It's now even easier to Speed up and Cancel a transaction #1495 -- Shortcuts on now supported for the Token Details menus #1449 - -### Fixed - -- Resolved an issue where Zora Chain would be unavailable for a subset of users after an extension upgrade #1496 -- Now hiding Transaction Simulation tabs when no result is available #1471 -- You'll now see a warning when Rainbow is missing asset prices for an attempted Swap #1498 -- Fixed an issue where transactions could appear for disabled networks in the Activity list #1474 -- Fixed an issue where dApp requests to switch chain may fail after adding a new network or RPC #1479 -- Fixed balance resolution for Custom Networks to properly display Transaction Previews instead of gas top-up requests #1486 -- Fixed dApp connection network badge positioning #1480 -- Fixed Transaction Preview network badge size #1492 -- Fixed double click text selection in Custom Gas gwei input fields #1488 -- Fixed currency pass-through in Transaction Previews so that you'll always see your preferred currency denomination #1500 -- Fixed UI for when a simulation balance and price is unavailable in Transaction Previews #1501 - -### Internal - -- Added a Zustand `migrate` util to assist sequential typed state migrations to ensure valid state for users upgrading from older versions of Rainbow #1470 -- Bumped `tar` resolution to `6.2.1` to resolve GHSA-f5x3-32g6-xq36 audit #1483 -- Bumped `protobufjs` to `7.2.5` to resolve audit #1487 -- Bumped `phin` to `3.7.1` to resolve audit #1493 - -## [v1.4.55](https://github.com/rainbow-me/browser-extension/releases/tag/v1.4.55) - -### Fixed - -- Fixed an issue with balance estimation during Sends on Polygon and BSC #1472 -- Now showing the right transaction explorer in the right-click menu on Activity #1419 -- Fixed an issue that hid failed transactions from the Activity list #1419 -- Resolved transaction display settlement issues after performing a Swap, to prevent incorrect transaction types from being displayed #1419 -- Improved currency display values in Activity Details for each transaction #1419 -- Resolved cell highlight rounding issues on the Settings menu when navigating with your keyboard #1464 -- Resolved missing asset icons in the Activity list #1468 - -### Internal - -- Amended Zora RPC url to favor new domain #1466 -- Added a migration to remove Custom RPCs added by Zora.co #1467 #1475 - -### Testing - -- Fixed test retries by moving retry logic to the runner scripts #1456 - -## [v1.4.50](https://github.com/rainbow-me/browser-extension/releases/tag/v1.4.50) - -### Fixed - -- Resolved an issue where network requests for Custom RPCs and Custom Networks were failing #1465 - -### Internal - -- Bumped `@rainbow-me/swaps` for future DEGEN chain Swap and Bridge support #1462 -- Added DeFi Positions networking requests and data handlers #1439 - -### Testing - -- Adopted Telos Tesnet for e2e Custom Networks tests because the Polygon zkEVM Testnet was unresponsive #1463 - -## [v1.4.48](https://github.com/rainbow-me/browser-extension/releases/tag/v1.4.48) - -### Added - -- Added support for Polygon Amoy testnet #1443 - -### Changed - -- Added `ETH` and `USDB` as default favorite assets for Blast #1448 -- Reduced filtering of assets for the Swap search to ensure broader asset coverage for networks like Blast #1453 - -### Fixed - -- Wallet balances will now display the correct balance after a token is hidden #1455 -- Fixed Swaps gas estimation for Blast network #1447 -- Resolved an issue that could cause the Connect Banner for dApps to appear on the lock screen #1424 -- Fixed a console error related to fetching an undefined image URL #1458 - -### Internal - -- Included refactors to unique asset ids (which were previously reverted) #1421 -- Resolved an unreleased issue related to #1421 that would cause activity parsing to fail #1452 -- Added `defi_positions_enabled` feature flag for DeFi Positions work #1440 -- Modified RPC requests to the RPC Proxy to ensure that `custom_rpc` flag is used correctly so that we favor the default Rainbow RPC where possible #1444 -- Resolved a Firebase networking error related to incorrectly fetching the Remote Config inside the background script #1457 -- Bumped `vitest` to resolve audit failure #1460 - -## [v1.4.41](https://github.com/rainbow-me/browser-extension/releases/tag/v1.4.41) - -### Added - -- You can now swap tokens on Blast and Zora with Rainbow's slick cross-chain swaps #1442 -- Right-click a spam token or a regret to hide it from your wallet. You can always search for these tokens to unhide in the future with the Magic Menu #1402 -- Blast Sepolia is now supported in Testnet Mode #1396 - -### Changed - -- Approve and Swap gas estimations are now more reliable #1395 -- Removed L2 support confirmations in Send when interacting with your own wallets #1429 -- Re-ordered Settings menus to be even cleaner for our recent additions like Approvals #1438 -- Removed automatic filtering for tokens with URLs like Ether.fi with our improved spam detection #1430 -- Supported the `return` hokey in the Save Contact flow #1398 - -### Fixed - -- Fixed ENS Profile resolution spamming utilized for the NFT Gallery that caused inadvertent performance issues #1433 -- Fixed accidental clicks when interacting with the right-click menus #1432 -- Activity Details will now properly restore after opening and closing Rainbow #1427 -- Corrected “Pin Extension” guide location for Arc Browser #1437 -- Fixed spacing in the Wallet Group selector #1436 - -### Removed - -- Removed deprecated Goerli network #1414 - -### Internal - -- Added and reverted changes to unique asset ids due to transaction fetching issues #1421 -- Reduced Imigix usage for NFT thumbnails for trusted sources #1428 -- Improved type safety of keychain manager #1434 -- Merging PKs to HD Groups wherever possible in keychain #1434 -- Migrated to Rudderstack from Segment for analytics #1410 - -## [v1.4.34](https://github.com/rainbow-me/browser-extension/releases/tag/v1.4.34) - -### Added - -- You can now bridge ETH to Blast with Rainbow #1411 - -### Fixed - -- Fixed an incorrect date calculate on the weekly Points drop breakdowns #1409 -- Fixed an issue where the Connect banner would not always appear upon switching wallets #1404 -- Resolved a color clash issue with the buttons on the Connect banners #1407 -- Fixed a crash when inspecting the Activity Details for certain airdropped tokens #1412 - -### Removed - -- Removed deprecated Arbitrum Goerli network #1397 - -### Internal - -- Limited the number of Webpack circular dependencies to 2 and resolved a handful of instances #1401 - -### Testing - -- Pinned e2e browser to Chrome 121 to mitigate Ledger dependency bundle failures in later versions of Chromedriver #1420 - -## [v1.4.25](https://github.com/rainbow-me/browser-extension/releases/tag/v1.4.25) - -### Changed - -- Improved Token Charts cursor hover and scroll feel #1394 -- Emitting `accountsChanged` upon disconnect to align with MetaMask RPC #1388 - -### Fixed - -- Resolved issue where users may only see a Cancel button on dApp requests for Custom Networks #1405 -- Fixed `sendAsync` RPC call crash #1392 -- Fixed rejection errors upon approving `wallet_watchAsset` RPC requests #1387 - -## [v1.4.22](https://github.com/rainbow-me/browser-extension/releases/tag/v1.4.22) - -### Added - -- Blast is now supported in Rainbow, with Swap and Bridge support coming soon #1379 -- You can now Pin and Unpin tokens with a hold-click to keep them at the top of your Tokens list #1370 -- Hardware Wallets can now be Unpaired from the Wallets & Keys menu in Settings #1372 - -### Changed - -- When Bridging assets, you can now right-click on a pending transaction to track the status on the Socketscan explorer #1377 -- Swaps are even faster now that we automatically populate your input token and provide a default amount #1378 -- Updated Weekly Points Overview with new line items for Referrals and Bonus Redemptions #1389 - -### Fixed - -- Resolved an issue where some wallets were missing in the Send wallet list #1368 -- Fixed an issue where the "Reveal Secret Phrase" option would be available on right-click for Hardware Wallets #1371 -- Fixed Mainnet ETH balance display duplication when Blast network is enabled #1383 -- Fixed Switch Chain errors after using `wallet_addEthereumChain` on a dApp #1365 -- Reversed arrows and colors on the Transaction Previews to be more clear about sending vs receiving #137 -- Fixed missing activity on user wallets when parsing assets #1286 -- Improved percentage formatting, fallback icons, and disappearing row on Swap Review #1380 -- Fixed Activity Details animations and inconsistencies #1384 - -### Removed - -- Removed deprecated Optimism Goerli network #1351 - -## [v1.4.12](https://github.com/rainbow-me/browser-extension/releases/tag/v1.4.12) - -### Added - -- You can now remove a Wallet Group in Settings to cleanup your unused wallets. Always make sure to backup your Secret Recovery Phrase first #1331 -- Added Custom Network autofill support for Blast and Redstone Holesky #1373 #1358 - -### Removed - -- Removed support for deprecated Zora Goerli testnet #1367 - -## [v1.4.8](https://github.com/rainbow-me/browser-extension/releases/tag/v1.4.8) - -### Added - -- Avalanche is now fully supported in Rainbow, so you'll now see any assets you own on Avalanche, and can easy Send, Swap, Bridge, or interact with Avalanche dApps right out of the box #1307 -- You can now see your current Token and NFT Approvals, and revoke each contract approval with 1-click from the Approvals menu in Settings #1322 -- NFTs can now be easily sent to another wallet from the NFT Details interface or by right-clicking on an NFT in your NFT Gallery #1323 #1334 - -### Changed - -- You can now report spam NFTs by right-clicking on an NFT or use the Report button in the drop-down menu on NFT Details #1357 -- You will now also see a `To` field on simulated transactions for simple asset transfers, instead of only seeing a contract address #1337 -- Added translations for recent improvements to Points, Networks, and NFT flows, and translated new Reset Rainbow, Approvals, and Testnet Faucet features #1356 - -### Fixed - -- Resolved an issue that required you to name a wallet when watching a new ENS address or address that resolves to an ENS name #1338 -- Increased default gas limits for Arbitrum Swaps to improve transaction success rates #1344 -- Fixed an issue for local RPCs with `10`, `192`, or `172` IP address prefixes that would be incorrectly proxied through Rainbow and requests would fail #1343 -- Fixed an issue with the vertical padding on token cells in the Send flow #1362 - -### Internal - -- Upgraded `vite`, `follow-redirects`, and statically specified `ip` version to `2.0.1` to resolve the `GHSA-78xj-cgh5-2h22` audit that is downastream of `@lavamoat/allow-scripts` #1345 #1349 -- Bumped policy updates to follow #1349 PR #1359 - -### Testing - -- e2e: Custom Networks coverage with tests for Add Network, Add Custom Network with RPC, Add Custom Testnet, Add Mainnet RPC, and Add Custom Tokens #1306 -- e2e: Send NFT coverage and fixes #1323 #1363 - -## [v1.3.78](https://github.com/rainbow-me/browser-extension/releases/tag/v1.3.78) - -### Added - -- NFTs are now supported across major Custom Networks and Testnets #1336 - -### Removed - -- Removed Base Goerli network that has been deprecated #1342 - -## [v1.3.73](https://github.com/rainbow-me/browser-extension/releases/tag/v1.3.73) - -### Added - -- Added support for the `eth_coinbase` RPC method to support dApps like Orbiter.finance #1312 - -### Changed - -- Your Rank for Points now shows a breakdown of your rank difference each week, and which direction you're trending on the leaderboard #1327 - -### Fixed - -- Fixed an issue where Activity Details would appear as a blank screen for Pending Transactions for Custom Networks #1311 -- Fixed Custom Gas shortcuts for L2s and unsupported networks #1321 -- Optimized ENS fetching network calls on the Points leaderboard #1328 -- Resolved an issue that prevent Custom Network assets from appearing in the Magic Menu search #1326 -- Fixed an issue with dApps like Blur where the dApp interface would sometimes hang upon transaction signing due to a BigNumber parsing issue #1330 -- Fixed text overflows on From and To addresses in Activity Details #1335 - -### Security - -- Upgraded Lavamoat supply chain to mitigate reported vulnerability #1339 - -### Internal - -- Disabled anonymized collection by default on Firefox #1325 -- Blocking token metadata network requests for unsupported networks to reduce backend request load #1324 - -### Testing - -- Updated Firefox Test Setup to pin specific versions of Firefox Developer Edition to mitigate `123.0b1` errors #1329 - -## [v1.3.57](https://github.com/rainbow-me/browser-extension/releases/tag/v1.3.57) - -### Added - -- You can now see your Points earnings breakdown each Tuesday with the "Your Earnings" card on the Points tabs #1317 -- Added Custom Network autofill support for Boba, Dogechain, Immutable zkEVM, Lyra, Manta, Mode, opBNB, Palm, PGN, Blast Sepolia, and RARI Chain #1302 #1316 - -### Changed - -- Wallet searching is even faster now that the search bar autofocuses when launching the wallet switcher #1299 -- When interacting with dApps that call `wallet_addEthereumChain`, Rainbow will now automatically switch to the network or RPC if you've already added it, and display a notification on the dApp #1298 -- Contracts on the Transaction Request decoding interface now has a more menu with explorer links #1314 - -### Fixed - -- Improved the reliability of Pending Transaction detection and reliability, including for Flashbots transactions #1296 -- Fixed text overflow for long NFT and NFT Collection names in NFT Details #1295 -- Resolved dApp Provider errors when interacting with 1inch Limit Orders and Curve.fi #1297 -- Resolved missing Learn more explainer link on the Flashbots toggle in Settings #1291 -- Resolved issue where Custom Gas options were incorrectly was exposed for L2 networks #1304 -- Preventing duplicate `wallet_addEthereumChain` calls for adding new networks, and now displaying errors #1298 - -### Internal - -- Added `undefined` check for our top telemetry errors related to navigation state restoration #1300 -- Bumped the Swap SDK to `0.9.1` for Avalanche support #1308 -- Allowlisted `GHSA-c24v-8rfc-w8vw` vitest vulnerability #1309 -- Upgraded Ledger packages for upstream Stax device support #1301 - -## [v1.3.42](https://github.com/rainbow-me/browser-extension/releases/tag/v1.3.42) - -### Added - -- You can now right-click on NFTs from the gallery for quick actions, including a new Hide feature #1273 #1274 -- Testnet Faucets are now presented when you run out of gas during a testnet transaction #1252 -- Reset Rainbow in Wallets & Keys in Settings now allows you to remove all of the wallets from Rainbow #1263 - -### Changed - -- Hardware wallet users attempting to sign a dApp interaction will now see a "Confirm on your device" loading indicator #1127 -- NFTs can now be refreshed with the `R` hotkey #1272 - -### Fixed - -- Resolved issue where wallet creation may fail in Wallet Details after previously removing a wallet #1254 -- Resolved Points leaderboard number formatting issues and missing icons #1259 -- Improved reliability of pending transaction status updates to prevent failed transactions from displaying as pending many hours later #1261 -- Fixed an issue that would cause duplicate wallets when importing the private key for a watched wallet #1266 -- Resolved `undefined` chain and `Insufficient ETH` errors for Custom Network dApp interactions on wallets with no assets #1275 #1271 -- Resolved an instability issue with Transaction Simulation and gas estimation when using a Custom RPC for Gashawk #1280 -- Resolved duplicate assets displayed for Custom Network assets #1281 -- Fixed the bottom padding on the Points leaderboard interface #1278 -- Resolved an invalid sender error for Custom Network transactions when the nonce is 0 #1284 -- Optimized RPC validation for autofilled networks when adding a new Custom Network #1279 -- Fixed an incorrect "Unlimited" amount displayed for large token amount transfers in the Transaction Simulation interface #1277 -- Optimized NFT image loading with low resolution placeholder thumbnails #1282 -- Resolved an issue with `signTypedData` RPC calls for the Safe app #1288 -- Fixed an issue with `chainId` discovery for Anvil RPCs #1289 -- Resolved an issue with filtering out Custom Network assets when a network is disabled #1289 -- Fixed issue with network drag-and-drop reordering in Settings #1285 -- Resolved disappearing Activity history after sending a transaction on Avalanche #1290 -- Allowing creation of wallets without specifying a wallet name #1287 -- More reliable account index increments after creating wallets #1287 - -### Internal - -- Pinned `follow-redirects` to `1.15.4` #1294 -- Upgraded `@metamask/eth-sig-util` to `7.0.1` to resolve [signing issues](https://github.com/MetaMask/metamask-mobile/issues/7792) with the Safe app #1288 -- Added anonymized tracking for Magic Menu interactions #1283 - -## [v1.3.26](https://github.com/rainbow-me/browser-extension/releases/tag/v1.3.26) - -### Added - -- Base Sepolia, and Zora Sepolia testnets are now supported #1251 - -### Fixed - -- Resolved incomplete token discovery when Custom Networks like Avalanche were enabled #1262 -- Fixed an issue where the Rainbow dApp provider would leak dependencies and sometimes conflict with dApps that relied on `lodash` #1264 - -### Internal - -- Bumped e2e packages including `chromedriver`, `geckodriver`, and `vitest` #1268 - -## [v1.3.19](https://github.com/rainbow-me/browser-extension/releases/tag/v1.3.19) - -### Fixed - -- Resolved an issue that blocked some eligible wallets with a balance from enrolling in Rainbow Points #1258 - -## [v1.3.17](https://github.com/rainbow-me/browser-extension/releases/tag/v1.3.17) - -### Added - -- Custom Networks and RPCs are now supported in Rainbow. In the Networks menu in Settings, you can add a network, switch your default RPC, or even add Custom Tokens to appear across Rainbow #1200 #1224 #1225 #1231 #1233 #1220 #1245 #1247 #1248 #1249 #1250 #1255 #1256 #1236 -- Chains like Arbitrum Nova, Polygon zkEVM, Canto, Ethereum Classic, Fantom, Moonbeam, Mantle, Metis, and Pulsechain are now pre-populated when adding a Custom Network for even easier access #1205 #1223 #1244 -- My NFTs and NFT Search is now available in the Magic Menu. NFTs are searchable by token names, token ids, collection names, and contract addresses for even faster access #1227 - -### Changed - -- `SignTypedData` is now supported by the Transaction Decoding feature for dApp interactions #1162 -- Rainbow now displays errors when Sends or Swaps fail to better diagnose gas or nonce issues and RPC failures #1199 #1210 #1240 #1232 #1243 #1238 -- When adding a new Custom Network, we now validate the RPC and display an error if it is not responding #1214 -- Improved the appearance of the Points leaderboard ranks #1229 -- Wallets without a balance now encounter an error and guide when enrolling into Rainbow Points #1230 -- Unranked users now see a different Rank card on the Points leaderboard #1237 -- Added translations for Points and Custom Networks features #1226 -- Improved NFT Gallery scroll pagination and loading indicators #1188 - -### Fixed - -- Resolved an issue with Custom Gas logic for Flashbots transactions. We now force a miner tip of `6` for Sends and dApp interactions when broadcast with the Flashbots RPC #1190 -- Improved Network selection drop-down height consistency when many networks are active #1191 -- Points earnings are now refreshed automatically with each Tuesday drop #1204 -- Resolved an issue where token prices could only be discovered for native assets on Custom Networks #1213 -- Resolved a parsing issue for Points referral codes when a URL is pasted instead of a code #1221 -- Resolved a UI consistency issue with Points referral code formatting #1228 -- Fixed an issue with Cool Mode on the Points and NFT tabs where particle effects wouldn't cease on click release #1219 -- Fixed an issue with Points onboarding for Trezor device users that require a web redirect #1211 -- Resolved a crash during Points onboarding for empty wallets #1218 -- Improved large number formatting on Points leaderboard #1222 -- Optimized the number of network calls when rendering the Points leaderboard #1212 -- Resolved an issue with NFTs disappearing when a Custom Network is active #1239 -- Resolved switch network failures on dApps after adding a Custom Network #1246 -- Fixed text clipping on Points text glow and shadows #1253 - -### Internal - -- Deprecated `useForceConnect` hook #1209 -- Refactored `displayName` usage in favor of `forwardRef` functions #1180 -- Regenerated design system symbols to reduce bundle size #1235 -- Custom RPC feature flag support #1234 -- Feature flag cleanup for Points and NFTs #1242 -- Deprecated the need for `chain id` input for Custom Networks because of auto-discovery #1257 -- Added Zora Sepolia testnet RPC to the allowlist #1202 - -## [v1.3.7](https://github.com/rainbow-me/browser-extension/releases/tag/v1.3.7) - -### Changed - -- When joining Rainbow Points, you can now easily share your referral link to X #1203 -- Explainer pop-ups can now be navigated more easily with your keyboard. Press an info button and use `tab`, `esc`, and `return` to get around even faster #1175 - -## [v1.3.6](https://github.com/rainbow-me/browser-extension/releases/tag/v1.3.6) - -### Added - -- Rainbow Points are here. True believers are always rewarded. [Learn more](https://rainbow.me/points) #1165 #1170 #1184 #1174 #1189 #1193 - -### Changed - -- The Network Changed notification on dapps can now be dismissed with a click #1192 - -### Fixed - -- Fixed an issue that would sometimes cause duplicate Polygon NFTs to appear in your gallery #1197 - -### Internal - -- Resolved an issue with NFT fetching introduced by e2e tests for NFTs #1196 - -### Testing - -- e2e coverage for NFT Gallery and NFT Details #1178 - -## [v1.3.5](https://github.com/rainbow-me/browser-extension/releases/tag/v1.3.5) - -### Internal - -- Resolved an issue with Rainbow Points feature flag coverage #1195 - -## [v1.3.0](https://github.com/rainbow-me/browser-extension/releases/tag/v1.3.0) - -### Added - -- You can now view all of your NFTs and NFT collections in Rainbow, just like the app. Head over to the new NFTs tab to view your Gallery and explore your NFTs with rich insights, including Floor Prices, Unique Owners, Rarity, and more #1182 - -### Changed - -- Drastically improved image fetching and rendering for Tokens, NFTs, and Avatars to make Rainbow feel even faster #1183 - -### Fixed - -- Resolved interface shift when loading Tokens and Token Charts #1164 -- Improved Onboarding Secret Recovery Phrase verification language #1172 - -### Internal - -- Added Floor Price Explainer for NFT Details #1169 -- Added autocomplete for a list of Networks in the Custom Networks flow #1177 #1179 -- Now providing all `chainIds` in Backend requests #1187 -- Now counting Custom Network assets in the token count, aggregated balance, and properly sorting the assets #1186 -- Resolved an issue with `chainId` conflicts for Custom Networks and the Networks menu filtering #1185 -- Upgraded `vite` to `4.5.1` to resolve CI checks #1176 - -## [v1.2.94](https://github.com/rainbow-me/browser-extension/releases/tag/v1.2.94) - -### Fixed - -- Resolved reliability issues with malicious transaction detection upon dapp requests, and improved readability of Rainbow Canary warnings #1168 -- Fixed a formatting issue with `eth_estimateGas` and `eth_gasPrice` RPC calls for dapps that could trigger an error and cause dapps to be unreliable #1163 - -### Internal - -- Added "Registered on" and "Expires in" rows in NFT Details for ENS assets #1166 -- Fixed missing Flashbots toggle store migration to improve developer workflow on rehydrations #1171 - -## [v1.2.88](https://github.com/rainbow-me/browser-extension/releases/tag/v1.2.88) - -### Added - -- Added Testnet support for Holesky, Arbitrum Sepolia, Optimism Sepolia networks #1147 - -### Changed - -- Improved translations for Magic Menu actions, and Transaction Simulation and Decoding #1160 - -### Fixed - -- Fixed dApp prompt positioning when using multiple windows and monitors #1151 -- Improved keyboard navigation consistency on dApp signature requests. Press `⌘+↵` to sign a message or transaction, `esc` to reject, and `tab` to navigate options #1148 -- Fixed an issue with native token balance resolution where you may sometimes see an incorrect "Insufficient ETH" error when switching between multiple dApps and wallets #1157 -- Fixed a rendering issue with price change estimates for Token Charts when the time window is changed #1145 -- Fixed a community reported issue where Rainbow's Firebase infrastructure would be incorrectly injected into dApps and trigger unnecessary network calls #1159 - -### Internal - -- Added support for `wallet_watchAsset` RPC call #1141 -- Added Custom RPC Settings form validation to test RPC endpoints #1144 #1241 -- Added NFT Gallery loading and empty states #1152 #1155 -- Added NFT Keyboard Navigation and sorting Shortcuts #1154 -- Added Owners and Distinct Owners fields in NFT Details #1153 -- Added ENS Profile resolution to NFT Details variant for ENS domains #1161 -- Filtering Custom Network assets in Testnet Mode #1158 -- Filtering Custom Networks from the Swap UI #1150 -- Fixed a fetching clash between NFTs and Custom RPC assets #1156 - -## [v1.2.78](https://github.com/rainbow-me/browser-extension/releases/tag/v1.2.78) - -### Added - -- Canary is now available to help you easily preview and simulate your transactions — all automatic and super fast. When Rainbow detects a transaction or dapp that appears malicious, Canary will warn you before you submit a bad transaction and lose your funds #1085 -- Added an "Enable Flashbots" toggle in the Magic Menu to submit all Mainnet transactions to Flashbots #1070 -- Added an "Export Addresses as CSV" feature in the Magic Menu to export information about your wallets to improve your wallet management workflow and help you out during Tax Season #1097 - -### Changed - -- Simplified the network support checkbox confirmations during the Send flow #1135 - -### Fixed - -- Fixed a "price not available" rendering issue on Token Charts when fetching the latest price #1143 - -### Internal - -- Added NFT Details interface for NFTs #1128 -- Added support for `wallet_addEthereumChain` RPC call #1133 -- Added transaction persistence for transaction submitted on Custom Networks #1134 -- Optimize NFT Gallery with Infinite Query refactor #1129 -- Whitelisted RPCs for Ethereum Holesky and Optimism Sepolia #1142 -- Whitelisted RPCs for Arbitrum Goerli and Arbitrum Sepolia #1139 - -### Testing - -- Disabled Testnet testing in Firefox #1137 - -## [v1.2.71](https://github.com/rainbow-me/browser-extension/releases/tag/v1.2.71) - -### Changed - -- Turned on support for Rainbow's RPC to improve transaction reliability, protect your privacy, and allow Custom Networks and RPCs in the near future #1125 -- Improved translations for Networks settings, Developer Tools and Testnet Mode, Malicious dApp warnings, Wallet management right-click menus, Clear transactions, and Points #1122 - -### Fixed - -- Resolved an issue where the About section for Tokens would sometimes be missing #1115 -- Ensuring a Swap pair prepopulates with the correct token when right-clicking a token to Swap #1118 -- Resolved an issue with Token filtering when a chain was hidden in Networks settings #1124 -- Fixed an issue with Send flow for Testnets that would cause native asset transfer transactions to be malformed #1130 -- Resolved an issue where EIP-712 signature requests did not trigger a Dapp Prompt when `chainid` was unavailable #1121 -- Fixed a UI overflow in Activity for transactions with lengthy names #1116 - -### Internal - -- Added NFT Gallery and NFT sorting feature #1082 -- Added low-level Custom Network support #1071 -- Added Custom Network asset and price, symbol discovery #1120 #1111 -- Added Asset Price and Chart fetching, and Chain icons for Custom Networks #1123 -- Bumped `chromedriver` from `119.0.0` to `119.0.1` #1126 -- Bumped `axios` to `1.6.1` #1131 - -## [v1.2.64](https://github.com/rainbow-me/browser-extension/releases/tag/v1.2.64) - -### Added - -- Testnet Mode is now available to safely interact with testnets and view testnet assets. Enable Developer Tools today in the Networks menu in Settings or with the Magic Menu, and toggle Testnet Mode with the `t` hotkey or by using the menu at the top-right of the wallet interface (#1057, #1113, #1109, #1106, #1108, #1110) -- When attempting to sign a mainnet transaction while Testnet Mode is active, you will now encounter a warning for extra workflow safety #1083 - -### Changed - -- Network Settings are now accessible through the Magic Menu #1108 - -### Fixed - -- Resolved an issue where the Network Changed notification had stopped appearing in dapps #1101 -- Fixed an overflow issues in the Token Right-click menu for tokens with long names #1098 -- Fixed missing strings for the Fee on Transfer Token explainer in Swaps #1084 -- Improvements to various strings and translations thanks to community feedback #1081 - -### Internal - -- Developer settings for Custom RPCs and Custom Assets for the upcoming Custom Networks feature (#1090, #1100) -- Merged the groundwork for NFT support with the `useNfts` fetch hook #1067 -- Refactored local storage usage with a wrapper and logging to investigate storage quota overflows #1102 -- Patching [GHSA-xwcq-pm8m-c4vf](https://github.com/advisories/GHSA-xwcq-pm8m-c4vf) with `crypto-js` resolution for the Ledger integration #1091 -- Patching vulnerabilities by bumping `yaml` and `browserify-sign` #1099 -- Upgraded SF Symbols to v5 #1108 -- Upgraded `chromedriver` for the latest Chrome support #1107 - -### Testing - -- Expanded end-to-end test coverage for networks and Testnet Mode #1075 -- Adopting `findByTestId` in e2e tests for reliability #1093 - -## [v1.2.56](https://github.com/rainbow-me/browser-extension/releases/tag/v1.2.56) - -### Fixed - -- Resolved a crash when quickly switching tabs #1089 -- Fixed tab selection and restoration when using Rainbow in Full Screen #1089 - -## [v1.2.54](https://github.com/rainbow-me/browser-extension/releases/tag/v1.2.54) - -### Added - -- A preview of Rainbow Points is here. True believers are always rewarded. #1078 -- Users can now clear troublesome pending transactions with a new button in the Transactions menu in Settings #1072 - -### Changed - -- Right-click is now available in the Wallets & Keys menu in Settings to make wallet management even easier #1054 - -### Fixed - -- Resolved an issue where Rainbow would not be available to dapps that supports EIP-6963 or `window.ethereum.providers` when the default wallet toggle was disabled #1079 - -## [v1.2.48](https://github.com/rainbow-me/browser-extension/releases/tag/v1.2.48) - -### Added - -- A new Networks menu is available in Settings to hide networks and their assets throughout Rainbow. Yon can also drag to reorder networks to access your favorite chains even faster #1044 -- Users will now encounter warnings when attempting to connect to or sign transactions and messages on dapps that appear to be malicious and harmful #1051 -- Korean, Thai, and Arabic language support is now available in Settings #1052 - -### Changed - -- Improved the consistency and reliability of the switch wallet banner when viewing a different wallet than what is currently connected to the dapp #996 -- Swaps are even faster now with 1 less click when selecting a swap pair #1060 -- The network switcher dropdown filter in Swaps can now be navigated using the keyboard #1043 -- Enhanced error handling and crashes with a new interface to report issues directly to the Rainbow team #1042 - -### Fixed - -- Corrected an issue where the network switcher for dapps in the top-left displayed inaccurate numbered hotkeys #1053 -- Fixed an issue where some token bridging routes would appear in the Bridge feature for unsupported networks #1055 -- Improved language strings throughout the Swap and Bridge interface #1059 -- Implemented a fallback dapp icon for dapps without a favicon or for icons that fail to load #1050 -- Removed a log that appeared while rendering external images #1068 - -### Internal - -- Introduced public provider routing for RPC calls, and added support for Rainbow's RPC for future use. This is not currently enabled in production #1058 - -### Testing - -- e2e: testing shortcuts in Swaps #1041 -- e2e: Optimism L2 Sends are now covered with tests #1048 - -## [v1.2.39](https://github.com/rainbow-me/browser-extension/releases/tag/v1.2.39) - -### Added -- Bridging is easier than ever when right-clicking on a token or choosing to Bridge from the Token Details pane #997 -- You can now connect to all major Testnets for Rainbow's supported networks to sign and send testnet transactions. When connecting to or switching networks from a dApp that supports Testnets, you'll automatically be connected to the correct testnet by default, without needing to manage an RPC or network list. #1027 - -### Changed -- Right-click is now available in even more places, like the wallet header to manage your wallet, Send, Swap, the Wallet Switcher, and the Wallets & Keys menu in the Settings interface #1017 #1004 -- The default tab when opening the wallet is now Tokens instead of the Activity pane #1029 -- Links within descriptions for Tokens are now clickable #1019 -- Support for keyboard navigation within the Custom Gas menu #1021 -- Support for keyboard navigation in the Token Details pane #1013 -- Support for keyboard navigation within Swap Settings -- Improved keyboard navigation for the Flip Assets feature in Swap #1023 - -### Fixed -- Improved handling of hex-encoded signature requests for `personal_sign` support for Ledger and Trezor hardware wallets #1035 -- Resolved an issue where Rainbow over-fetched metadata for dApps as users navigated the web. Rainbow now only fetches metadata for dApps that first interact with the Rainbow RPC provider #1038 -- Allowance field no longer appears in Token Details if there is no contract approval allowance to display #1045 -- `ESC` hotkey can now be used to close Token Details #1058 -- Fixed image clipping for token images and NFT previews in the Activity transactions list #1040 -- Improved spacing in the `Del` shortcut hint bubble for the menu to Cancel or Speed up a transaction #1030 - -### Security -- Deprecated support for dApps that rely on the vulnerable `eth_sign` method for signatures #1049 -- Sanitizing fields in EIP-712 signature requests to mitigate common phishing attacks #1028 - -### Internal -- Added debounce to some fee calculations in Send flow #1032 -- Stricter Sentry filtering on `beforeSend` #1036 -- Reduce price/coingecko queries in swap search list #1033 - -### Testing -- e2e: Send Shortcut test #1002 -- e2e: shortcut wallet switcher test #1037 - - -## [v1.2.36](https://github.com/rainbow-me/browser-extension/releases/tag/v1.2.36) - -### Added - -- When hovering over icons and buttons throughout Rainbow, you will now see tooltips that tell you more about the feature and highlight keyboard shortcuts #930 - -### Changed - -- Filtering unverified asset results in Swap Search for short search strings to speed up search #1001 -- Improvements to Wallet Switcher banner logic #996 -- Support for keyboard navigation in Token Details #1013 - -### Fixed - -- Filtering imported Private Keys from Wallet Group creation flow #988 -- Fixed a scenario where you couldn’t imported the Secret Recovery Phrase for Watched Wallets #987 -- Restored shortcut hints on the Tokens Right-click menu #1011 -- Fixed an issue where the MATIC symbol was missing during the Swap flow when the user did not already own that asset #990 -- Resolved an issue with EIP-6963 support where we announce the provider before attempting EIP-1193 `window.ethereum` injection, which can fail #994 -- Resolved a crash during provider injected that prevented in-dApp notifications during network switching #1005 -- Resolved duplicate inpage id logs during provider injection #999 -- Prefetching dApp metadata before interacting with a dApp’s prompts #1009 -- Fixed inaccuracies with exchange rate field for transactions in Activity Details #1022 -- Fixed an issue with Layer 2 network Sends that caused transaction failures during internal testing due to invalid max priority fees #1025 -- Resolved an issue where the Wallet Details drop-down could overlap with the Rename Wallet modal #1010 -- Fixed background color inconsistencies in the Wallet Selection step of Import during Onboarding #1006 -- Truncating lengthy ENS names on Activity Details #944 -- Improved Context Menu sizing reliability #973 -- Improved layered context menus animations and sizing in the dApp Menu #1018 - -### Internal - -- Added a dev setting to clear nonces #993 -- Cryptography library upgrades beyond Ethers v5, and evaluating path to Viem upgrade #1007 -- Refactored AccentColorProvider component, and deprecated AccentColorProviderWrapper #1003 -- Updated i18n translations #998 -- Resolved an issue with how we count imported wallets in tracking #989 -- Resolved an issue with the Publish GitHub Action #1024 -- Upgraded to Node 18 from 16 for Firefox support #1020 -- Upgraded `deep-object-diff` to resolve dependency audit issues #1000 -- Upgraded `vite`, `vitest`, and `chai` #1015 - -### Testing -- e2e coverage of Home shortcuts #942 - -## [v1.2.26](https://github.com/rainbow-me/browser-extension/releases/tag/v1.2.26) - -### Added - -- You can now Buy crypto from our On-ramp Partners with the Buy button, and Transfer Crypto for free from Coinbase for new wallets #957 #976 -- Legacy Ledger HD derivation paths are now supported via a drop-down option while pairing your Ledger device #929 -- Indonesian language support is now available in Settings #937 -- You will now see improvements to dApp naming and other metadata, tailored by the Rainbow team. Let us know if we missed any! #955 -- Rainbow is now open source and licensed under GPLv3 #965 - -### Changed - -- Rainbow has a new look and feel with a new Tab Bar to navigate between Activity, Tokens, and NFTs #962 -- The Command K interface is now the Magic Menu and is available as `⌘K` on macOS, `Ctrl-K` on Windows, and simply `k` across all platforms #982 -- The nudge to Switch Wallets while interacting with a dApp has a new look and feel, and now appears immediately for an even faster switching experience #980 -- You can now disable Sounds with a toggle in Settings #953 -- Keyboard navigation now fully supports the Onboarding flow #924 -- Context menus are now dynamically sized for better localization support #949 - -### Fixed - -- Resolved an issue where connections with Ledger devices was unreliable and blocked interactions like Swaps #983 -- Resolved an issue where the Create Wallet action in the Magic Menu incorrectly routed to the Welcome screen #959 -- Handling a scenario where Home actions wouldn't appear when fetches for an ENS avatar failed #946 -- Fixed a crash when dApp session data is temporarily unavailable #947 -- Fixed a crash on the Lock screen that periodically prevented users from unlocking the extension #947 -- Fixed a crash on the Wallet Details interface when removing a Secret Recovery Phrase attached to multiple wallets #947 -- Improved transaction pagination reliability in Activity for wallets with many filtered transactions, including ProtocolRewards events #960 -- Resolved an issue where keyboard navigation to the Token Details interface clashed and opened the right-click menu #974 -- Adjusted the logic for Backup Reminders to rely on when a user confirms "I've saved these words" #978 -- Prevent resizing animation jitters on presentation of Activity Details with loading skeletons for affected rows #945 -- Resolved a display issue for cross-chain swaps on the Activity pane #948 -- Resolved an issue where you could use the `s` hotkey on Watched wallets #954 -- Resolved a clash where the new Tab Bar would appear over Pending Transaction prompts #979 -- Fixed an alignment inconsistency for Activity transaction cells #969 -- Improved icon fallbacks for NFTs that fail to load for Activity transactions #970 -- Favoring a dApp's hostname when other metadata is unavailable on prompts #971 -- Fixed copy for Stronger Password recommendations during Onboarding #975 -- Resolved an issue where wallet avatars could be clipped when using an Emoji #977 -- Resolved issues with the border radius of remotely fetched images throughout the interface #981 - -### Removed - -- Rainbow is now available to all without an invite code. We've removed this step from Onboarding #963 - -### Internal - -- Refactored the initial load of i18n strings on cold boot #950 -- Refactored wallet avatar fetching and added a cache #958 -- Improved fallbacks and handling of Backend-driven transaction types for contract interaction Activity cells #972 -- Updated i18n translations for Activity Details #951 -- Improved Sentry logging for Send, Import, and Hardware Wallet pairing keychain interactions #961 -- Publish actions are now seperate for Chrome and Firefox stores #964 -- Upgraded `@ledgerhq/hw-app-eth` and `@ledgerhq/hw-transport-webhid` and accompanying patches and Wekpack configuration #983 -- Upgraded `@metamask/eth-sig-util`, `@trezor/connect-plugin-ethereum`, `@ledgerhq/cryptoassets`, `wagmi`, `zustand`, Sentry, Webpack, and LavaMoat packages #956 - -## [v1.2.21](https://github.com/rainbow-me/browser-extension/releases/tag/v1.2.21) - -### Added - -- Transactions are now even easier to read with rich metadata when scrolling through your Activity list. You can also see detailed transaction information when clicking on each transaction #853 -- Support for EIP-6963 to improve the dApp connection experience and mitigating `window.ethereum` namespace conflicts and overrides for users with multiple active extensions #926 -- Reminders to backup Secret Recovery Phrases for users that have created a new wallet and skipped the backup and quiz verification #931 - -### Changed - -- Wallet switching is faster than ever. Give it a try by using the numbered hotkeys to rapidly switch between wallets in your list #940 -- After sending a Swap or Send, you'll now jump right to the Activity tab to follow the transaction progress #936 - -### Fixed - -- Fixed a crash on Zora and Base when sending transactions because of invalid the Optimism L1 Security fees #933 -- Resolved an issue with the Immediate auto-lock preference, where the wallet would not always immediately lock, and users could get stuck in an auto-lock loop after attempting to unlock #939 -- Fixed an issue where the first submitted transaction for a generated wallet would not properly appear as pending #936 -- Fixed an issue where Swaps and Sends were persisted by state restoration even after a successful transaction submission #935 -- Resolved an issue where the slippage percentage for Swaps would sometimes appear as `-Infinity%` or `NaN%` #938 -- Resolved an issue with USD estimates for Swap quotes on a subset of networks #934 -- Resolved a theme conflict for WETH tokens in dark mode #928 - -### Internal - -- Improved Swap analytics event with standardized `tradeAmountUSD` values returned from the Quote API #927 -- Refactoring the inpage provider injection script and deprecating the `window.ethereum.providers` proxy #919 -- Bundle size optimizations #932 -- Type safety improvements with `AddressOrEth` type #896 - -### Testing - -- Helpers for shortcut e2e tests #941 -- Resolved an e2e test failure within the Send flow #943 - -## [v1.2.13](https://github.com/rainbow-me/browser-extension/releases/tag/v1.2.13) - -### Changed -- Introduced a transaction submission loading spinner for the "Send" button in the Send feature, mirroring the behavior in Swaps #916 -- Implemented an explainer for tokens that aren't supported in Swaps because their contract requires a fee on transfer #917 - -### Fixed -- Resolved an issue with Rainbow's "default wallet" toggle behavior for users that interacted with dApps across multiple tabs simultaneously #906 -- The dApp and More menus on the core wallet screen can now be toggled properly with the `n` and `.` hotkeys #913 -- Resolved a conflict where the `w` hotkey for the Wallet Switcher could be triggered while the dApp menu is active #913 - -### Internal - -- Resolved an issue with development hot reloading #920 - -### Testing - -- Improvements to e2e reliability, and disabling unstable tests on Firefox #921 - -## [v1.2.11](https://github.com/rainbow-me/browser-extension/releases/tag/v1.2.11) - -### Changed -- Temporarily disabled hardware wallet support on Firefox due to browser restrictions #907 -- Enhanced internalization support for multi-lingual store listings on the Chrome Web Store #895 - -### Fixed -- Resolved an issue where some pending transactions would get stuck in the Activity list or transaction nonces would be incorrect for users simultaneously sending transactions across multiple wallets or networks #914 -- Improved transaction gas estimation reliability on Base, Arbitrum, Optimism, and Zora with Rainbow estimates #911 -- Improved gas estimation support for OP Stack chains with an L1 security fee #911 -- Improved behavior on dApps that use Web3Modal by supporting the expected `window.ethereum.providers` provider ordering #910 - -### Internal -- Upgraded to TypeScript 5 to enhance type safety #890 - -### Testing -- Shortcuts support for end-to-end tests #899 -- Addressed timeouts in end-to-end testing to foster more consistent results #909 - -## [v1.2.3](https://github.com/rainbow-me/browser-extension/releases/tag/v1.2.3) - -### Added - -- When accidentally closing the extension while preparing a Swap or Send or managing your wallets in Settings, Rainbow will now restore your state and let you continue where you left off #878 -- You can now Disconnect (`D`) your wallet or Switch Wallet (`W`) to change the wallet connected to a dApp from the dApp Menu in the top-left. You can always open this menu with the `N` shortcut, and follow the shortcut hints to drill-down without touching your mouse #866 - -### Changed - -- Flashbots support now includes more builders for faster transaction inclusion #903 -- Provider injection is now available for Firefox for a future release #859 - -### Fixed - -- Resolved issues with nonce management and transaction submission reliability for users that interact across multiple wallets and networks simultaneously #891 -- Fixed an issue with the Activity pane when using Rainbow in Full Screen mode #889 -- Resolved transaction caching issues on the Activity pane that caused unnecessary network refreshes #905 -- Fixed a potential crash when fetching output-based Swap quotes #887 -- Improved anonymized logging to better diagnose wallets with keychain store issues #892 - -## [v1.1.79](https://github.com/rainbow-me/browser-extension/releases/tag/v1.1.79) - -### Added - -- Command K (⌘K or Ctrl+K) is now available to search and launch screens, toggle settings, search your wallets and tokens, and search ENS and public addresses to quickly watch a new wallet. ⌘⏎ or ⇧⏎ are available to expose additional actions for wallets and tokens. Press ESC to go back or close. #869 -- dApp Account Switch functionality when switching wallets while interacting with a dApp #845 #863 #860 -- Language option in Settings and internationalization support for Latin American Spanish, Simplified Chinese, Japanese, French, Brazilian Portuguese, Hindi, Turkish, and Russian #817 - -### Changed - -- State restoration for the Swap and Send flows so that user selections and input are sticky for a short period of time for when you need to close the pop-up or back-out to copy an address #852 -- Paginated scroll and loading indicators on the Tokens and Activity interfaces for heavy wallets #880 -- Analytics for anonymized metrics on the types of wallets our users interface with in Rainbow #805 - -### Fixed - -- Resolved regression in Right-click support for Tokens #865 -- Fixed an issue with dApps that support `window.ethereum.providers` when Rainbow is toggled as the default browser wallet #867 -- Resolved an issue that could cause wallet discovery to fail during Onboarding #868 -- Improved pricing chart fallbacks and hovering behavior in Token Details #865 -- Improved Hide Small Balances toggle reliability #870 -- Fixed a white screen flash that would sometimes appear when launching Rainbow #877 -- Resolved a crash in browsers without support for IndexedDB before attempting to initialize our Firebase configuration #864 -- Color shading consistency on the dApp Switch Network menu #761 - -## [v1.1.70](https://github.com/rainbow-me/browser-extension/releases/tag/v1.1.70) - -### Fixed - -- Resolved an issue that caused some users to experience a loop where creating or importing a wallet during Onboarding could bring them back to the initial Onboarding step #861 - -## [v1.1.67](https://github.com/rainbow-me/browser-extension/releases/tag/v1.1.67) - -### Changed - -- Improved token caching in scenarios where network requests fail #843 -- Improved styling in preparation for Firefox support #842 -- Removed Send and Swap buttons in Token Details for watched wallets #849 - -### Fixed - -- Resolved a crash that could be caused by an invalid dApp url #857 -- Fixed a memory leak in Alert components #849 -- Blocking interaction with Swap and Send confirmation buttons while gas is being fetched to prevent invalid transactions from being dispatched #824 -- Resolved an issue that could lead to an infinite loop when syncing keychain stores #851 -- Improved logic in dApp provider throttling and resolved a potential crash #855 -- Resolved a crash when pricing data is unavailable for Token Details charts #856 - -## [v1.1.59](https://github.com/rainbow-me/browser-extension/releases/tag/v1.1.59) - -### Added - -- The "Auto-hide balances under $1" toggle in Settings is now available to automatically filter out spam and token dust #818 - -### Changed - -- Introduced a new alert when attempting to sign a message or transaction for a Ledger wallet when the device is disconnected #826 -- Added an Unsupported Browser explainer during Onboarding for unsupported browsers with known issues, including Kiwi Browser #828 -- When fetching additional transactions when scrolling the Activity list, a new loading indicator is available #830 -- Migrated to a more efficient transactions API and introduced pagination in the Activity list #816 #827 -- Improved empty state loading skeletons for the Tokens and Activity interfaces #833 -- The wallet header will now consistently collapse when scrolling Tokens on wallets with a limited number of assets #831 -- Keyboard shortcut and navigation analytics #837 - -### Fixed - -- Fixed a crash for Trezor devices if connectivity is established more than once #819 -- Fixed a crash on dApp interactions if address or chain metadata was unavailable #822 -- Resolved an issue that prevented clicking the Terms of Service link during Onboarding #823 -- Fixed a bug where the Connected Apps network badge wouldn't display properly #825 -- Fixed a bug where wallet name in the Header couldn't be selected with tab keyboard navigation #832 -- Resolved an issue where the user would be asked to create a password again after updating their password in Wallets & Keys in Settings and then using the back button #834 -- Fixed an issue where a user couldn't create a name for a wallet after creating a new Wallet Group #836 -- Improved logging for dApp message signing errors to diagnose problematic dApps #821 - -## [v1.1.48](https://github.com/rainbow-me/browser-extension/releases/tag/v1.1.48) - -### Changed - -- Adopted Socket v2 contracts for gas optimization for token bridging #814 -- Adopted colloquial BSC naming for Binance Smart Chain #768 -- Added support for deprecated `send` and `sendAsync` RPC calls #792 -- Rate limiting dApps that abuse the `window.ethereum` RPC provider #785 -- Analytics for device context to learn about our user’s browsers #776 -- Analytics for screen routing events to follow user journeys #775 - -### Fixed - -- Improved keychain vault stability to resolve an issue where the extension could appear like the user had not yet onboarded #813 -- Resolved an issue with Ledger account discovery for users with more than 1 address #807 -- Resolved an issue where dApps would not reflect a disconnection when using Disconnect All #806 -- Fixed an issue with the `window.ethereum` provider when no other wallets were injected #800 -- Fixed a crash in the Recovery Phrase Seed quiz #780 -- Fixed a crash in Edge when a New Tab is opened #811 -- Fixed an issue with the styling of the Network Changed notification on certain dApps #809 -- Removed unnecessary console logs #769 - -## [v1.1.40](https://github.com/rainbow-me/browser-extension/releases/tag/v1.1.40) - -### Changed - -- Keyboard navigation for dApp prompts is now even easier. Connect to a dApp with `return` and `tab` between Wallets and Network selection more quickly. Smart defaults ensure that you won't accidentally sign or send a transaction, with cancel/rejection actions always the default. #592 -- The native currency values are now editable in the Swap flow so that you can i.e. swap $100 USD of ETH to a different token, without manually estimating token amounts #702 -- You can now confirm a Swap or Send with the keyboard `return` key, and navigate around with `tab` and arrow keys to adjust settings #699 #763 -- Improved lengthy token amount display behavior in the Swap flow when using the Max feature #711 -- The destination Wallet Selection in Send is now collapsible by clicking the drop-down cell #712 #758 -- Improved header scroll animation and feel #691 #748 -- Improved token click and wallet reorder animation polish #762 -- You can now dismiss Swap Settings pop-ups by clicking outside the sheet #743 -- Renamed to `Binance Chain` chain to `BNB Smart Chain` #716 -- Renamed `Polygon (Matic)` chain to `Polygon` #737 - -### Fixed - -- Improvements for `eth_requestAccounts` and `eth_accounts` RPC calls to mirror MetaMask, as well as param order inversion support #730 -- Ledger connection fixes, including “device is already open” scenario and waiting for the transport to closed #720 -- Improvements to dApp provider responsiveness, including network and account changes sent from a dApp #722 -- Fixed analytics toggle that would get stuck in the on position #759 -- dApp Prompts in Arc are now sized correctly and include a background #744 -- Fixed shortcut instruction UI on the Welcome screen on Linux #742 -- Improved Token right-click Send flow to correctly highlight wallet selection instead of token selection #714 -- Fixed a scenario where you could inadvertently create a new wallet after canceling the create process during the naming step #726 -- Fixed spacing on the green/red dApp connection indicator #721 -- Fixed My QR Code styling to mirror the Rainbow App and RainbowKit #734 -- Improved consistency of the Hide Balance setting in the Swaps flow #760 -- Fixed an issue where clicking the wallet name on the My QR Code screen opened the Wallet Switcher #734 -- Settings style fixes & tweaks #719 -- Fixed Watched Wallet alert when Swap keyboard shortcuts are used #723 -- Trimming whitespace when entering an ENS or public address to watch #681 -- Fixed an issue where Swap input fields set by Max would be cleared when selecting a destination token #755 -- Fixed an issue with keyboard navigation on dApp signature prompts where network drop-downs were highlightable #751 -- Fixed a crash on dApp signature prompts when dApp session data is unavailable #713 -- Fixed Sign Message crash in scenarios where the keychain is still booting #772 -- Fixed a crash where the dApp session data could be unavailable and crash the Send flow #729 #718 -- Fixed Send flow crash when an ENS name is unavailable #766 -- Improved keychain boot/deserialization stability #735 -- Fixed an issue where you could select the Send flow before the keychain is finished booting #727 -- Resolved problem area when fetching from localstorage APIs to anticipate undefined when they’re still booting #728 -- Network caching improvements for ENS Profile avatars #704 -- Network query reliability and caching for asset discovery #745 -- Improved error handling for multi-transactions like Swaps when existing transactions are pending #740 -- Fixed React implementation issue with symbols that caused some console warnings in Settings #736 -- Error handling for Trezor SDK initialization #770 -- Improved Trezor integration logging to get error visibility #771 #765 -- Improved Meterology integration to prevent bad gas data crashes #731 - -## [v1.1.17](https://github.com/rainbow-me/browser-extension/releases/tag/v1.1.17) - -### Fixed - -- Resolved an issue with wallet balances in the Wallet Switcher list #717 - -## [v1.1.15](https://github.com/rainbow-me/browser-extension/releases/tag/v1.1.15) - -### Changed - -- You can now dismiss alerts by clicking the blurred background area #698 -- Improved keyboard navigation and tab highlighting in the Hardware Wallet connection flows #696 -- Now waiting for users to complete Onboarding before injecting the Rainbow provider into dApps #686 - -### Fixed - -- Fixed incorrect balances displayed in Wallet Switcher #706 -- Assets are now sticky after a Swap so that you can refresh your asset list before token transfers are indexed onchain #672 -- When using the extension in full screen mode for Hardware Wallet interactions, back buttons are now hidden #693 -- Fixed white screen failure on Hardware Wallet connection success screen #694 #705 -- Now ignoring invalid calldata when parsing and displaying transactions #710 -- Improvements to Ledger Hardware Wallet connection management and cleanup upon disconnect #700 -- Fixed scenario where the Send flow could break if the extension scripts had not yet been awoken by Chrome #709 -- Fixed icon misalignments in Settings menu items #703 -- Tweaked Wallet Group cell paddings and layout in the Wallets & Keys Settings #697 - -## [v1.1.12](https://github.com/rainbow-me/browser-extension/releases/tag/v1.1.12) - -### Added - -- Version numbers are now available at the bottom of Settings #687 - -### Changed - -- Tailored Onboarding welcome screen "Pin Rainbow to your toolbar" for the Arc browser #674 -- Analytics for Swap, Bridge, and Send submissions #656 - -### Fixed - -- Major performance improvements for Tokens and Activity lists for large wallets #675 -- Fixed crash when a Send transaction fails #685 -- Fixed an issue with how Insufficient Gas errors were displayed for native gas tokens in Swaps #673 -- Fixed missing copy scenario on wallet selection during the Secret Recovery Phrase import flow #684 -- Fixed scenario where the Send wallet selection dropdown would not display any selectable wallets #688 -- Fixed animations of Send token selection dropdown #688 -- Fixed missing token highlight on Send token selection dropdown #688 -- Fixed text line height cutoff on "No activity yet" empty state #653 -- Compressed image and sounds assets for performance #689 - -### Security - -- Added infrastructure and CI errors to further strengthen circular dependency vulnerability protections #680 #683 - -## [v1.1.5](https://github.com/rainbow-me/browser-extension/releases/tag/v1.1.5) - -### Changed - -- We’ve introduced new wallet recommendations for the “Watch an Ethereum address” step of Onboarding #633 -- Analytics for global Flashbots RPC setting #657 - -### Fixed - -- Wallets with an ENS name are now searchable in the Wallet Switcher #665 -- Resolved incorrect empty state avatar before a wallet was selected on the Send flow #676 -- Resolved a race condition where delays in fetching the Remote Config would mean Onboarding was not properly gated for Invite Codes #677 - -## [v1.1.0](https://github.com/rainbow-me/browser-extension/releases/tag/v1.1.0) - -### Added - -- `R` hotkey to refresh Tokens & Activity - -### Changed - -- Faster wallet discovery on Secret Recovery Phrase import -- Allowing users to import a wallet without on-chain activity in the Secret Recovery Phrase import flow -- Replaced “Recovery Phrase n” with “Wallet Group n” in the Wallets & Keys flows in Settings -- Improved Hardware Wallet USB connection reliability issues -- “Pin Rainbow to your toolbar” nudge is now available on the Welcome screen -- Improved drag-and-drop wallet list reliability in the Wallet Switcher -- Introduced a “Unknown Token” fallback name for new or unindexed tokens -- Adjusted Ledger flow Unlock copy -- Updated copy in Secret Recovery Phrase Import to clarify that users can paste their seed phrase -- Replaced “Swapping via” and “Bridging via” for Bridges -- Increased Send warning checkbox click area -- Deprecated “injection complete in window” console log on dApps - -### Fixed - -- Fixed overlapping cells in Tokens and Activity -- Fixed an issue that caused certain ENS Profile avatars to incorrectly resolve -- Fixed backward navigation order issues -- Fixed an issue that sometimes caused wallet creation in Onboarding to fail -- Fixed gas estimates that caused “Insufficient ETH” error when using the Max feature in Swaps -- Removed Send and Swap items from the right-click menu on Token cells for Watched wallets -- Fixed Bridge failures for a subset of tokens -- Fixed issue where tokens appear in the wrong wallet when switching wallets quickly with hotkeys -- Fixed layout issues in Send inputs -- Fixed ratio distortion on non-square avatars -- Preventing infinite loop after using Wallet Switcher shortcuts -- Disabled autocomplete dropdown on input fields -- Activity header height fix -- Fixed an issue with certain interfaces that require custom tabIndex values -- Fixed issue where Custom Gas label shows when not using Custom Gas in Swap flow -- Improved scroll behavior after switching Tokens and Activity tabs -- Disabled `f` and `m` hotkeys when searching tokens in Swap and Send -- Improved transition smoothness on Welcome screen shortcut preview -- Fixed an issue that caused the Shortcut preview on the Welcome screen to highlight when unfocusing the window -- Fixed an issue where the `f` flip shortcut in Swap would clear asset amount input fields -- Fixed hover highlight animation on “Create a new wallet” button in Onboarding -- Removed “Last tx” label for wallets without activity during import flows -- Fixed color contrast for certain assets in dark mode -- Resolved an issue with the Rename Wallet prompt where input field was not always cleared -- Hiding currently selected wallet from the wallet list in Send -- Fixed UI display issue for wrapped tokens in Swaps -- Fixed an issue where you could select the same native token in the Swap pair selection -- Fixed a quiet failure after attempting to import the Secret Recovery Phrase or Private Key for a watched wallet -- Fixed an issue where UI broke during Swaps when no token pricing data was available -- Fixed incorrect order of Approve and Swap transactions in the Activity list for Swaps -- Fixed a scenario where the Secret Recovery Phrase input UI was clipped - -### Security - -- Import flow redesign to mitigate known Demonic vulnerability reported by Halborn - -## v1.0.170 - -### Added - -- Default Provider setting is now on by default -- Notifications Permission request in anticipation of upcoming features - -### Changed - -- Wallet Selection menus are now scrollable -- Changed Gwei Settings title to Gas Settings -- Better “Connect your wallets” copy during the Hardware Wallet import step -- Better “Connected successfully” copy during the Hardware Wallet connect flow -- Improved handling for full screen flows -- Text color consistency between Ledger and Trezor Hardware Wallet flows -- Improved copy on discovered wallet list for Secret Recovery Phrase imports -- Added `Share Feedback` and `Guides & Support` links to the More menu -- Added `Share Beta Feedback` and `Guides & Support` like to Settings -- Deprecating placeholder Contacts menu item in Settings - -### Fixed - -- Fixed issue where a user could broadcast multiple transactions when spamming confirmation button in the Send or Swap flow -- Fixed scenario where user could get stuck with only the “Skip” button at the Verify Seed Phrase step of Onboarding -- Fixed mainnet Send and Swap transactions for Ledger and Trezor Hardware Wallets -- Fixed missing Trezor icon in Wallets & Keys Settings -- Excluding Hardware Wallets from Wallet Groups list -- Fixed Welcome screen `Alt+Shift+R` reliability -- Fixed “Watching” mode alert when attempting to Sign messages or transactions with a watched wallet -- Fixed backward navigation flows in Wallets & Keys submenus in Settings -- Fixed light mode switch network menu accent colors -- Fixed Swap Review sheet height -- Prompts now have a max width and look better in Hardware Wallet flows -- Fixes for fullscreen flows to better support Hardware Wallet flows -- Fixed Wallet Group list cell separator alignment - -## v1.0.160 - -### Added - -- Choose a Wallet Group (aka Secret Recovery Phrase) when creating a new wallet -- Redesigned Welcome screen that features the `Alt+Shift+R` shortcut -- “Connect your Ledger” UI for Ledger Hardware Wallets -- “Connected successfully” UI for Ledger Hardware Wallets - -### Changed - -- You can now tab through options in the Onboarding import flow and the Wallet Switcher “Add another wallet” menu -- Improved Ledger “Connect your wallets” selection cells for wallets without discoverable balances -- Improved animation when watching a new wallet from the Wallet Switcher -- Improved wallet ordering defaults in Send address dropdown -- Improved “Add a wallet by its index” screen presentation transition for Hardware Wallets - -### Fixed - -- Fixed an issue with the `,` shortcut for Settings -- Fixed an issue where Bridging in the Swap flow would lead to a blank screen -- Fixed an issue where right-clicked tokens were not unset upon entering the Swap or Send flow from the Swap and Send buttons -- Fixed an issue where pressing `Enter` during the Onboarding “Watch Wallet” flow caused the empty input to be accepted -- Fixed an issue where pressing `Enter` twice during the Onboarding “Import Wallet” flow caused the empty input to be accepted -- Resolved incorrect swap values for `BNB` token and other native asset swap pairs -- Resolved display issues on the custom gas input fields when large numbers are entered -- Fixed edge cases in the Swap flow where USD estimates for certain assets appeared as $Nan -- Fixed an issue where the Onboarding “Watch wallet” button was not clickable after deselecting and reselecting an ENS -- Fixed Swaps inside the fullscreen flow for Hardware Wallet signatures - -## v1.0.151 - -### Added - -- Onboarding UI fixes and consistency improvements -- Swap Quotes now have more accurate fee-adjusted display values - -### Fixed - -- Fixed scrolling in Wallet Switcher when Tips are dismissed -- Fixed Send token selection clash for assets that use the same address across multiple networks -- Fixed allowance issues for cross-chain swaps that caused some swaps to fail -- Fixed input-output USD difference Swap quote estimation subline -- Fixed Wallet Switcher accent colors -- Blocking navigation during Onboarding to resolve scenarios where a user could create multiple seed phrases simultaneously -- Clearing keychain on Welcome screen when no password is set -- Fixed out-of-bounds toast component edge cases -- Fixed Swap token Copy action inadvertent select behavior -- Preventing token-selection clicks in Swaps once token pairs are selected to streamline token amount editing -- Fixed Small Market Warning text overflow in Swap Review - -## v1.0.145 - -### Added - -- Wallet Switcher Shortcuts - - Press `/` from the Wallet Switcher list to focus on the Search input field - - This allows you to use `W` and `/` in succession from the core wallet screen to quickly switch to the wallet you’re looking for, without touching your mouse. - - You can even toggle the extension open and closed with `Option-Shift-R` on macOS or `Alt-Shift-R` on Windows. -- dApp Transaction and Signature Prompt Shortcuts - - Press `ESC` to deny a transaction or message signature request popup -- dApp Connection Prompt Shortcuts - - Switch Wallet Shortcuts (`1` `2` `3` etc. hotkeys) are now available within a dApp Connection Prompt. Just press a number hotkey to switch to the wallet you’d like to connect. - - Press `W` to open the Wallet Selection menu to see your list of wallets and their corresponding number hotkeys - - Press `N` to open the Network Selection menu to see the list of available networks. You can also use the `1` `2` `3` hotkeys to select the corresponding network - - All of the menus throughout Rainbow can also be navigated with the `↑ ↓` arrow keys and `ENTER` -- Tokens Shortcuts - - Click on a Token and quickly select a menu action with shortcuts - - Swap Token: `X` - - Send Token: `S` - - View Token on Etherscan: `V` -- Activity Shortcuts - - Click on a Transaction and quickly select a menu action with shortcuts - - For confirmed transactions: - - View Transaction on Etherscan: `V` - - Copy Transaction Hash: `C` - - For pending transactions: - - Speed up Transaction: `S` - - Cancel Transaction: `ESC` - - Copy Transaction Hash: `C` -- Contacts Shortcuts - - Click on the `...` more menu in the Send flow to manage a Contact - - Copy Contact address: `C` - - Edit Contact Name: `E` - -### Changed - -- Edit Wallets option now available during the Secret Recovery Phrase Import flow in Onboarding -- You will now hear a success or failure sound when completing the Secret Recovery Phrase quiz during Onboarding -- Onboarding UI consistency improvements - -### Fixed - -- Fixed unexpected navigation behavior when copying a wallet address in the Wallet Switcher -- Fixed scrolling in Wallet Switcher when the Tip is dismissed -- Fixed Send token selection clash for assets that use the same address across multiple networks -- Fixed Swap token Copy action inadvertent select behavior -- Fixed Wallet Switcher accent colors -- Fixed invalid address logic when attempting to Watch an Ethereum address -- Blocking navigation during Onboarding to resolve scenarios where a user could create multiple seed phrases simultaneously -- Clearing keychain on Welcome screen when no password is set -- Fixed out-of-bounds toast component edge cases -- Fixed a unique key console error message in Onboarding - -## v1.0.132 - -### Added - -- Swap Shortcuts - - Top Input: `⌥ + ↑, ALT + ↑` - - Bottom Input: `⌥ + ↓, ALT + ↓` - - Flip Tokens: `F` -- Wallet Switching Shortcuts - - `1` `2` `3` etc. hotkeys from core wallet screen will switch to your corresponding wallet - - These numbers reflect the order of your wallets within the Wallet Switcher. You can always reorder wallets with a click-drag. - -### Changed - -- Removed “Save Contact” for imported/owned wallets -- Custom wallet ordering is now reflected in the Send wallet selector -- Removed accent colors in Onboarding flows - -### Fixed - -- Fixed spacing in Create Password screen title -- Fixed scrolling on Seed Backup and Seed Reveal screens -- Fixed Create Password back arrow styling (should be `flat` based on designs) -- Seed Phrase Quiz now appears on new seed phrase creation -- Now blocking wallet creation when a wallet is already being generated - -## v1.0.123 - -### Added - -- Send Shortcuts - - Select Top Input `⌥ + ↑, ALT + ↑` - - Select Bottom Input `⌥ + ↓, ALT + ↓` - - Max Amount `M` - - Switch Value `F` - - Custom Gas `C` - - Gas Menu `G` - - Contact Menu `.` - -### Changed - -- Speed up & Cancel now handles more transaction scenarios, including swaps -- `priceimpact` support for cross-chain Swap quotes -- Copy address toast now appears on the Wallet Switcher -- Now displaying L2 native asset icons on dApp transaction prompts -- Onboarding grammar and punctuation consistency - -### Fixed - -- Fixed the More context menu click area for wallets in Wallet Details -- Fixed missing prices for destination swap assets -- Fixed swap `serviceTime` estimation alerts -- Home screen tab key keyboard navigation fixes -- Onboarding import flow text alignment consistency -- Fixed swap token row context menu margin -- Fixed padding and active highlight color when importing a seed phrase with 1 discovered wallet -- Fixed incorrect token icons in Swap token selection - -## v1.0.118 - -### Added - -- Swap & Bridge - - Rainbow’s token swap & network bridge experience that you’re familiar with is now available within the extension! - - Swap your tokens with confidence with Rainbow’s trusted Verified token list and control over your very own Favorites - - Bridge your assets to and from Mainnet, Arbitrum, Optimism, Polygon, and BSC -- Flashbots toggle - - You can now send all Mainnet transactions through Flashbots by flipping on a toggle in Settings - - Flashbots can also be enabled for individual swaps within the Review screen -- Force Connect to dApps - - You can now connect to a dApp from within the Rainbow extension by choosing Connect and a Network in the top-left dApp menu -- Keyboard Shortcuts available within the core wallet screen - - Settings `,` - - More Menu `.` - - Close `Esc` - - Lock `L` - - Copy `C` - - Swap `X` - - Send `S` - - Profile `P` - - My QR Code `Q` - - Connected Apps `D` - - Wallet Switcher `W` - -### Fixed - -- Fixed contract deployment calls -- Unsupported `eth_` RPC methods now forward to Rainbow’s node provider -- Fixed transaction method names in Activity list, favoring Rainbow’s overrides -- Fixed swap quote search crash when value was undefined -- Fixed WETH-ETH swaps -- Fixed Custom Gas fee display -- Fixed lock screen UI bugs -- Fixed transaction time estimate label alignment -- Fixed missing asset prices for certain swap assets -- Fixed speed up and cancel - -## v1.0.109 - -### Added - -- My QR Code - - A quick way to share your wallet address with others or scan it from a mobile wallet. Now available in the top-right menu of your wallets. -- Keyboard navigation and shortcuts for Settings, Tokens, and Activity - -### Changed - -- Now rejecting `eth_signTypedData_v4` RPC requests where the `chainId` doesn't match the session chain -- Improved dApp metadata and handling of long dApp names for Connect -- Better handling of long dApp URLs in Connected Apps - -### Fixed - -- Blocking native context menus on Right Click -- Fixed shadow clipping on Tokens and Activity in Light Mode -- Disconnect All button border display bug - -## v1.0.99 - -### Changed - -- Better dApp metadata and shorthand names -- Added missing RPC methods used by OpenSea -- When deleting the last wallet, we now wipe the keychain and send you to the welcome screen to start with the onboarding process again -- Ellipsis for long-name dApps in dApp Connect request -- Added spinner to unlock button -- Added spinner to the CTA of all the dApp requests -- Added spinner on create wallet button -- Added accent color to password input -- Contact text wrapping - -### Fixed - -- Fixed seed word table going into a different row when length is too long - -## v1.0.89 - -### Added - -- Right-click Quick Actions for Tokens & Activity - - Tokens: Swap, Send, or view a token on Etherscan - - Activity: View a transaction on Etherscan, or Copy transaction hashes -- Toasts UI on Copy actions in Onboarding - -### Changed - -- Improved gas speed defaults for Arbitrum, Optimism, and BSC -- Added native asset amount and native price on dApp transaction prompts -- Alerts and Context Menus are now dismissed with Esc key shortcut - -### Fixed - -- Fixed the private key export flow in Settings -- Fixed Onboarding import wallet spacing inconsistencies -- Fixed Onboarding recovery phrase quiz alignment for long words -- dApp Prompts now dismiss without triggering navigation flashes -- Fixed flash of wallet screen in auto-lock state -- Fixed dApp icons that were missing or wouldn’t load -- Fixed Activity cell USD estimate text alignment - -## v1.0.70 - -### Added - -- Use `Option-Shift-R` on macOS and `Alt-Shift-R` on Windows to quickly open the extension from any dApp - -### Changed - -- Sends for Layer 2 networks will now show a Pending spinner in Activity -- Images and avatars resolved from ENS will now load much faster -- Faster Token and Assets fetching -- New alert UI for dApp interaction attempts for Watched wallets -- Argent addresses now produce a warning in the Send flow to prevent users from incorrectly sending funds to Argent smart contract wallets - -### Fixed - -- Fixed an issue that caused some contract interactions to be malformed and fail -- Fixed test overflow for tokens with lengthy names -- Fixed an issue that prevented users from creating a wallet without specifying a name -- Improved maxFee gas estimates for Custom Gas transactions -- Fixed private key imports for keys without 0x -- Fixed the Coming Soon alert for users that try to interact with Swaps +- Releasing the first version of OrbyPlayground diff --git a/README.md b/README.md index 124a2c0c7e..f02727eb22 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@ ![](.github/hero.png) -## 🌈️ Rainbow Extension +## OrbyPlayground Extension Built for speed. Built for power. Built for you. -Rainbow is a fun, simple, and secure Ethereum wallet that makes managing your assets a joy. Great for newcomers and power users alike, Rainbow allows you to be in total control of your crypto. You own your assets directly thanks to the power of cryptography and the Ethereum blockchain, and Rainbow makes managing all of your wallets and keys a breeze. +OrbyPlayground is a playground extension for showcasing the power of Orby. It is a fork of the Rainbow Extension with chain abstraction and gas abstraction features. ### Features -- Auto-discovers tokens and assets -- Supports Layer 2 chains like Arbitrum, Optimism, Base, Polygon, Avalanche, & Zora right out-of-the-box -- Built-in Send, Bridge, and Swap to power all of your DeFi needs -- Keyboard shortcuts for pros to switch wallets like 1 2 3 -- Search and navigate your wallets with ⌘K or Ctrl-K for the Magic Menu -- Watch wallets and interact with dApps in Impersonation mode +- Chain Abstraction, and ability to use the power of all your assets on any app on any chain. +- Gas abstraction for EOAs +- Gas sponsorship for EOAs +- Unified balances for assets across all supported chains. +- Auto-discovers tokens and assets across all supported chains: Mainnet, Base, Arbitrum, Optimism, and Polyon +- Built-in Send and Swap using your unified balance to power all of your DeFi needs ...and a lot more. @@ -21,39 +21,19 @@ Rainbow is a fun, simple, and secure Ethereum wallet that makes managing your as
Chrome -[Chromium](https://chrome.google.com/webstore/detail/rainbow/opfgelmcmbiajamepnmloijbpoleiama) including Chrome, Brave and Arc +[Chromium][coming soon]() including Chrome, Brave and Arc Edge -[Edge](https://chrome.google.com/webstore/detail/opfgelmcmbiajamepnmloijbpoleiama) +[Edge][coming soon]() Firefox -[Firefox](https://addons.mozilla.org/en-US/firefox/addon/rainbow-extension/) +[Firefox][coming soon]() Safari -Safari is [coming soon](https://rainbowdotme.typeform.com/to/iT919yeN) - -## Security architecture - -Rainbow is one of the first extensions to use the new Manifest v3 extension standard. This comes with some important security benefits: - -- **Runtime isolation**: Remotely hosted code is no longer allowed; an extension can only execute JavaScript that is included within its package. -- **Network firewall**: Content security policy (CSP) allows us to define which domains the extension can interact with, similar to a "firewall". This means that if at any point the extension is compromised, it will not be able to communicate with any domain that is not explicitly allowed in the CSP, preventing any kind of data exfiltration. - -The v3 standard also improves the overall reliability of Rainbow: - -- **Performance**: lighter CPU and memory footprint - the extension consumes resources only when active thanks to service workers. You can compare how quickly the extension loads compared to others. -- **Reliable hardware wallets**: The extension can directly access web technologies like WebUSB and HID that make the integration with hardware wallets much simpler and more secure. - -Additionally, we're using some well known tools engineered by the MetaMask team: - -- [@lavamoat/allow-scripts](https://github.com/LavaMoat/LavaMoat/tree/main/packages/allow-scripts) and [@lavamoat/preinstall-always-fail](https://github.com/LavaMoat/LavaMoat/tree/main/packages/preinstall-always-fail) are used to disable or allow dependency lifecycle scripts (eg. "postinstall"), a common build-time vulnerability -- [lavamoat](https://github.com/LavaMoat/lavamoat) aka LavaMoat Node is a NodeJS runtime that protects our build process, which aims to reduce the risk of malicious code in the dependency graph, commonly known as "software supply chain attacks" -- [browser-passworder](https://github.com/MetaMask/browser-passworder) is our shared encryption library used to encrypt a user's keychain while at rest - -> NOTE: We don't rely on LavaMoat at runtime because of the performance overhead and the benefits we already receive from Manifest v3, but we may consider it in the future. +Safari is [coming soon]() ## Getting started diff --git a/README_FIREFOX.md b/README_FIREFOX.md index d1e29fc012..36df537963 100644 --- a/README_FIREFOX.md +++ b/README_FIREFOX.md @@ -1,4 +1,4 @@ -# Rainbow Extension for Firefox +# OrbyPlayground Extension for Firefox ## Prerequisites @@ -23,7 +23,6 @@ nvm use 20.16.0 yarn setup ``` - ### 3. Build the extension ```bash @@ -36,4 +35,4 @@ yarn firefox:build && yarn update-manifest:prod yarn zip && yarn firefox:zip ``` -You should find a xpi file named `rainbowbx.xpi` in the root folder of this repository \ No newline at end of file +You should find a xpi file named `rainbowbx.xpi` in the root folder of this repository diff --git a/manifest/internal.json b/manifest/internal.json index 3e4c5421c1..1400f982f0 100644 --- a/manifest/internal.json +++ b/manifest/internal.json @@ -1,4 +1,4 @@ { - "name": "Rainbow DEVELOPMENT BUILD", + "name": "OrbyPlayground DEVELOPMENT BUILD", "description": "THIS EXTENSION IS FOR BETA TESTING" -} \ No newline at end of file +} diff --git a/package.json b/package.json index dc44cd6b31..a80af5ff52 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "@ledgerhq/hw-transport-webhid": "6.29.3", "@metamask/browser-passworder": "4.1.0", "@metamask/eth-sig-util": "7.0.1", - "@orb-labs/orby-react": "0.0.22", + "@orb-labs/orby-react": "0.0.27", "@radix-ui/react-accordion": "1.1.2", "@radix-ui/react-context-menu": "2.1.1", "@radix-ui/react-dropdown-menu": "2.0.1", diff --git a/src/entries/popup/components/TransactionFee/GasTokenMenu.tsx b/src/entries/popup/components/TransactionFee/GasTokenMenu.tsx index de285aceb2..75e5dfc388 100644 --- a/src/entries/popup/components/TransactionFee/GasTokenMenu.tsx +++ b/src/entries/popup/components/TransactionFee/GasTokenMenu.tsx @@ -72,6 +72,7 @@ export const SwitchGasTokenMenuSelector = ({ networks: [], uniqueId: gasToken.standardizedTokenId, }} + isParent={true} size={18} /> )} @@ -151,6 +152,7 @@ export const SwitchGasTokenMenu = React.forwardRef< networks: [], uniqueId: selectedGasToken?.standardizedTokenId || '', }} + isParent={true} size={18} /> )} diff --git a/src/entries/popup/components/TransactionRoute/index.tsx b/src/entries/popup/components/TransactionRoute/index.tsx new file mode 100644 index 0000000000..326be6751e --- /dev/null +++ b/src/entries/popup/components/TransactionRoute/index.tsx @@ -0,0 +1,41 @@ +import { OperationSet } from '@orb-labs/orby-core'; +import { memo, useMemo } from 'react'; +import { formatUnits } from 'viem'; + +import { getChain } from '~/core/utils/chains'; +import { Box, Inline, Symbol, Text } from '~/design-system'; + +export const TransactionRoute = memo(function TransactionRoute({ + operationSet, +}: { + operationSet?: OperationSet; +}) { + const fungibleTokens = useMemo(() => { + return operationSet?.inputState?.getFungibleTokens(); + }, [operationSet]); + + return ( + + + Using Funds + + {fungibleTokens?.map((input, i) => ( + + + + + Use {formatUnits(input.toRawAmount(), input.token.decimals)}{' '} + {input.token.symbol} from{' '} + {getChain({ chainId: Number(input.token.chainId) }).name} + + + + ))} + + ); +}); diff --git a/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts b/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts index e7cc3c614f..b8bed3b130 100644 --- a/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts +++ b/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts @@ -1,46 +1,38 @@ +import { CreateOperationsStatus, OperationSet } from '@orb-labs/orby-core'; import { useMemo } from 'react'; import { DAppStatus } from '~/core/graphql/__generated__/metadata'; import { i18n } from '~/core/languages'; import { ActiveSession } from '~/core/state/appSessions'; -import { useConnectedToHardhatStore } from '~/core/state/currentSettings/connectedToHardhat'; -import { ChainId } from '~/core/types/chains'; -import { chainIdToUse, getChain } from '~/core/utils/chains'; export const useApproveAppRequestValidations = ({ - session, dappStatus, + operationSet, }: { session: ActiveSession; dappStatus?: DAppStatus; + operationSet?: OperationSet; }) => { - const { connectedToHardhat, connectedToHardhatOp } = - useConnectedToHardhatStore(); - const enoughNativeAssetForGas = true; const buttonLabel = useMemo(() => { - const activeChainId = chainIdToUse( - connectedToHardhat, - connectedToHardhatOp, - session?.chainId || ChainId.mainnet, - ); if (dappStatus === DAppStatus.Scam) return i18n.t('approve_request.send_transaction_anyway'); - if (!enoughNativeAssetForGas) - return i18n.t('approve_request.insufficient_native_asset_for_gas', { - symbol: getChain({ chainId: activeChainId }).nativeCurrency.name, - }); + if (operationSet?.status == CreateOperationsStatus.INSUFFICIENT_FUNDS) { + return i18n.t('send.button_label.insufficient_asset'); + } + + if (operationSet?.status == CreateOperationsStatus.NO_EXECUTION_PATH) { + return i18n.t('send.button_label.no_execution_path'); + } + + if (operationSet?.status == CreateOperationsStatus.SUCCESS) { + return i18n.t('send.button_label.review'); + } return i18n.t('approve_request.send_transaction'); - }, [ - connectedToHardhat, - connectedToHardhatOp, - session?.chainId, - dappStatus, - enoughNativeAssetForGas, - ]); + }, [dappStatus, operationSet?.status]); return { enoughNativeAssetForGas, diff --git a/src/entries/popup/hooks/useConnectAppSessions.ts b/src/entries/popup/hooks/useConnectAppSessions.ts index 2e130875ef..2270bb12e5 100644 --- a/src/entries/popup/hooks/useConnectAppSessions.ts +++ b/src/entries/popup/hooks/useConnectAppSessions.ts @@ -1,9 +1,5 @@ -import { Account, AccountType, VMType } from '@orb-labs/orby-core'; -import { bulkResetConnectedAppSessions } from '@orb-labs/orby-core-mini'; -import { useOrby } from '@orb-labs/orby-react'; -import { OrbyActions } from '@orb-labs/orby-viem-extension'; +import { useBulkConnectAppSessions } from '@orb-labs/orby-react'; import * as React from 'react'; -import { Client, HttpTransport, PublicRpcSchema } from 'viem'; import { useAppSessionsStore } from '~/core/state'; @@ -19,92 +15,3 @@ export function useConnectAppSessions() { const { isLoading, isConnected } = useBulkConnectAppSessions(activeSessions); return { isLoading, isConnected }; } - -export function useBulkConnectAppSessions( - activeSessions: { host: string; address: `0x${string}` }[], -) { - const [isLoading, setIsLoading] = React.useState(false); - const [isConnected, setIsConnected] = React.useState(false); - const { baseMainnetClient } = useOrby(); - - // A ref to track the previous count value - const prevCountRef = React.useRef<{ host: string; address: `0x${string}` }[]>( - [], - ); - - React.useEffect(() => { - const resetConnectedAppSessions = async () => { - setIsLoading(true); - try { - if ( - activeSessions.length == 0 || - !baseMainnetClient || - prevCountRef.current.sort((a, b) => b.host.localeCompare(a.host)) == - activeSessions.sort((a, b) => b.host.localeCompare(a.host)) - ) { - return; - } - - const connected = await connectAppSessions( - activeSessions, - // @ts-ignore - baseMainnetClient, - ); - - prevCountRef.current = activeSessions.map((session) => ({ - ...session, - })); - - // Update the previous value after render - setIsConnected(connected); - } catch (error) { - console.error('Failed to reset connected app', error); - } finally { - setIsLoading(false); - } - }; - - resetConnectedAppSessions(); - }, [activeSessions, baseMainnetClient]); - - return { isLoading, isConnected }; -} - -export async function connectAppSessions( - activeSessions?: { host: string; address: string }[], - baseMainnetClient?: Client< - HttpTransport, - undefined, - undefined, - PublicRpcSchema, - OrbyActions - >, -) { - if (!activeSessions || !baseMainnetClient) { - return false; - } - - const promises = activeSessions?.map( - async ({ host, address }: { host: string; address: string }) => { - const account = new Account( - address?.toLowerCase(), - AccountType.EOA, - VMType.EVM, - undefined, - ); - - const accountCluster = await baseMainnetClient.createAccountCluster([ - account, - ]); - - return { - appUrl: host, - activeAccountClusterId: accountCluster.accountClusterId, - }; - }, - ); - - const sessions = await Promise.all(promises); - bulkResetConnectedAppSessions(sessions); - return true; -} diff --git a/src/entries/popup/pages/messages/SendTransaction/SendTransactionActions.tsx b/src/entries/popup/pages/messages/SendTransaction/SendTransactionActions.tsx index 63740b797b..0b2d004ff3 100644 --- a/src/entries/popup/pages/messages/SendTransaction/SendTransactionActions.tsx +++ b/src/entries/popup/pages/messages/SendTransaction/SendTransactionActions.tsx @@ -1,3 +1,6 @@ +import { CreateOperationsStatus, OperationSet } from '@orb-labs/orby-core'; +import { useMemo } from 'react'; + import { DAppStatus } from '~/core/graphql/__generated__/metadata'; import { i18n } from '~/core/languages'; import { shortcuts } from '~/core/references/shortcuts'; @@ -16,6 +19,7 @@ export const SendTransactionActions = ({ waitingForDevice, loading = false, dappStatus, + operationSet, }: { session: ActiveSession; onAcceptRequest: () => void; @@ -23,13 +27,16 @@ export const SendTransactionActions = ({ waitingForDevice: boolean; loading: boolean; dappStatus?: DAppStatus; + operationSet?: OperationSet; }) => { const { buttonLabel } = useApproveAppRequestValidations({ session, dappStatus, }); - const enoughNativeAssetForGas = true; + const disabled = useMemo(() => { + return operationSet?.status != CreateOperationsStatus.SUCCESS; + }, [operationSet]); const { trackShortcut } = useKeyboardAnalytics(); useKeyboardShortcut({ @@ -52,15 +59,14 @@ export const SendTransactionActions = ({ label={i18n.t('common_actions.cancel')} dappStatus={dappStatus} /> - {enoughNativeAssetForGas && ( - - )} + ); }; diff --git a/src/entries/popup/pages/messages/SendTransaction/SendTransactionsInfo.tsx b/src/entries/popup/pages/messages/SendTransaction/SendTransactionsInfo.tsx index 5997faeb5a..43c4c3e09a 100644 --- a/src/entries/popup/pages/messages/SendTransaction/SendTransactionsInfo.tsx +++ b/src/entries/popup/pages/messages/SendTransaction/SendTransactionsInfo.tsx @@ -1,8 +1,8 @@ import { TransactionRequest } from '@ethersproject/abstract-provider'; -import { OperationSet } from '@orb-labs/orby-core'; +import { CreateOperationsStatus, OperationSet } from '@orb-labs/orby-core'; import { AnimatePresence, motion } from 'framer-motion'; import { ReactNode, memo, useMemo, useState } from 'react'; -import { Address, formatUnits } from 'viem'; +import { Address } from 'viem'; import { DAppStatus } from '~/core/graphql/__generated__/metadata'; import { i18n } from '~/core/languages'; @@ -35,6 +35,7 @@ import { ChainBadge } from '~/entries/popup/components/ChainBadge/ChainBadge'; import { DappIcon } from '~/entries/popup/components/DappIcon/DappIcon'; import { Tag } from '~/entries/popup/components/Tag'; import { triggerToast } from '~/entries/popup/components/Toast/Toast'; +import { TransactionRoute } from '~/entries/popup/components/TransactionRoute'; import { useAppSession } from '~/entries/popup/hooks/useAppSession'; import { useRainbowNavigate } from '~/entries/popup/hooks/useRainbowNavigate'; import { useUserNativeAsset } from '~/entries/popup/hooks/useUserNativeAsset'; @@ -169,41 +170,6 @@ const Overview = memo(function Overview({ ); }); -const TransactionRoute = memo(function TransactionRoute({ - operationSet, -}: { - operationSet?: OperationSet; -}) { - const fungibleTokens = useMemo(() => { - return operationSet?.inputState?.getFungibleTokens(); - }, [operationSet]); - - return ( - - - Using Funds - - {fungibleTokens?.map((input, i) => ( - - - - - Use {formatUnits(input.toRawAmount(), input.token.decimals)}{' '} - {input.token.symbol} from{' '} - {getChain({ chainId: Number(input.token.chainId) }).name} - - - - ))} - - ); -}); - const TransactionDetails = memo(function TransactionDetails({ simulation, session, @@ -366,7 +332,7 @@ function TransactionInfo({ diff --git a/src/entries/popup/pages/messages/SignMessage/SignMessageInfo.tsx b/src/entries/popup/pages/messages/SignMessage/SignMessageInfo.tsx index f841cdebf5..6563b90a2a 100644 --- a/src/entries/popup/pages/messages/SignMessage/SignMessageInfo.tsx +++ b/src/entries/popup/pages/messages/SignMessage/SignMessageInfo.tsx @@ -1,7 +1,6 @@ import { OnchainOperation, OperationSet } from '@orb-labs/orby-core'; import { AnimatePresence, motion } from 'framer-motion'; -import { memo, useMemo, useState } from 'react'; -import { formatUnits } from 'viem'; +import { useState } from 'react'; import { DAppStatus } from '~/core/graphql/__generated__/metadata'; import { i18n } from '~/core/languages'; @@ -9,12 +8,12 @@ import { useDappMetadata } from '~/core/resources/metadata/dapp'; import { useCurrentCurrencyStore } from '~/core/state'; import { ProviderRequestPayload } from '~/core/transports/providerRequestTransport'; import { ChainId } from '~/core/types/chains'; -import { getChain } from '~/core/utils/chains'; import { copy } from '~/core/utils/copy'; import { getSigningRequestDisplayDetails } from '~/core/utils/signMessages'; import { truncateString } from '~/core/utils/strings'; import { Box, Inline, Separator, Stack, Symbol, Text } from '~/design-system'; import { DappIcon } from '~/entries/popup/components/DappIcon/DappIcon'; +import { TransactionRoute } from '~/entries/popup/components/TransactionRoute'; import { useAppSession } from '~/entries/popup/hooks/useAppSession'; import { DappHostName, MaliciousRequestWarning } from '../DappScanStatus'; @@ -93,45 +92,8 @@ function Overview({ ); } -const TransactionRoute = memo(function TransactionRoute({ - operationSet, -}: { - operations?: OnchainOperation[]; - operationSet?: OperationSet; -}) { - const fungibleTokens = useMemo(() => { - return operationSet?.inputState?.getFungibleTokens(); - }, [operationSet]); - - return ( - - - Using Funds - - {fungibleTokens?.map((input, i) => ( - - - - - Use {formatUnits(input.toRawAmount(), input.token.decimals)}{' '} - {input.token.symbol} from{' '} - {getChain({ chainId: Number(input.token.chainId) }).name} - - - - ))} - - ); -}); - export const SignMessageInfo = ({ request, - operations, operationSet, }: SignMessageProps) => { const dappUrl = request?.meta?.sender?.url || ''; @@ -165,6 +127,8 @@ export const SignMessageInfo = ({ const tabLabel = (tab: string) => i18n.t(tab, { scope: 'simulation.tabs' }); + console.log('SignMessageInfo', operationSet); + return ( - {operations && ( - - )} + {operationSet && } diff --git a/src/entries/popup/pages/messages/SignMessage/index.tsx b/src/entries/popup/pages/messages/SignMessage/index.tsx index cfce45f13c..7ead66e03c 100644 --- a/src/entries/popup/pages/messages/SignMessage/index.tsx +++ b/src/entries/popup/pages/messages/SignMessage/index.tsx @@ -5,22 +5,27 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { analytics } from '~/analytics'; import { event } from '~/analytics/event'; +import config from '~/core/firebase/remoteConfig'; import { i18n } from '~/core/languages'; import { useDappMetadata } from '~/core/resources/metadata/dapp'; +import { useFlashbotsEnabledStore } from '~/core/state/currentSettings'; import { useFeatureFlagsStore } from '~/core/state/currentSettings/featureFlags'; import { ProviderRequestPayload } from '~/core/transports/providerRequestTransport'; +import { ChainId } from '~/core/types/chains'; import { RPCMethod } from '~/core/types/rpcMethods'; import { POPUP_DIMENSIONS } from '~/core/utils/dimensions'; import { signOperation } from '~/core/utils/orb'; import { getSigningRequestDisplayDetails } from '~/core/utils/signMessages'; import { Bleed, Box, Stack } from '~/design-system'; import { triggerAlert } from '~/design-system/components/Alert/Alert'; +import { TransactionFee } from '~/entries/popup/components/TransactionFee/TransactionFee'; import { showLedgerDisconnectedAlertIfNeeded } from '~/entries/popup/handlers/ledger'; import { useAppSession } from '~/entries/popup/hooks/useAppSession'; import { useWallets } from '~/entries/popup/hooks/useWallets'; import { RainbowError, logger } from '~/logger'; import * as wallet from '../../../handlers/wallet'; +import { GasTokenInput } from '../../send'; import { AccountSigningWith } from '../AccountSigningWith'; import { SignMessageActions } from './SignMessageActions'; @@ -52,6 +57,12 @@ export function SignMessage({ }: ApproveRequestProps) { const [loading, setLoading] = useState(false); const [waitingForDevice, setWaitingForDevice] = useState(false); + const [selectedGasToken, setSelectedGasToken] = useState({ + name: 'no gas abstraction', + standardizedTokenId: undefined, + isDefault: true, + }); + const { data: dappMetadata } = useDappMetadata({ url: request?.meta?.sender?.url, }); @@ -67,13 +78,16 @@ export function SignMessage({ const { accountCluster, baseMainnetClient } = useOrby(); - const { operations, operationSet, virtualNode } = + const { operations, operationSet, virtualNode, isLoading, aggregateFee } = useGetOperationsToSignTypedData( request?.method == 'personal_sign' ? '' : JSON.stringify(requestPayload.msgData), activeSession?.address?.toLowerCase(), activeSession?.chainId ? BigInt(activeSession.chainId) : undefined, + selectedGasToken.standardizedTokenId + ? { standardizedTokenId: selectedGasToken.standardizedTokenId } + : undefined, ); const operationStatusesUpdated = useCallback( @@ -228,6 +242,28 @@ export function SignMessage({ } }, [featureFlags.full_watching_wallets, isWatchingWallet, rejectRequest]); + const chainId = useMemo(() => { + return activeSession?.chainId || ChainId.mainnet; + }, [activeSession?.chainId]); + + const selectGasToken = useCallback( + (gasToken?: GasTokenInput) => { + if (gasToken) { + setSelectedGasToken(gasToken); + } + }, + [setSelectedGasToken], + ); + + const { flashbotsEnabled } = useFlashbotsEnabledStore(); + const flashbotsEnabledGlobally = useMemo(() => { + return ( + config.flashbots_enabled && + flashbotsEnabled && + activeSession?.chainId === ChainId.mainnet + ); + }, [activeSession?.chainId, flashbotsEnabled]); + return ( + diff --git a/yarn.lock b/yarn.lock index 07cf6dff43..130f3b132d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3052,28 +3052,28 @@ resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca" integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q== -"@orb-labs/orby-core-mini@0.0.1": - version "0.0.1" - resolved "https://registry.yarnpkg.com/@orb-labs/orby-core-mini/-/orby-core-mini-0.0.1.tgz#e023ea1e88c62ff96e17fc7fc3602942fced07b3" - integrity sha512-luwYyPAsvf/B9g3ZoaDsNCrxj4+l/NovC2e4sMDz1Lt0d6hn4oBF1rf8SnjS5AZcqntpInaSiG5rNMjtfFIGrQ== +"@orb-labs/orby-core-mini@0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@orb-labs/orby-core-mini/-/orby-core-mini-0.0.3.tgz#fe8a7e37f10e5f634364ca752cc0adbe079e34bb" + integrity sha512-WBvpk+7WMlASUXTbKdwLK6d1coB0wLn1tkHRkgCac/0OulVxQAuD97wTXfcdaA2u+Mrs8GXSaJEfSLxq/t1Tjw== dependencies: node-fetch "^3.3.2" -"@orb-labs/orby-core@0.0.10": - version "0.0.10" - resolved "https://registry.yarnpkg.com/@orb-labs/orby-core/-/orby-core-0.0.10.tgz#2bb6f3ce5d55ed7c525993dd0a61723ddd27d15c" - integrity sha512-GNoA4ob1k/o0tFAYpCTDzjxC1YyrEnE1HI77KRLTA0DtuLxhFmgETLhxCp4punfqRZJO0xbmGhdZ0R53FXRJcQ== +"@orb-labs/orby-core@0.0.11": + version "0.0.11" + resolved "https://registry.yarnpkg.com/@orb-labs/orby-core/-/orby-core-0.0.11.tgz#b61589a734f0a22d5541768b6878929f886d57a3" + integrity sha512-ZdUq7x88sghiwwUMg4OCJDT4TOuqKsqemoqTvGK6vT0NzYPBpda8KDPON2DMbwjixhUXXLuXbEh1IdPNvORv7A== dependencies: "@uniswap/sdk-core" "^5.9.0" jsbi "^3.1.4" -"@orb-labs/orby-react@0.0.22": - version "0.0.22" - resolved "https://registry.yarnpkg.com/@orb-labs/orby-react/-/orby-react-0.0.22.tgz#a83a307178529e362a5a7015cd966458e5be80aa" - integrity sha512-1AMzRJGDh7NyKQQYzWBuqdaRBcPPpGe6r9eHD2M/HZcxE1Pp5bDU7ZkO+QiC4wYRv7nMJhnXEyBCXLSrDjHrPA== +"@orb-labs/orby-react@0.0.27": + version "0.0.27" + resolved "https://registry.yarnpkg.com/@orb-labs/orby-react/-/orby-react-0.0.27.tgz#d9e18813136ccd195c4c560bfff0a2d8fc0b0af6" + integrity sha512-IOwLfI3VXYjh+DH/rurZ3xpZVTftpA8bNeVLaltpDRgivdOb3G80ONAIlMEl9OuAORls0wyGVV0pKTKCKBX0FQ== dependencies: - "@orb-labs/orby-core-mini" "0.0.1" - "@orb-labs/orby-viem-extension" "0.0.9" + "@orb-labs/orby-core-mini" "0.0.3" + "@orb-labs/orby-viem-extension" "0.0.10" "@uidotdev/usehooks" "^2.4.1" "@web3modal/wagmi" "^5.0.10" autoprefixer "^10.4.16" @@ -3086,12 +3086,12 @@ tailwindcss "^3.4.1" viem "^2.9.25" -"@orb-labs/orby-viem-extension@0.0.9": - version "0.0.9" - resolved "https://registry.yarnpkg.com/@orb-labs/orby-viem-extension/-/orby-viem-extension-0.0.9.tgz#bdff671c91c354e98c6725b1d2928cb8a9cfb18d" - integrity sha512-ejHUsZrhZLCoG0HMYZKhq/LO+OrWvaApQ673tmJWpaeVEebCE2vmbzgJEdmjMFpjU/YvPaoaCyNSZgjXUQbNNw== +"@orb-labs/orby-viem-extension@0.0.10": + version "0.0.10" + resolved "https://registry.yarnpkg.com/@orb-labs/orby-viem-extension/-/orby-viem-extension-0.0.10.tgz#e3c3fa76ddb95488c95bc5526cc131dc13b98502" + integrity sha512-tDzrgmL+2NruwZGpGw5DFnsBaC4gc6vyPVmee3ud66EHPcBMCnrwZUpcFMlGEPu4iOcG9rfkaebO1hzlUj9jHQ== dependencies: - "@orb-labs/orby-core" "0.0.10" + "@orb-labs/orby-core" "0.0.11" "@parcel/watcher-android-arm64@2.4.1": version "2.4.1" From e432b4cce9c54de66e31f0f75552d61972917f3b Mon Sep 17 00:00:00 2001 From: felimadu Date: Mon, 16 Dec 2024 01:16:35 -0500 Subject: [PATCH 13/15] adding more error handling --- package.json | 2 +- .../keychainTypes/readOnlyKeychain.ts | 1 - .../useApproveAppRequestValidations.ts | 6 + .../popup/hooks/send/useSendValidations.ts | 6 + src/entries/popup/pages/home/TabHeader.tsx | 1 - src/entries/popup/pages/home/Tokens.tsx | 2 - .../SendTransactionActions.tsx | 1 + .../pages/messages/SendTransaction/index.tsx | 5 +- static/json/languages/en_US.json | 1 + yarn.lock | 200 +++++++++--------- 10 files changed, 112 insertions(+), 113 deletions(-) diff --git a/package.json b/package.json index a80af5ff52..c84996fb27 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "@ledgerhq/hw-transport-webhid": "6.29.3", "@metamask/browser-passworder": "4.1.0", "@metamask/eth-sig-util": "7.0.1", - "@orb-labs/orby-react": "0.0.27", + "@orb-labs/orby-react": "0.0.30", "@radix-ui/react-accordion": "1.1.2", "@radix-ui/react-context-menu": "2.1.1", "@radix-ui/react-dropdown-menu": "2.0.1", diff --git a/src/core/keychain/keychainTypes/readOnlyKeychain.ts b/src/core/keychain/keychainTypes/readOnlyKeychain.ts index bab0863643..31e515d410 100644 --- a/src/core/keychain/keychainTypes/readOnlyKeychain.ts +++ b/src/core/keychain/keychainTypes/readOnlyKeychain.ts @@ -9,7 +9,6 @@ import { KeychainType } from '~/core/types/keychainTypes'; import { logger } from '~/logger'; import { IKeychain, PrivateKey } from '../IKeychain'; -import { RainbowSigner } from '../RainbowSigner'; export interface SerializedReadOnlyKeychain { type: KeychainType.ReadOnlyKeychain; diff --git a/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts b/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts index b8bed3b130..cfb1b2f82b 100644 --- a/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts +++ b/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts @@ -23,6 +23,12 @@ export const useApproveAppRequestValidations = ({ return i18n.t('send.button_label.insufficient_asset'); } + if ( + operationSet?.status == CreateOperationsStatus.INSUFFICIENT_FUNDS_FOR_GAS + ) { + return i18n.t('send.button_label.insufficient_gas_funds'); + } + if (operationSet?.status == CreateOperationsStatus.NO_EXECUTION_PATH) { return i18n.t('send.button_label.no_execution_path'); } diff --git a/src/entries/popup/hooks/send/useSendValidations.ts b/src/entries/popup/hooks/send/useSendValidations.ts index 8a75510149..6a853251ac 100644 --- a/src/entries/popup/hooks/send/useSendValidations.ts +++ b/src/entries/popup/hooks/send/useSendValidations.ts @@ -143,6 +143,12 @@ export const useSendValidations = ({ return i18n.t('send.button_label.enter_address'); } + if ( + operationSet?.status == CreateOperationsStatus.INSUFFICIENT_FUNDS_FOR_GAS + ) { + return i18n.t('send.button_label.insufficient_gas_funds'); + } + if (operationSet?.status == CreateOperationsStatus.INSUFFICIENT_FUNDS) { return i18n.t('send.button_label.insufficient_asset', { symbol: asset?.symbol, diff --git a/src/entries/popup/pages/home/TabHeader.tsx b/src/entries/popup/pages/home/TabHeader.tsx index 820965f992..9e2cfc75fd 100644 --- a/src/entries/popup/pages/home/TabHeader.tsx +++ b/src/entries/popup/pages/home/TabHeader.tsx @@ -53,7 +53,6 @@ export function TabHeader({ cursor="text" > {userAssetsBalanceDisplay || ''} - {/* {balance || ''} */} ), [activeTab, currentCurrency, hideAssetBalances, userAssetsBalanceDisplay], diff --git a/src/entries/popup/pages/home/Tokens.tsx b/src/entries/popup/pages/home/Tokens.tsx index b3c9cfc743..79d94308fb 100644 --- a/src/entries/popup/pages/home/Tokens.tsx +++ b/src/entries/popup/pages/home/Tokens.tsx @@ -115,8 +115,6 @@ export function Tokens({ scrollY }: { scrollY: MotionValue }) { setCombinedAssets(convertStandardizedBalanceToParsedUserAssets(portfolio)); }, [portfolio]); - console.log('isLoading', isLoading); - const onCombineLists = useCallback( (standardizedTokenId?: string) => { if (!standardizedTokenId) { diff --git a/src/entries/popup/pages/messages/SendTransaction/SendTransactionActions.tsx b/src/entries/popup/pages/messages/SendTransaction/SendTransactionActions.tsx index 0b2d004ff3..13262c3928 100644 --- a/src/entries/popup/pages/messages/SendTransaction/SendTransactionActions.tsx +++ b/src/entries/popup/pages/messages/SendTransaction/SendTransactionActions.tsx @@ -32,6 +32,7 @@ export const SendTransactionActions = ({ const { buttonLabel } = useApproveAppRequestValidations({ session, dappStatus, + operationSet, }); const disabled = useMemo(() => { diff --git a/src/entries/popup/pages/messages/SendTransaction/index.tsx b/src/entries/popup/pages/messages/SendTransaction/index.tsx index 2c8af846c6..b344724340 100644 --- a/src/entries/popup/pages/messages/SendTransaction/index.tsx +++ b/src/entries/popup/pages/messages/SendTransaction/index.tsx @@ -112,7 +112,6 @@ export function SendTransaction({ statusSummary, ) ) { - const hash = statuses[statuses.length - 1].hash; const activeChainId = chainIdToUse( connectedToHardhat, connectedToHardhatOp, @@ -125,7 +124,7 @@ export function SendTransaction({ dappName: dappMetadata?.appName, }); - approveRequest(hash); + approveRequest(finalTransactionStatus?.hash); setWaitingForDevice(false); setLoading(false); } @@ -281,8 +280,6 @@ export function SendTransaction({ return activeSession?.chainId || ChainId.mainnet; }, [activeSession?.chainId]); - console.log('SendTransaction', operationSet); - return ( Date: Tue, 7 Jan 2025 17:26:45 -0500 Subject: [PATCH 14/15] releasing --- .github/workflows/publish-internal.yml | 9 ++++----- .github/workflows/publish-prod-chrome.yml | 11 ++++------- .gitignore | 2 ++ README.md | 4 ++-- package.json | 6 +++--- static/manifest.json | 12 ++++++------ 6 files changed, 21 insertions(+), 23 deletions(-) diff --git a/.github/workflows/publish-internal.yml b/.github/workflows/publish-internal.yml index c98852fd61..c2e1b4e5c7 100644 --- a/.github/workflows/publish-internal.yml +++ b/.github/workflows/publish-internal.yml @@ -1,4 +1,3 @@ - name: Publish to Chrome WebStore (Trusted Testers) on: @@ -9,7 +8,7 @@ on: jobs: build: runs-on: ubuntu-latest - concurrency: + concurrency: group: ${{ github.workflow }}-${{ github.ref }} steps: - uses: actions/checkout@v4 @@ -26,7 +25,7 @@ jobs: fi - uses: actions/setup-node@v4 with: - node-version: "20.16.0" + node-version: '20.16.0' cache: 'yarn' - name: Install deps via Yarn run: yarn setup @@ -52,12 +51,12 @@ jobs: - name: Archive the build artifact uses: actions/upload-artifact@v4 with: - name: rainbowbx-v${{ env.release_version }} + name: orbyplaygroundbx-v${{ env.release_version }} path: build/ - name: Submit to the chrome webstore uses: PlasmoHQ/bpp@v3 with: - artifact: ./rainbowbx.zip + artifact: ./orbyplaygroundbx.zip keys: ${{ secrets.BPP_KEYS_INTERNAL }} - name: Commit changes uses: EndBug/add-and-commit@v9 diff --git a/.github/workflows/publish-prod-chrome.yml b/.github/workflows/publish-prod-chrome.yml index 8b8846c4ca..86982df23b 100644 --- a/.github/workflows/publish-prod-chrome.yml +++ b/.github/workflows/publish-prod-chrome.yml @@ -1,4 +1,3 @@ - name: Publish to Chrome WebStore (Production) on: @@ -6,7 +5,7 @@ on: jobs: build: runs-on: ubuntu-latest - concurrency: + concurrency: group: ${{ github.workflow }}-${{ github.ref }} steps: - uses: actions/checkout@v4 @@ -16,7 +15,7 @@ jobs: token: ${{ secrets.REPO_TOKEN }} - uses: actions/setup-node@v4 with: - node-version: "20.16.0" + node-version: '20.16.0' cache: 'yarn' - name: Install deps via Yarn run: yarn setup @@ -36,14 +35,14 @@ jobs: - name: Archive the build artifact uses: actions/upload-artifact@v4 with: - name: rainbowbx-chrome-v${{ env.release_version }} + name: orbyplaygroundbx-chrome-v${{ env.release_version }} path: build/ - name: Zip it run: yarn zip - name: Submit to the chrome webstore uses: PlasmoHQ/bpp@v3.5.0 with: - artifact: ./rainbowbx.zip + artifact: ./orbyplaygroundbx.zip keys: ${{ secrets.BPP_KEYS_PROD }} - name: Create Sentry release uses: getsentry/action-release@v1 @@ -57,5 +56,3 @@ jobs: sourcemaps: ./build version: ${{ env.release_version }} url_prefix: 'chrome-extension://opfgelmcmbiajamepnmloijbpoleiama' - - diff --git a/.gitignore b/.gitignore index e21ba3d6e3..ba39d57b47 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ screenshots/ anvil*.log rainbowbx.xpi +orbyplaygroundbx.xpi +orbyplaygroundbx.zip .idea/** diff --git a/README.md b/README.md index f02727eb22..906b36bad5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## OrbyPlayground Extension -Built for speed. Built for power. Built for you. +Unify Accounts. Unify Ethereum. OrbyPlayground is a playground extension for showcasing the power of Orby. It is a fork of the Rainbow Extension with chain abstraction and gas abstraction features. @@ -12,7 +12,7 @@ OrbyPlayground is a playground extension for showcasing the power of Orby. It is - Gas abstraction for EOAs - Gas sponsorship for EOAs - Unified balances for assets across all supported chains. -- Auto-discovers tokens and assets across all supported chains: Mainnet, Base, Arbitrum, Optimism, and Polyon +- Auto-discovers tokens and assets across all supported chains: Mainnet, Base, Arbitrum, Optimism, and Polygon - Built-in Send and Swap using your unified balance to power all of your DeFi needs ...and a lot more. diff --git a/package.json b/package.json index c84996fb27..b1fdf8f1b6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "browser-extension", "license": "GPL-3.0-only", - "version": "1.5.48", + "version": "0.0.1", "scripts": { "//enable dev mode": "", "devmode:on": "sed -i'' -e 's/IS_DEV.*/IS_DEV=true/g' .env", @@ -50,7 +50,7 @@ "// runs audit against dep. tree": "", "audit:ci": "yarn audit-ci --moderate --config audit-ci.jsonc", "// Generates a zip file based on the build folder ready to upload to the chrome web store": "", - "zip": "cd build/ && zip -r ../rainbowbx.zip *", + "zip": "cd build/ && zip -r ../orbyplaygroundbx.zip *", "// Build and zip": "", "bundle": "yarn build && yarn zip", "// Runs tests": "", @@ -73,7 +73,7 @@ "e2e": "yarn e2e:mac", "firefox:manifest": "node scripts/firefox-manifest.js", "firefox:build": "yarn build && yarn firefox:manifest", - "firefox:zip": "yarn zip && mv rainbowbx.zip rainbowbx.xpi", + "firefox:zip": "yarn zip && mv orbyplaygroundbx.zip orbyplaygroundbx.xpi", "firefox:lint": "yarn web-ext lint --source-dir ./build", "firefox:run": "yarn web-ext run --source-dir ./build/ -f='/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox'" }, diff --git a/static/manifest.json b/static/manifest.json index 2446b97589..70503cb762 100644 --- a/static/manifest.json +++ b/static/manifest.json @@ -11,7 +11,7 @@ }, "default_popup": "popup.html" }, - "author": "https://rainbow.me", + "author": "https://www.orblabs.xyz/", "background": { "service_worker": "background.js" }, @@ -31,7 +31,7 @@ "extension_pages": "frame-ancestors 'none'; script-src 'self'; object-src 'self'; connect-src 'self'" }, "default_locale": "en_US", - "description": "DEV VERSION", + "description": "OrbyPlayground is a playground extension for showcasing the power of Orby.", "host_permissions": ["http://*/*", "https://*/*", "wss://*/*"], "icons": { "16": "images/icon-16.png", @@ -44,7 +44,7 @@ }, "manifest_version": 3, "minimum_chrome_version": "88", - "name": "Rainbow DEVELOPMENT BUILD", + "name": "OrbyPlayground", "permissions": [ "activeTab", "clipboardWrite", @@ -54,8 +54,8 @@ "unlimitedStorage", "notifications" ], - "short_name": "Rainbow", - "version": "1.5.48", + "short_name": "OrbyPlayground", + "version": "0.0.1", "web_accessible_resources": [ { "matches": [""], @@ -76,7 +76,7 @@ "chromeos": "Alt+Shift+R", "linux": "Alt+Shift+R" }, - "description": "Open the Rainbow Wallet extension" + "description": "Open the OrbyPlayground Wallet extension" } } } From 18229dea92d80e44ebdee0d6d47480bad6228fc1 Mon Sep 17 00:00:00 2001 From: felimadu Date: Tue, 15 Apr 2025 12:11:06 -0400 Subject: [PATCH 15/15] loading... --- CHANGELOG.md | 6 + e2e/helpers.ts | 6 +- e2e/parallel/newWalletFlow.test.ts | 2 +- e2e/parallel/watchWalletFlow.test.ts | 2 +- .../1_appInteractionsFlow.test.ts | 2 +- .../2_dappInteractionFlow.test.ts | 11 +- .../3_dappAccountsSwitcher.test.ts | 2 +- e2e/serial/swap/1_swapFlow1.test.ts | 2 +- e2e/serial/swap/2_swapFlow2.test.ts | 2 +- lavamoat/build-webpack/policy.json | 38 +- package.json | 19 +- src/analytics/screen.ts | 3 +- src/core/keychain/IKeychain.ts | 5 + src/core/keychain/KeychainManager.ts | 17 +- src/core/keychain/index.ts | 5 + .../keychainTypes/hardwareWalletKeychain.ts | 8 +- src/core/keychain/keychainTypes/hdKeychain.ts | 49 +- .../keychain/keychainTypes/keyPairKeychain.ts | 18 +- .../keychainTypes/readOnlyKeychain.ts | 15 +- src/core/keychain/utils.ts | 6 +- src/core/network/nfts.ts | 4 +- src/core/raps/unlockAndCrosschainSwap.ts | 5 +- src/core/raps/unlockAndSwap.ts | 5 +- src/core/raps/utils.ts | 3 +- src/core/references/assets.ts | 13 + src/core/references/rawImages.ts | 2 +- src/core/resources/nfts/collections.ts | 5 +- .../resources/transactions/transaction.ts | 5 +- .../state/currentSettings/currentAddress.ts | 20 +- src/core/state/rainbowChains/index.ts | 5 +- src/core/types/rpcMethods.ts | 2 + src/core/utils/ethereum.ts | 6 +- src/core/utils/nfts.ts | 17 +- src/core/utils/orb.ts | 127 ++- src/core/utils/signMessages.tsx | 10 + src/core/utils/transactions.ts | 5 +- .../handlers/handleProviderRequest.ts | 22 +- .../handlers/rnbwHandleProviderRequest.ts | 511 +++++++++ src/entries/background/index.ts | 2 + src/entries/inpage/RainbowProvider.ts | 287 +++++ src/entries/inpage/index.ts | 9 +- src/entries/popup/App.tsx | 24 +- .../popup/components/CommandK/useCommands.tsx | 90 +- .../popup/components/CommandK/utils.ts | 3 +- .../FlyingRainbows/FlyingRainbows.tsx | 20 +- .../ImportWallet/ImportWalletSelection.tsx | 13 +- .../ImportWalletSelectionEdit.tsx | 2 + .../ImportWalletViaPrivateKey.tsx | 11 +- .../SwitchMenu/SwitchNetworkMenu.tsx | 13 +- .../components/WatchWallet/WatchWallet.tsx | 3 + src/entries/popup/handlers/ledger.ts | 6 +- src/entries/popup/handlers/trezor.ts | 16 +- .../popup/hooks/send/useAllFilteredWallets.ts | 3 +- src/entries/popup/hooks/useAppSession.ts | 63 +- .../popup/hooks/useInfiniteTransactionList.ts | 4 + .../popup/hooks/useSearchCurrencyLists.ts | 3 +- .../popup/hooks/useUserAssetsBalance.ts | 10 +- src/entries/popup/hooks/useWalletsSummary.ts | 3 +- .../pages/messages/ApproveAppRequest.tsx | 4 + .../pages/messages/BottomActions/index.tsx | 37 +- .../RequestAccountsActions.tsx | 5 + .../pages/messages/RequestAccounts/index.tsx | 34 +- .../SendTransaction/SendTransactionsInfo.tsx | 10 +- .../pages/messages/SendTransaction/index.tsx | 82 +- .../messages/SignMessage/SignMessageInfo.tsx | 2 - .../pages/messages/SignMessage/index.tsx | 77 +- src/entries/popup/pages/send/index.tsx | 14 +- .../swap/SwapReviewSheet/SwapReviewSheet.tsx | 7 +- src/entries/popup/pages/swap/index.tsx | 10 +- .../popup/pages/swap/useSwapButton.tsx | 18 +- src/entries/popup/pages/unlock/index.tsx | 2 +- src/entries/popup/pages/walletReady/index.tsx | 2 +- .../walletSwitcher/createWalletPrompt.tsx | 3 + .../pages/welcome/ImportOrCreateWallet.tsx | 10 +- src/entries/popup/pages/welcome/index.tsx | 9 +- .../popup/utils/emojiAvatarForAddress.ts | 4 +- src/entries/wallet-standard/src/account.ts | 68 ++ src/entries/wallet-standard/src/icon.ts | 4 + src/entries/wallet-standard/src/index.ts | 2 + src/entries/wallet-standard/src/initialize.ts | 7 + src/entries/wallet-standard/src/register.ts | 83 ++ src/entries/wallet-standard/src/solana.ts | 40 + src/entries/wallet-standard/src/util.ts | 23 + src/entries/wallet-standard/src/wallet.ts | 351 ++++++ src/entries/wallet-standard/src/window.ts | 50 + static/allowlist.json | 3 + static/assets/rainbow/og-orblabs.png | Bin 0 -> 4534 bytes static/assets/rainbow/og-orby.png | Bin 0 -> 8282 bytes static/assets/rainbow/og-orby@2x.png | Bin 0 -> 25665 bytes static/images/icon-16.png | Bin 815 -> 630 bytes static/images/icon-16@2x.png | Bin 2057 -> 1563 bytes static/images/icon-16@32x.png | Bin 155476 -> 114042 bytes static/images/icon-16@4x.png | Bin 5556 -> 4544 bytes static/images/icon-16@8x.png | Bin 15825 -> 13586 bytes static/images/icon-19.png | Bin 984 -> 814 bytes static/images/icon-19@2x.png | Bin 2655 -> 2042 bytes static/json/languages/en_US.json | 88 +- static/json/languages/es_419.json | 4 +- static/manifest.json | 2 +- yarn.lock | 1004 +++++------------ 100 files changed, 2535 insertions(+), 1094 deletions(-) create mode 100644 src/entries/background/handlers/rnbwHandleProviderRequest.ts create mode 100644 src/entries/inpage/RainbowProvider.ts create mode 100644 src/entries/wallet-standard/src/account.ts create mode 100644 src/entries/wallet-standard/src/icon.ts create mode 100644 src/entries/wallet-standard/src/index.ts create mode 100644 src/entries/wallet-standard/src/initialize.ts create mode 100644 src/entries/wallet-standard/src/register.ts create mode 100644 src/entries/wallet-standard/src/solana.ts create mode 100644 src/entries/wallet-standard/src/util.ts create mode 100644 src/entries/wallet-standard/src/wallet.ts create mode 100644 src/entries/wallet-standard/src/window.ts create mode 100644 static/assets/rainbow/og-orblabs.png create mode 100644 static/assets/rainbow/og-orby.png create mode 100644 static/assets/rainbow/og-orby@2x.png diff --git a/CHANGELOG.md b/CHANGELOG.md index e7901e9912..1db3447785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/) ### Testing +## [v0.0.2](https://github.com/orb-labs/playground-extension/releases/tag/v0.0.2) + +### Added + +- Updating the orby npm package + ## [v0.0.1](https://github.com/orb-labs/playground-extension/releases/tag/v0.0.1) ### Added diff --git a/e2e/helpers.ts b/e2e/helpers.ts index 5c54b2da6e..edb50d6717 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -838,7 +838,7 @@ export async function importHardwareWalletFlow( }); await findElementByTestIdAndClick({ id: 'set-password-button', driver }); await delayTime('long'); - await findElementByText(driver, 'Rainbow is ready to use'); + await findElementByText(driver, 'OrbyPlayground is ready to use'); } export async function importWalletFlowUsingKeyboardNavigation( @@ -931,7 +931,7 @@ export async function importWalletFlowUsingKeyboardNavigation( await delayTime('long'); const welcomeText = await findElementByText( driver, - 'Rainbow is ready to use', + 'OrbyPlayground is ready to use', ); expect(welcomeText).toBeTruthy(); } @@ -1010,7 +1010,7 @@ export async function importWalletFlow( await delayTime('long'); const welcomeText = await findElementByText( driver, - 'Rainbow is ready to use', + 'OrbyPlayground is ready to use', ); expect(welcomeText).toBeTruthy(); } diff --git a/e2e/parallel/newWalletFlow.test.ts b/e2e/parallel/newWalletFlow.test.ts index 69918972dd..118465ae64 100644 --- a/e2e/parallel/newWalletFlow.test.ts +++ b/e2e/parallel/newWalletFlow.test.ts @@ -61,7 +61,7 @@ describe('New wallet flow', () => { await findElementByTestIdAndClick({ id: 'set-password-button', driver }); await delayTime('long'); - await findElementByText(driver, 'Rainbow is ready to use'); + await findElementByText(driver, 'OrbyPlayground is ready to use'); }); it('should display account name', async () => { diff --git a/e2e/parallel/watchWalletFlow.test.ts b/e2e/parallel/watchWalletFlow.test.ts index f46141ad11..1811523c2f 100644 --- a/e2e/parallel/watchWalletFlow.test.ts +++ b/e2e/parallel/watchWalletFlow.test.ts @@ -79,7 +79,7 @@ describe('Watch wallet then add more and switch between them', () => { await confirmPasswordInput.sendKeys('test1234'); await findElementByTestIdAndClick({ id: 'set-password-button', driver }); - await findElementByText(driver, 'Rainbow is ready to use'); + await findElementByText(driver, 'OrbyPlayground is ready to use'); }); it('should display watched account name', async () => { diff --git a/e2e/serial/dappInteractions/1_appInteractionsFlow.test.ts b/e2e/serial/dappInteractions/1_appInteractionsFlow.test.ts index 339267ce56..2ad6969100 100644 --- a/e2e/serial/dappInteractions/1_appInteractionsFlow.test.ts +++ b/e2e/serial/dappInteractions/1_appInteractionsFlow.test.ts @@ -133,7 +133,7 @@ describe.runIf(browser !== 'firefox')('App interactions flow', () => { }); await findElementByTestIdAndClick({ id: 'set-password-button', driver }); await delayTime('long'); - await findElementByText(driver, 'Rainbow is ready to use'); + await findElementByText(driver, 'OrbyPlayground is ready to use'); }); it('should be able to go to setings', async () => { diff --git a/e2e/serial/dappInteractions/2_dappInteractionFlow.test.ts b/e2e/serial/dappInteractions/2_dappInteractionFlow.test.ts index aa0f12c92c..1b652eb994 100644 --- a/e2e/serial/dappInteractions/2_dappInteractionFlow.test.ts +++ b/e2e/serial/dappInteractions/2_dappInteractionFlow.test.ts @@ -1,6 +1,7 @@ import 'chromedriver'; import 'geckodriver'; import { getAddress } from '@ethersproject/address'; +import { validateAndFormatAddress } from '@orb-labs/orby-core'; import { WebDriver } from 'selenium-webdriver'; import { afterAll, @@ -101,7 +102,7 @@ describe.runIf(browser !== 'firefox')('App interactions flow', () => { }); await findElementByTestIdAndClick({ id: 'set-password-button', driver }); await delayTime('long'); - await findElementByText(driver, 'Rainbow is ready to use'); + await findElementByText(driver, 'OrbyPlayground is ready to use'); }); it('should be able to go to setings', async () => { @@ -223,7 +224,9 @@ describe.runIf(browser !== 'firefox')('App interactions flow', () => { id: 'signTypedDataV3VerifyResult', driver, }); - expect(result).toBe(TEST_VARIABLES.SEED_WALLET.ADDRESS.toLowerCase()); + expect(result).toBe( + validateAndFormatAddress(TEST_VARIABLES.SEED_WALLET.ADDRESS), + ); }); it('should be able to sign typed data (v4)', async () => { @@ -262,7 +265,9 @@ describe.runIf(browser !== 'firefox')('App interactions flow', () => { id: 'signTypedDataV4VerifyResult', driver, }); - expect(result).toBe(TEST_VARIABLES.SEED_WALLET.ADDRESS.toLowerCase()); + expect(result).toBe( + validateAndFormatAddress(TEST_VARIABLES.SEED_WALLET.ADDRESS), + ); }); it('should be able to switch network to hardhat', async () => { diff --git a/e2e/serial/dappInteractions/3_dappAccountsSwitcher.test.ts b/e2e/serial/dappInteractions/3_dappAccountsSwitcher.test.ts index 6a83b37e80..9db7b685b8 100644 --- a/e2e/serial/dappInteractions/3_dappAccountsSwitcher.test.ts +++ b/e2e/serial/dappInteractions/3_dappAccountsSwitcher.test.ts @@ -96,7 +96,7 @@ describe.runIf(browser !== 'firefox')('Dapp accounts switcher flow', () => { }); await findElementByTestIdAndClick({ id: 'set-password-button', driver }); await delayTime('long'); - await findElementByText(driver, 'Rainbow is ready to use'); + await findElementByText(driver, 'OrbyPlayground is ready to use'); }); it('should be able to go to setings', async () => { diff --git a/e2e/serial/swap/1_swapFlow1.test.ts b/e2e/serial/swap/1_swapFlow1.test.ts index aee9c56fda..44a76e8b82 100644 --- a/e2e/serial/swap/1_swapFlow1.test.ts +++ b/e2e/serial/swap/1_swapFlow1.test.ts @@ -99,7 +99,7 @@ it('should be able import a wallet via pk', async () => { }); await findElementByTestIdAndClick({ id: 'set-password-button', driver }); await delayTime('long'); - await findElementByText(driver, 'Rainbow is ready to use'); + await findElementByText(driver, 'OrbyPlayground is ready to use'); }); it('should be able to go to setings', async () => { diff --git a/e2e/serial/swap/2_swapFlow2.test.ts b/e2e/serial/swap/2_swapFlow2.test.ts index f86412e473..be1acfd0fa 100644 --- a/e2e/serial/swap/2_swapFlow2.test.ts +++ b/e2e/serial/swap/2_swapFlow2.test.ts @@ -108,7 +108,7 @@ describe('Swap Flow 2', () => { }); await findElementByTestIdAndClick({ id: 'set-password-button', driver }); await delayTime('long'); - await findElementByText(driver, 'Rainbow is ready to use'); + await findElementByText(driver, 'OrbyPlayground is ready to use'); }); it('should be able to go to setings', async () => { diff --git a/lavamoat/build-webpack/policy.json b/lavamoat/build-webpack/policy.json index b58bedc4ed..19440ec708 100644 --- a/lavamoat/build-webpack/policy.json +++ b/lavamoat/build-webpack/policy.json @@ -1,20 +1,5 @@ { "resources": { - "@orb-labs/orby-react>tailwindcss>sucrase>@jridgewell/gen-mapping": { - "globals": { - "define": true - }, - "packages": { - "@orb-labs/orby-react>tailwindcss>sucrase>@jridgewell/gen-mapping>@jridgewell/set-array": true, - "jest>@jest/core>@jest/reporters>@jridgewell/trace-mapping": true, - "jest>@jest/core>@jest/reporters>@jridgewell/trace-mapping>@jridgewell/sourcemap-codec": true - } - }, - "@orb-labs/orby-react>tailwindcss>sucrase>@jridgewell/gen-mapping>@jridgewell/set-array": { - "globals": { - "define": true - } - }, "@testing-library/react>@testing-library/dom>@babel/code-frame": { "globals": { "console.warn": true, @@ -574,8 +559,8 @@ "define": true }, "packages": { - "@orb-labs/orby-react>tailwindcss>sucrase>@jridgewell/gen-mapping>@jridgewell/set-array": true, - "jest>@jest/core>@jest/reporters>@jridgewell/trace-mapping>@jridgewell/sourcemap-codec": true + "jest>@jest/core>@jest/reporters>@jridgewell/trace-mapping>@jridgewell/sourcemap-codec": true, + "jest>@jest/core>jest-snapshot>@babel/generator>@jridgewell/gen-mapping>@jridgewell/set-array": true } }, "eslint-config-rainbow>eslint-import-resolver-babel-module>@babel/core>@babel/helper-compilation-targets": { @@ -1109,11 +1094,26 @@ "console.error": true }, "packages": { - "@orb-labs/orby-react>tailwindcss>sucrase>@jridgewell/gen-mapping": true, + "jest>@jest/core>jest-snapshot>@babel/generator>@jridgewell/gen-mapping": true, "jest>@jest/core>jest-snapshot>@babel/generator>jsesc": true, "jest>@jest/core>jest-snapshot>@babel/types": true } }, + "jest>@jest/core>jest-snapshot>@babel/generator>@jridgewell/gen-mapping": { + "globals": { + "define": true + }, + "packages": { + "jest>@jest/core>@jest/reporters>@jridgewell/trace-mapping": true, + "jest>@jest/core>@jest/reporters>@jridgewell/trace-mapping>@jridgewell/sourcemap-codec": true, + "jest>@jest/core>jest-snapshot>@babel/generator>@jridgewell/gen-mapping>@jridgewell/set-array": true + } + }, + "jest>@jest/core>jest-snapshot>@babel/generator>@jridgewell/gen-mapping>@jridgewell/set-array": { + "globals": { + "define": true + } + }, "jest>@jest/core>jest-snapshot>@babel/generator>jsesc": { "globals": { "Buffer.isBuffer": true @@ -1173,7 +1173,7 @@ "console.warn": true }, "packages": { - "@orb-labs/orby-react>tailwindcss>sucrase>@jridgewell/gen-mapping": true, + "jest>@jest/core>jest-snapshot>@babel/generator>@jridgewell/gen-mapping": true, "jest>@jest/core>jest-snapshot>@babel/generator>jsesc": true, "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/generator>@jridgewell/trace-mapping": true, "jest>@jest/core>jest-snapshot>@babel/traverse>@babel/types": true diff --git a/package.json b/package.json index b1fdf8f1b6..a567568a98 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "browser-extension", "license": "GPL-3.0-only", - "version": "0.0.1", + "version": "0.0.5", "scripts": { "//enable dev mode": "", "devmode:on": "sed -i'' -e 's/IS_DEV.*/IS_DEV=true/g' .env", @@ -46,7 +46,7 @@ "// Checks the lockfile for changes": "", "check-lockfile": "./scripts/check-lockfile.sh", "// Installs deps, runs allowed scripts, patches packages and generates lavamoat policies": "", - "setup": "yarn install && yarn ds:install && yarn ds:generate-symbols && yarn fetch:networks && yarn allow-scripts && yarn patch-package && yarn policy", + "setup": "yarn install && yarn ds:install && yarn ds:generate-symbols && yarn fetch:networks && yarn allow-scripts auto && yarn patch-package && yarn policy", "// runs audit against dep. tree": "", "audit:ci": "yarn audit-ci --moderate --config audit-ci.jsonc", "// Generates a zip file based on the build folder ready to upload to the chrome web store": "", @@ -101,7 +101,7 @@ "@ledgerhq/hw-transport-webhid": "6.29.3", "@metamask/browser-passworder": "4.1.0", "@metamask/eth-sig-util": "7.0.1", - "@orb-labs/orby-react": "0.0.30", + "@orb-labs/orby-react": "0.0.57", "@radix-ui/react-accordion": "1.1.2", "@radix-ui/react-context-menu": "2.1.1", "@radix-ui/react-dropdown-menu": "2.0.1", @@ -114,6 +114,9 @@ "@rudderstack/analytics-js-service-worker": "3.0.6", "@scure/bip39": "1.2.1", "@sentry/browser": "8.33.0", + "@solana/wallet-adapter-base": "0.9.23", + "@solana/wallet-standard-features": "1.3.0", + "@solana/web3.js": "1.98.0", "@tanstack/query-async-storage-persister": "5.35.1", "@tanstack/react-query": "5.35.1", "@tanstack/react-query-persist-client": "5.35.1", @@ -124,7 +127,11 @@ "@vanilla-extract/css-utils": "0.1.2", "@vanilla-extract/dynamic": "2.0.2", "@vanilla-extract/sprinkles": "1.5.0", + "@wallet-standard/base": "1.1.0", + "@wallet-standard/features": "1.1.0", "bignumber.js": "9.0.1", + "bip39": "3.1.0", + "bs58": "6.0.0", "buffer": "6.0.3", "check-password-strength": "2.0.7", "chroma-js": "2.4.2", @@ -133,6 +140,7 @@ "currency.js": "2.0.4", "date-fns": "2.29.3", "deepmerge": "4.2.2", + "ed25519-hd-key": "1.3.0", "ethereum-cryptography": "2.1.2", "eventemitter3": "4.0.7", "firebase": "10.12.3", @@ -336,7 +344,10 @@ "viem>ws>bufferutil": false, "viem>ws>utf-8-validate": false, "wagmi>@wagmi/connectors>@coinbase/wallet-sdk>keccak": false, - "wagmi>@wagmi/connectors>@metamask/sdk>eciesjs>secp256k1": false + "wagmi>@wagmi/connectors>@metamask/sdk>eciesjs>secp256k1": false, + "@solana/web3.js>bigint-buffer": false, + "wagmi>@wagmi/connectors>@metamask/sdk>@metamask/sdk-communication-layer>bufferutil": false, + "wagmi>@wagmi/connectors>@metamask/sdk>@metamask/sdk-communication-layer>utf-8-validate": false } } } diff --git a/src/analytics/screen.ts b/src/analytics/screen.ts index 67012471ec..8062a84fc2 100644 --- a/src/analytics/screen.ts +++ b/src/analytics/screen.ts @@ -1,4 +1,5 @@ /* eslint sort-keys: "error"*/ +import { validateAndLowerCase } from '@orb-labs/orby-core'; import { ROUTES } from '~/entries/popup/urls'; @@ -11,6 +12,6 @@ import { ROUTES } from '~/entries/popup/urls'; export const screen = Object.fromEntries( Object.entries(ROUTES).map(([key, value]) => [ value, - key.toLowerCase().replaceAll('__', '.'), + validateAndLowerCase(key)?.replaceAll('__', '.'), ]), ); diff --git a/src/core/keychain/IKeychain.ts b/src/core/keychain/IKeychain.ts index 32723e6b16..a3866bbaf8 100644 --- a/src/core/keychain/IKeychain.ts +++ b/src/core/keychain/IKeychain.ts @@ -1,13 +1,17 @@ import { Signer } from '@ethersproject/abstract-signer'; import { Mnemonic } from '@ethersproject/hdnode'; import { Wallet } from '@ethersproject/wallet'; +import { Keypair } from '@solana/web3.js'; import { Address } from 'viem'; export type PrivateKey = string; export type TWallet = Omit & { address: Address; + evmAddress: string; + svmAddress: string; privateKey: PrivateKey; + svmKey: Keypair; }; export interface IKeychain { @@ -18,6 +22,7 @@ export interface IKeychain { addAccountAtIndex(index: number, address: Address): Promise
; getAccounts(): Promise>; getSigner(address: Address): Signer; + getKeyPair(_address: Address): Keypair; exportAccount(address: Address): Promise; exportKeychain(address: Address): Promise; removeAccount(address: Address): Promise; diff --git a/src/core/keychain/KeychainManager.ts b/src/core/keychain/KeychainManager.ts index 95f4341a7e..f189ccfa0e 100644 --- a/src/core/keychain/KeychainManager.ts +++ b/src/core/keychain/KeychainManager.ts @@ -7,7 +7,9 @@ import { encryptWithKey, importKey, } from '@metamask/browser-passworder'; +import { validateAndFormatAddress } from '@orb-labs/orby-core'; import * as Sentry from '@sentry/browser'; +import { Keypair } from '@solana/web3.js'; import { Address } from 'viem'; import { LocalStorage, SessionStorage } from '../storage'; @@ -87,6 +89,7 @@ class KeychainManager { }), ); await privates.get(this).persist(); + this.state.isUnlocked = true; } } catch (e) { @@ -313,7 +316,7 @@ class KeychainManager { ); if (matchingExistingAccount) { const existingAccountWallet = await this.getWallet( - matchingExistingAccount, + matchingExistingAccount as Address, ); if (existingAccountWallet.type !== KeychainType.ReadOnlyKeychain) { throw new Error(`Duplicate account ${newAccounts[i]}`); @@ -416,7 +419,8 @@ class KeychainManager { async removeAccount(address: Address) { for (let i = 0; i < this.state.keychains.length; i++) { const accounts = await this.state.keychains[i].getAccounts(); - if (accounts.includes(address)) { + const addresses = accounts.map((address) => address); + if (addresses.includes(address)) { await this.state.keychains[i].removeAccount(address); await privates.get(this).removeEmptyKeychainsIfNeeded(); } @@ -537,7 +541,9 @@ class KeychainManager { const keychain = this.state.keychains[i]; const accounts = await keychain.getAccounts(); if ( - accounts.map((a) => a.toLowerCase()).includes(address.toLowerCase()) + accounts + .map((a) => validateAndFormatAddress(a)) + .includes(validateAndFormatAddress(address)) ) { return keychain; } @@ -550,6 +556,11 @@ class KeychainManager { const keychain = await this.getKeychain(address); return keychain.getSigner(address); } + + async getKeyPair(address: Address): Promise { + const keychain = await this.getKeychain(address); + return keychain.getKeyPair(address); + } } export const keychainManager = new KeychainManager(); diff --git a/src/core/keychain/index.ts b/src/core/keychain/index.ts index a5b4ce79b9..b6e7a70545 100644 --- a/src/core/keychain/index.ts +++ b/src/core/keychain/index.ts @@ -115,6 +115,7 @@ export const deriveAccountsFromSecret = async ( type: KeychainType.HdKeychain, mnemonic: secret, }); + console.log('accounts', accounts); break; } case EthereumWalletType.privateKey: { @@ -128,6 +129,8 @@ export const deriveAccountsFromSecret = async ( accounts = await keychainManager.deriveAccounts({ type: KeychainType.ReadOnlyKeychain, address: secret as Address, + evmAddress: '', + svmAddress: '', }); break; } @@ -188,6 +191,8 @@ export const importWallet = async ( const keychain = await keychainManager.importKeychain({ type: KeychainType.ReadOnlyKeychain, address: secret as Address, + evmAddress: '', + svmAddress: '', }); const address = (await keychain.getAccounts())[0]; return address; diff --git a/src/core/keychain/keychainTypes/hardwareWalletKeychain.ts b/src/core/keychain/keychainTypes/hardwareWalletKeychain.ts index a8c047190b..b21aa65757 100644 --- a/src/core/keychain/keychainTypes/hardwareWalletKeychain.ts +++ b/src/core/keychain/keychainTypes/hardwareWalletKeychain.ts @@ -1,5 +1,6 @@ import { Signer } from '@ethersproject/abstract-signer'; import { Wallet } from '@ethersproject/wallet'; +import { Keypair } from '@solana/web3.js'; import { Address } from 'viem'; import { mainnet } from 'viem/chains'; @@ -89,6 +90,11 @@ export class HardwareWalletKeychain implements IKeychain { ); } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getKeyPair(_address: Address): Keypair { + throw new Error('[HardwareWalletKeychain] Method not implemented.'); + } + getPath(address: Address): string { const wallet = privates .get(this) @@ -130,7 +136,7 @@ export class HardwareWalletKeychain implements IKeychain { getAccounts(): Promise> { const addresses = privates .get(this) - .wallets.map((wallet: Wallet) => (wallet as Wallet).address as Address); + .wallets.map((wallet: Wallet) => wallet as Wallet); return Promise.resolve(addresses); } diff --git a/src/core/keychain/keychainTypes/hdKeychain.ts b/src/core/keychain/keychainTypes/hdKeychain.ts index 1749ea5294..31950d1ce0 100644 --- a/src/core/keychain/keychainTypes/hdKeychain.ts +++ b/src/core/keychain/keychainTypes/hdKeychain.ts @@ -2,8 +2,12 @@ import { Signer } from '@ethersproject/abstract-signer'; import { BytesLike } from '@ethersproject/bytes'; import { Wallet } from '@ethersproject/wallet'; +import { validateAndFormatAddress } from '@orb-labs/orby-core'; import * as bip39 from '@scure/bip39'; import { wordlist as englishWordlist } from '@scure/bip39/wordlists/english'; +import { Keypair } from '@solana/web3.js'; +import * as bip392 from 'bip39'; +import { derivePath } from 'ed25519-hd-key'; import { HDKey } from 'ethereum-cryptography/hdkey'; import { bytesToHex } from 'ethereum-cryptography/utils'; import { Address } from 'viem'; @@ -19,6 +23,9 @@ import { autoDiscoverAccounts } from '../utils'; export interface RainbowHDKey extends HDKey { address: Address; + evmAddress: string; + svmAddress: string; + svmKey: Keypair; } type SupportedHDPath = "m/44'/60'/0'/0"; @@ -43,7 +50,7 @@ const privates = new WeakMap< hdPath: SupportedHDPath; getWalletForAddress(address: Address): Wallet | undefined; deriveWallet(index: number): RainbowHDKey; - addAccount(index: number): Wallet; + addAccount(index: number): TWallet; } >(); @@ -65,7 +72,10 @@ export class HdKeychain implements IKeychain { .get(this)! .wallets.find( ({ wallet }) => - wallet.address.toLowerCase() === address.toLowerCase(), + validateAndFormatAddress(wallet.svmAddress) === + validateAndFormatAddress(address) || + validateAndFormatAddress(wallet.evmAddress) === + validateAndFormatAddress(address), )?.wallet; }, deriveWallet: (index: number): RainbowHDKey => { @@ -77,12 +87,24 @@ export class HdKeychain implements IKeychain { const root = hdNode.derive(_privates.hdPath); const derivedWallet = root.deriveChild(index) as RainbowHDKey; const pkeyHex = bytesToHex(derivedWallet.privateKey as Uint8Array); + + const seed2 = bip392.mnemonicToSeedSync(_privates.mnemonic); + const derivedKey1 = derivePath( + `m/44'/501'/${index}'/0'`, + seed2.toString('hex'), + ); + const keypair = Keypair.fromSeed(Uint8Array.from(derivedKey1.key)); + const wallet = new Wallet(pkeyHex) as TWallet; derivedWallet.address = wallet.address; + derivedWallet.evmAddress = derivedWallet.address; + derivedWallet.svmAddress = keypair.publicKey.toString(); + derivedWallet.svmKey = keypair; + return derivedWallet; }, - addAccount: (index: number): Wallet => { + addAccount: (index: number): TWallet => { const _privates = privates.get(this)!; const derivedWallet = _privates.deriveWallet(index); @@ -103,6 +125,10 @@ export class HdKeychain implements IKeychain { const wallet = new Wallet( derivedWallet.privateKey as BytesLike, ) as TWallet; + + wallet.evmAddress = derivedWallet.evmAddress; + wallet.svmAddress = derivedWallet.svmAddress; + wallet.svmKey = derivedWallet.svmKey; _privates.wallets.push({ wallet, index: derivedWallet.index }); return wallet; }, @@ -122,6 +148,13 @@ export class HdKeychain implements IKeychain { return new RainbowSigner(provider, wallet.privateKey, wallet.address); } + getKeyPair(address: Address): Keypair { + const _privates = privates.get(this)!; + const wallet = _privates!.getWalletForAddress(address) as TWallet; + if (!wallet) throw new Error('[HdKeychain] Account not found'); + return wallet.svmKey; + } + async serialize(): Promise { const _privates = privates.get(this)!; if (!_privates.mnemonic) throw new Error('No mnemonic'); @@ -204,12 +237,18 @@ export class HdKeychain implements IKeychain { (i) => i !== index, ); - return Promise.resolve(newAccount.address as Address); + return Promise.resolve(newAccount.address); } getAccounts(): Promise> { const _privates = privates.get(this)!; - const addresses = _privates.wallets.map(({ wallet }) => wallet.address); + const addresses = _privates.wallets + .map(({ wallet }) => [ + wallet.evmAddress as Address, + wallet.svmAddress as Address, + ]) + .flat(); + return Promise.resolve(addresses); } diff --git a/src/core/keychain/keychainTypes/keyPairKeychain.ts b/src/core/keychain/keychainTypes/keyPairKeychain.ts index 366f8dd18e..6b0e108cda 100644 --- a/src/core/keychain/keychainTypes/keyPairKeychain.ts +++ b/src/core/keychain/keychainTypes/keyPairKeychain.ts @@ -2,6 +2,7 @@ import { Signer } from '@ethersproject/abstract-signer'; import { Mnemonic } from '@ethersproject/hdnode'; import { Wallet } from '@ethersproject/wallet'; +import { Keypair } from '@solana/web3.js'; import { Address } from 'viem'; import { mainnet } from 'viem/chains'; @@ -31,13 +32,19 @@ export class KeyPairKeychain implements IKeychain { this.deserialize(options); } - getSigner(address: Address): Signer { + getSigner(_address: Address): Signer { const provider = getProvider({ chainId: mainnet.id }); const wallet = privates.get(this).wallets[0] as TWallet; if (!wallet) throw new Error('Account not found'); return new RainbowSigner(provider, wallet.privateKey, wallet.address); } + getKeyPair(_address: Address): Keypair { + const wallet = privates.get(this).wallets[0] as TWallet; + if (!wallet) throw new Error('[KeyPairKeychain] Account not found'); + return wallet.svmKey; + } + async serialize(): Promise { return { privateKey: (privates.get(this).wallets[0] as Wallet) @@ -59,10 +66,13 @@ export class KeyPairKeychain implements IKeychain { } getAccounts(): Promise> { - const addresses = privates + console.log(privates.get(this).wallets); + return privates .get(this) - .wallets.map((wallet: Wallet) => (wallet as Wallet).address as Address); - return Promise.resolve(addresses); + .wallets.map((wallet: TWallet) => { + return [wallet.evmAddress, wallet.svmAddress]; + }) + .flatMap(); } async exportAccount(address: Address): Promise { diff --git a/src/core/keychain/keychainTypes/readOnlyKeychain.ts b/src/core/keychain/keychainTypes/readOnlyKeychain.ts index 31e515d410..5191d7d709 100644 --- a/src/core/keychain/keychainTypes/readOnlyKeychain.ts +++ b/src/core/keychain/keychainTypes/readOnlyKeychain.ts @@ -3,6 +3,7 @@ import { Signer } from '@ethersproject/abstract-signer'; import { isAddress } from '@ethersproject/address'; import { Mnemonic } from '@ethersproject/hdnode'; import { Wallet } from '@ethersproject/wallet'; +import { Keypair } from '@solana/web3.js'; import { Address } from 'viem'; import { KeychainType } from '~/core/types/keychainTypes'; @@ -13,10 +14,14 @@ import { IKeychain, PrivateKey } from '../IKeychain'; export interface SerializedReadOnlyKeychain { type: KeychainType.ReadOnlyKeychain; address: Address; + evmAddress: string; + svmAddress: string; } export class ReadOnlyKeychain implements IKeychain { type: KeychainType.ReadOnlyKeychain = KeychainType.ReadOnlyKeychain; address?: Address; + evmAddress?: string; + svmAddress?: string; init(options: SerializedReadOnlyKeychain) { this.deserialize(options); @@ -30,6 +35,10 @@ export class ReadOnlyKeychain implements IKeychain { throw new Error('Method not implemented.'); } + getKeyPair(_address: Address): Keypair { + throw new Error('[ReadOnlyKeychain] Method not implemented.'); + } + addAccountAtIndex(index: number, address: Address): Promise
{ throw new Error('Method not implemented.'); } @@ -38,6 +47,8 @@ export class ReadOnlyKeychain implements IKeychain { return { address: this.address as Address, type: this.type, + evmAddress: this.evmAddress as string, + svmAddress: this.svmAddress as string, }; } @@ -54,9 +65,7 @@ export class ReadOnlyKeychain implements IKeychain { } getAccounts(): Promise> { - const addresses = (this.address as Address) - ? [this.address as Address] - : []; + const addresses = this.address ? [this.address] : []; return Promise.resolve(addresses); } diff --git a/src/core/keychain/utils.ts b/src/core/keychain/utils.ts index 9c353ad53c..f00bbcb0e1 100644 --- a/src/core/keychain/utils.ts +++ b/src/core/keychain/utils.ts @@ -1,3 +1,5 @@ +import { validateAndFormatAddress } from '@orb-labs/orby-core'; + import { RainbowError, logger } from '~/logger'; import { ahaHttp } from '../network/aha'; @@ -45,7 +47,9 @@ export const autoDiscoverAccountsFromIndex = async ({ const firstNotUsedAddressIndex = addresses.findIndex( (address) => - !addressesHaveBeenUsed.data.addresses[address?.toLowerCase()], + !addressesHaveBeenUsed.data.addresses[ + validateAndFormatAddress(address)! + ], ); return { diff --git a/src/core/network/nfts.ts b/src/core/network/nfts.ts index cb2276f151..1d4eeb2ae8 100644 --- a/src/core/network/nfts.ts +++ b/src/core/network/nfts.ts @@ -1,3 +1,5 @@ +import { validateAndFormatAddress } from '@orb-labs/orby-core'; + import { RainbowError, logger } from '~/logger'; import { queryClient } from '../react-query'; @@ -151,7 +153,7 @@ export const fetchPolygonAllowList = }>('/137-allowlist.json'); const polygonAllowListDictionary = allowList.data?.data?.addresses?.reduce( (allowListDict, tokenAddress) => { - allowListDict[tokenAddress.toLowerCase()] = true; + allowListDict[validateAndFormatAddress(tokenAddress)] = true; return allowListDict; }, {} as PolygonAllowListDictionary, diff --git a/src/core/raps/unlockAndCrosschainSwap.ts b/src/core/raps/unlockAndCrosschainSwap.ts index 38956d2ea3..39f770a7f7 100644 --- a/src/core/raps/unlockAndCrosschainSwap.ts +++ b/src/core/raps/unlockAndCrosschainSwap.ts @@ -1,3 +1,4 @@ +import { validateAndFormatAddress } from '@orb-labs/orby-core'; import { ALLOWS_PERMIT, ChainId, @@ -133,7 +134,9 @@ export const createUnlockAndCrosschainSwapRap = async ( !nativeAsset && chainId === ChainId.mainnet && ALLOWS_PERMIT[ - assetToSell.address?.toLowerCase() as keyof PermitSupportedTokenList + validateAndFormatAddress( + assetToSell.address, + ) as keyof PermitSupportedTokenList ]; if (swapAssetNeedsUnlocking && !allowsPermit) { diff --git a/src/core/raps/unlockAndSwap.ts b/src/core/raps/unlockAndSwap.ts index 88b0ce48da..08525fed96 100644 --- a/src/core/raps/unlockAndSwap.ts +++ b/src/core/raps/unlockAndSwap.ts @@ -1,3 +1,4 @@ +import { validateAndFormatAddress } from '@orb-labs/orby-core'; import { ALLOWS_PERMIT, ETH_ADDRESS as ETH_ADDRESS_AGGREGATOR, @@ -147,7 +148,9 @@ export const createUnlockAndSwapRap = async ( !nativeAsset && chainId === ChainId.mainnet && ALLOWS_PERMIT[ - assetToSell.address?.toLowerCase() as keyof PermitSupportedTokenList + validateAndFormatAddress( + assetToSell.address, + ) as keyof PermitSupportedTokenList ]; if (swapAssetNeedsUnlocking && !allowsPermit) { diff --git a/src/core/raps/utils.ts b/src/core/raps/utils.ts index a31799ffd1..200686d0a9 100644 --- a/src/core/raps/utils.ts +++ b/src/core/raps/utils.ts @@ -2,6 +2,7 @@ import { Block, Provider } from '@ethersproject/abstract-provider'; import { MaxUint256 } from '@ethersproject/constants'; import { Contract, PopulatedTransaction } from '@ethersproject/contracts'; import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { validateAndFormatAddress } from '@orb-labs/orby-core'; import { ALLOWS_PERMIT, CrosschainQuote, @@ -209,7 +210,7 @@ export const getDefaultGasLimitForTrade = ( ): string => { const allowsPermit = chainId === mainnet.id && - ALLOWS_PERMIT[quote?.sellTokenAddress?.toLowerCase()]; + ALLOWS_PERMIT[validateAndFormatAddress(quote?.sellTokenAddress)]; let defaultGasLimit = quote?.defaultGasLimit; diff --git a/src/core/references/assets.ts b/src/core/references/assets.ts index 557d84c246..d03a4a991f 100644 --- a/src/core/references/assets.ts +++ b/src/core/references/assets.ts @@ -1,10 +1,12 @@ import { arbitrum, arbitrumNova, + arbitrumSepolia, aurora, auroraTestnet, avalanche, base, + baseSepolia, blast, blastSepolia, bob, @@ -29,6 +31,7 @@ import { harmonyOne, hedera, hederaTestnet, + holesky, immutableZkEvm, immutableZkEvmTestnet, kava, @@ -53,10 +56,12 @@ import { opBNB, opBNBTestnet, optimism, + optimismSepolia, palm, palmTestnet, pgn, polygon, + polygonAmoy, polygonZkEvm, polygonZkEvmCardona, polygonZkEvmTestnet, @@ -69,6 +74,7 @@ import { saigon, scroll, scrollSepolia, + sepolia, shapeSepolia, xai, xaiTestnet, @@ -82,11 +88,13 @@ import { ChainId } from '../types/chains'; export const customChainIdsToAssetNames: Record = { [arbitrum.id]: 'arbitrum', + [arbitrumSepolia.id]: 'arbitrum', [arbitrumNova.id]: 'arbitrumnova', [aurora.id]: 'aurora', [auroraTestnet.id]: 'auroratestnet', [avalanche.id]: 'avalanchex', [base.id]: 'base', + [baseSepolia.id]: 'base', [blast.id]: 'blast', [blastSepolia.id]: 'blastsepolia', [bob.id]: 'bob', @@ -122,6 +130,7 @@ export const customChainIdsToAssetNames: Record = { [immutableZkEvmTestnet.id]: 'immutablezkevmtestnet', 2410: 'karak', 8054: 'karaksepolia', + 101: 'solana', [kava.id]: 'kavaevm', [kavaTestnet.id]: 'kavaevmtestnet', [klaytn.id]: 'klaytn', @@ -132,6 +141,8 @@ export const customChainIdsToAssetNames: Record = { [ChainId.loot]: 'loot', [lyra.id]: 'lyra', [mainnet.id]: 'ethereum', + [sepolia.id]: 'ethereum', + [holesky.id]: 'ethereum', [manta.id]: 'manta', [mantaSepoliaTestnet.id]: 'mantasepolia', [mantle.id]: 'mantle', @@ -149,10 +160,12 @@ export const customChainIdsToAssetNames: Record = { [opBNB.id]: 'opbnb', [opBNBTestnet.id]: 'opbnbtestnet', [optimism.id]: 'optimism', + [optimismSepolia.id]: 'optimism', [palm.id]: 'palm', [palmTestnet.id]: 'palmtestnet', [pgn.id]: 'pgn', [polygon.id]: 'polygon', + [polygonAmoy.id]: 'polygon', [polygonZkEvm.id]: 'polygonzkevm', [polygonZkEvmCardona.id]: 'polygonzkevmcardona', [polygonZkEvmTestnet.id]: 'polygonzkevmtestnet', diff --git a/src/core/references/rawImages.ts b/src/core/references/rawImages.ts index 5d210f6b5c..b53408ff37 100644 --- a/src/core/references/rawImages.ts +++ b/src/core/references/rawImages.ts @@ -1,2 +1,2 @@ export const RAINBOW_ICON_RAW_SVG = - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0ibm9uZSI+PGcgY2xpcC1wYXRoPSJ1cmwoI2EpIj48cGF0aCBmaWxsPSJ1cmwoI2IpIiBkPSJNMCAwaDMydjMySDB6Ii8+PHBhdGggZmlsbD0idXJsKCNjKSIgZD0iTTUuMzMzIDEwLjEzM2gxLjZjOC4yNDggMCAxNC45MzQgNi42ODYgMTQuOTM0IDE0LjkzNHYxLjZoMy4yYTEuNiAxLjYgMCAwIDAgMS42LTEuNmMwLTEwLjg5OS04LjgzNS0xOS43MzQtMTkuNzM0LTE5LjczNGExLjYgMS42IDAgMCAwLTEuNiAxLjZ2My4yWiIvPjxwYXRoIGZpbGw9InVybCgjZCkiIGQ9Ik0yMi40IDI1LjA2N2g0LjI2N2ExLjYgMS42IDAgMCAxLTEuNiAxLjZIMjIuNHYtMS42WiIvPjxwYXRoIGZpbGw9InVybCgjZSkiIGQ9Ik02LjkzMyA1LjMzM1Y5LjZoLTEuNlY2LjkzM2ExLjYgMS42IDAgMCAxIDEuNi0xLjZaIi8+PHBhdGggZmlsbD0idXJsKCNmKSIgZD0iTTUuMzMzIDkuNmgxLjZjOC41NDIgMCAxNS40NjcgNi45MjUgMTUuNDY3IDE1LjQ2N3YxLjZoLTQuOHYtMS42YzAtNS44OTEtNC43NzYtMTAuNjY3LTEwLjY2Ny0xMC42NjdoLTEuNlY5LjZaIi8+PHBhdGggZmlsbD0idXJsKCNnKSIgZD0iTTE4LjEzMyAyNS4wNjdIMjIuNHYxLjZoLTQuMjY3di0xLjZaIi8+PHBhdGggZmlsbD0idXJsKCNoKSIgZD0iTTUuMzMzIDEzLjg2N1Y5LjZoMS42djQuMjY3aC0xLjZaIi8+PHBhdGggZmlsbD0idXJsKCNpKSIgZD0iTTUuMzMzIDE2LjUzM2ExLjYgMS42IDAgMCAwIDEuNiAxLjYgNi45MzMgNi45MzMgMCAwIDEgNi45MzQgNi45MzQgMS42IDEuNiAwIDAgMCAxLjYgMS42aDIuNjY2di0xLjZjMC02LjE4Ni01LjAxNC0xMS4yLTExLjItMTEuMmgtMS42djIuNjY2WiIvPjxwYXRoIGZpbGw9InVybCgjaikiIGQ9Ik0xMy44NjcgMjUuMDY3aDQuMjY2djEuNmgtMi42NjZhMS42IDEuNiAwIDAgMS0xLjYtMS42WiIvPjxwYXRoIGZpbGw9InVybCgjaykiIGQ9Ik02LjkzMyAxOC4xMzNhMS42IDEuNiAwIDAgMS0xLjYtMS42di0yLjY2NmgxLjZ2NC4yNjZaIi8+PC9nPjxkZWZzPjxyYWRpYWxHcmFkaWVudCBpZD0iYyIgY3g9IjAiIGN5PSIwIiByPSIxIiBncmFkaWVudFRyYW5zZm9ybT0icm90YXRlKC05MCAxNiA5LjA2Nykgc2NhbGUoMTkuNzMzMykiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9Ii43NyIgc3RvcC1jb2xvcj0iI0ZGNDAwMCIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzg3NTRDOSIvPjwvcmFkaWFsR3JhZGllbnQ+PHJhZGlhbEdyYWRpZW50IGlkPSJmIiBjeD0iMCIgY3k9IjAiIHI9IjEiIGdyYWRpZW50VHJhbnNmb3JtPSJyb3RhdGUoLTkwIDE2IDkuMDY3KSBzY2FsZSgxNS40NjY3KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iLjcyNCIgc3RvcC1jb2xvcj0iI0ZGRjcwMCIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0ZGOTkwMSIvPjwvcmFkaWFsR3JhZGllbnQ+PHJhZGlhbEdyYWRpZW50IGlkPSJpIiBjeD0iMCIgY3k9IjAiIHI9IjEiIGdyYWRpZW50VHJhbnNmb3JtPSJyb3RhdGUoLTkwIDE2IDkuMDY3KSBzY2FsZSgxMS4yKSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iLjU5NSIgc3RvcC1jb2xvcj0iIzBBRiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzAxREE0MCIvPjwvcmFkaWFsR3JhZGllbnQ+PHJhZGlhbEdyYWRpZW50IGlkPSJqIiBjeD0iMCIgY3k9IjAiIHI9IjEiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoNC41MzMzMyAwIDAgMTIuMDg4OSAxMy42IDI1Ljg2NykiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBzdG9wLWNvbG9yPSIjMEFGIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDFEQTQwIi8+PC9yYWRpYWxHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9ImsiIGN4PSIwIiBjeT0iMCIgcj0iMSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgwIC00LjUzMzMzIDg1Ljk2NTQgMCA2LjEzMyAxOC40KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiMwQUYiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMwMURBNDAiLz48L3JhZGlhbEdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iYiIgeDE9IjE2IiB4Mj0iMTYiIHkxPSIwIiB5Mj0iMzIiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBzdG9wLWNvbG9yPSIjMTc0Mjk5Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDAxRTU5Ii8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImQiIHgxPSIyMi4xMzMiIHgyPSIyNi42NjciIHkxPSIyNS44NjciIHkyPSIyNS44NjciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBzdG9wLWNvbG9yPSIjRkY0MDAwIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjODc1NEM5Ii8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImUiIHgxPSI2LjEzMyIgeDI9IjYuMTMzIiB5MT0iNS4zMzMiIHkyPSI5Ljg2NyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiM4NzU0QzkiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNGRjQwMDAiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iZyIgeDE9IjE4LjEzMyIgeDI9IjIyLjQiIHkxPSIyNS44NjciIHkyPSIyNS44NjciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBzdG9wLWNvbG9yPSIjRkZGNzAwIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjRkY5OTAxIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImgiIHgxPSI2LjEzMyIgeDI9IjYuMTMzIiB5MT0iMTMuODY3IiB5Mj0iOS42IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agc3RvcC1jb2xvcj0iI0ZGRjcwMCIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0ZGOTkwMSIvPjwvbGluZWFyR3JhZGllbnQ+PGNsaXBQYXRoIGlkPSJhIj48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMCAwaDMydjMySDB6Ii8+PC9jbGlwUGF0aD48L2RlZnM+PC9zdmc+'; + 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB3aWR0aD0iMTAwJSIgdmlld0JveD0iMCAwIDEyOCAxMjgiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDEyOCAxMjgiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8cGF0aCBmaWxsPSIjMDcwOTFGIiBvcGFjaXR5PSIxLjAwMDAwMCIgc3Ryb2tlPSJub25lIiAKCWQ9IgpNODcuMDAwMDAwLDEyOS4wMDAwMDAgCglDNTguMDAwMDAwLDEyOS4wMDAwMDAgMjkuNTAwMDAwLDEyOS4wMDAwMDAgMS4wMDAwMDAsMTI5LjAwMDAwMCAKCUMxLjAwMDAwMCw4Ni4zMzMzMzYgMS4wMDAwMDAsNDMuNjY2NjY4IDEuMDAwMDAwLDEuMDAwMDAwIAoJQzQzLjY2NjY2OCwxLjAwMDAwMCA4Ni4zMzMzMzYsMS4wMDAwMDAgMTI5LjAwMDAwMCwxLjAwMDAwMCAKCUMxMjkuMDAwMDAwLDQzLjY2NjY2OCAxMjkuMDAwMDAwLDg2LjMzMzMzNiAxMjkuMDAwMDAwLDEyOS4wMDAwMDAgCglDMTE1LjE2NjY2NCwxMjkuMDAwMDAwIDEwMS4zMzMzMzYsMTI5LjAwMDAwMCA4Ny4wMDAwMDAsMTI5LjAwMDAwMCAKTTc0LjAyMTExOCw3Ni42Mjc1MDIgCglDODEuNTI3NTQyLDY5LjQ4MjQ1MiA4MS42NTI2NTcsNTkuNTQ3MDg1IDc0LjMxMDI4MCw1My42NjQzNjQgCglDNjguNTg2MzI3LDQ5LjA3ODMzNSA2MC40NjE2MjAsNDkuNDM1MjA0IDU1LjE0MTI3MCw1NC41MDYzMjkgCglDNDkuODE5NDkyLDU5LjU3ODgxNSA0OS4wOTEzMTYsNjcuNzAwMTg4IDUzLjQyODgyNSw3My42MDUzNzcgCglDNTguMDk2OTA1LDc5Ljk2MDYwOSA2NS42NzkxNTMsODEuMjY4OTk3IDc0LjAyMTExOCw3Ni42Mjc1MDIgCk0zMi4yMzIxOTMsODQuNTE0NTcyIAoJQzQwLjg4MzE3NSwxMDAuMjYzNjQxIDYxLjU2MjY1MywxMDcuOTQ5NDcxIDc3LjIwOTc0NywxMDEuMjMxMTI1IAoJQzc1LjgzMjg0MCw5NC42Mjk1NTUgNzIuMzA4OTUyLDkxLjI1NzU0NSA2NS4yOTAzOTAsOTAuNzI2MDM2IAoJQzU4Ljc0NzQ1Miw5MC4yMzA1MzcgNTMuMDkwMzAyLDg4LjExMjI3NCA0Ny44Nzk0MTQsODQuMDc4NTE0IAoJQzQzLjA0NzExOSw4MC4zMzc4MzAgMzcuNzQ0MzYyLDgwLjI1NjI4NyAzMi4yMzIxOTMsODQuNTE0NTcyIApNODUuNzU1MzcxLDQ4LjEwMTkzNiAKCUM5MC4zOTIxNDMsNDkuNjUxNTM1IDk0Ljc1NzIxNyw0OS43MDgwMTIgOTguNTY5NTk1LDQ2LjA5NzM1MSAKCUM4OS4yOTc5NTgsMjkuNzg4Mjc3IDY3LjgzNTU5NCwyMS42ODIwNTggNTMuNjQ4NTcxLDI4Ljg5ODA3MSAKCUM1NS4yMzQzNDgsMzQuOTY2ODA1IDU4LjIzODU2NCwzOC42MjEzNTMgNjMuNzM5OTYwLDM5LjAyMzI5NiAKCUM3MS45MzI4MDAsMzkuNjIxODgzIDc4LjgzNzMxOCw0Mi42ODIxNzggODUuNzU1MzcxLDQ4LjEwMTkzNiAKTTMzLjU2NDE2Nyw0My4wODQ0OTkgCglDMjYuNTIwMjkyLDUzLjUyMDIyMiAyNS4wNjYyODgsNjQuNzM1MjQ1IDI4LjQ3NzQ5NSw3Ni43NDkxNjggCglDMzQuNTg1MzY1LDc1Ljc2OTU2MiAzOC41NjUxODIsNzIuNjkyMDQ3IDM5LjE0ODU3NSw2Ni45NjUxNjQgCglDMzkuOTUxMjE4LDU5LjA4NjEwMiA0Mi40NjA1MzcsNTIuMjg3NDIyIDQ3LjI3ODc3OCw0NS45NjE3OTYgCglDNTAuNjI4Mzg3LDQxLjU2NDI2NiA0OS41NTE4OTUsMzYuMDQwODk3IDQ1LjEwMjg0OCwzMS4zNDEzNDMgCglDNDEuMzg0MTE3LDM1LjA2NzY2NSAzNy42NzkyODMsMzguNzgwMDU2IDMzLjU2NDE2Nyw0My4wODQ0OTkgCk0xMDIuNDg0MDc3LDc0Ljg3NDI5OCAKCUMxMDQuMzQ5MzY1LDY3LjMxMDU0NyAxMDQuMzg5NjEwLDU5Ljg2NDMwMCAxMDEuNDIyNzkxLDUyLjUyNTAwOSAKCUM5NC42MjM0MDUsNTQuNTUyODMwIDkxLjI4NzIwMSw1Ny45NzcyNjggOTAuOTgyMzQ2LDYzLjk1MjAwNyAKCUM5MC42MzA0MDIsNzAuODQ5NTcxIDg4LjQ5ODEwMCw3Ni44Mzk0MTcgODQuMjczOTg3LDgyLjM1Mzk2NiAKCUM4MC4zNjUxNzMsODcuNDU2OTE3IDgwLjg3MDg4OCw5Mi44NTU2NTIgODUuMTY3NjQ4LDk4LjAwOTg0MiAKCUM5My42NTkzMzIsOTIuNjg3ODY2IDk5LjQxOTEyOCw4NS4zMjYxOTUgMTAyLjQ4NDA3Nyw3NC44NzQyOTggCnoiLz4KPHBhdGggZmlsbD0iI0Y3RjdGNyIgb3BhY2l0eT0iMS4wMDAwMDAiIHN0cm9rZT0ibm9uZSIgCglkPSIKTTczLjcxNDI2NCw3Ni44NDAxNzkgCglDNjUuNjc5MTUzLDgxLjI2ODk5NyA1OC4wOTY5MDUsNzkuOTYwNjA5IDUzLjQyODgyNSw3My42MDUzNzcgCglDNDkuMDkxMzE2LDY3LjcwMDE4OCA0OS44MTk0OTIsNTkuNTc4ODE1IDU1LjE0MTI3MCw1NC41MDYzMjkgCglDNjAuNDYxNjIwLDQ5LjQzNTIwNCA2OC41ODYzMjcsNDkuMDc4MzM1IDc0LjMxMDI4MCw1My42NjQzNjQgCglDODEuNjUyNjU3LDU5LjU0NzA4NSA4MS41Mjc1NDIsNjkuNDgyNDUyIDczLjcxNDI2NCw3Ni44NDAxNzkgCnoiLz4KPHBhdGggZmlsbD0iI0YzRjNGMyIgb3BhY2l0eT0iMS4wMDAwMDAiIHN0cm9rZT0ibm9uZSIgCglkPSIKTTMyLjQyMDYwMSw4NC4yMDQ5MTAgCglDMzcuNzQ0MzYyLDgwLjI1NjI4NyA0My4wNDcxMTksODAuMzM3ODMwIDQ3Ljg3OTQxNCw4NC4wNzg1MTQgCglDNTMuMDkwMzAyLDg4LjExMjI3NCA1OC43NDc0NTIsOTAuMjMwNTM3IDY1LjI5MDM5MCw5MC43MjYwMzYgCglDNzIuMzA4OTUyLDkxLjI1NzU0NSA3NS44MzI4NDAsOTQuNjI5NTU1IDc3LjIwOTc0NywxMDEuMjMxMTI1IAoJQzYxLjU2MjY1MywxMDcuOTQ5NDcxIDQwLjg4MzE3NSwxMDAuMjYzNjQxIDMyLjQyMDYwMSw4NC4yMDQ5MTAgCnoiLz4KPHBhdGggZmlsbD0iI0YzRjNGNCIgb3BhY2l0eT0iMS4wMDAwMDAiIHN0cm9rZT0ibm9uZSIgCglkPSIKTTg1LjM5Nzc5Nyw0Ny45NDE5NDAgCglDNzguODM3MzE4LDQyLjY4MjE3OCA3MS45MzI4MDAsMzkuNjIxODgzIDYzLjczOTk2MCwzOS4wMjMyOTYgCglDNTguMjM4NTY0LDM4LjYyMTM1MyA1NS4yMzQzNDgsMzQuOTY2ODA1IDUzLjY0ODU3MSwyOC44OTgwNzEgCglDNjcuODM1NTk0LDIxLjY4MjA1OCA4OS4yOTc5NTgsMjkuNzg4Mjc3IDk4LjU2OTU5NSw0Ni4wOTczNTEgCglDOTQuNzU3MjE3LDQ5LjcwODAxMiA5MC4zOTIxNDMsNDkuNjUxNTM1IDg1LjM5Nzc5Nyw0Ny45NDE5NDAgCnoiLz4KPHBhdGggZmlsbD0iI0YzRjNGNCIgb3BhY2l0eT0iMS4wMDAwMDAiIHN0cm9rZT0ibm9uZSIgCglkPSIKTTMzLjc2OTMxMCw0Mi43ODg0NzUgCglDMzcuNjc5MjgzLDM4Ljc4MDA1NiA0MS4zODQxMTcsMzUuMDY3NjY1IDQ1LjEwMjg0OCwzMS4zNDEzNDMgCglDNDkuNTUxODk1LDM2LjA0MDg5NyA1MC42MjgzODcsNDEuNTY0MjY2IDQ3LjI3ODc3OCw0NS45NjE3OTYgCglDNDIuNDYwNTM3LDUyLjI4NzQyMiAzOS45NTEyMTgsNTkuMDg2MTAyIDM5LjE0ODU3NSw2Ni45NjUxNjQgCglDMzguNTY1MTgyLDcyLjY5MjA0NyAzNC41ODUzNjUsNzUuNzY5NTYyIDI4LjQ3NzQ5NSw3Ni43NDkxNjggCglDMjUuMDY2Mjg4LDY0LjczNTI0NSAyNi41MjAyOTIsNTMuNTIwMjIyIDMzLjc2OTMxMCw0Mi43ODg0NzUgCnoiLz4KPHBhdGggZmlsbD0iI0YzRjNGMyIgb3BhY2l0eT0iMS4wMDAwMDAiIHN0cm9rZT0ibm9uZSIgCglkPSIKTTEwMi4zNDcwOTksNzUuMjU4NTk4IAoJQzk5LjQxOTEyOCw4NS4zMjYxOTUgOTMuNjU5MzMyLDkyLjY4Nzg2NiA4NS4xNjc2NDgsOTguMDA5ODQyIAoJQzgwLjg3MDg4OCw5Mi44NTU2NTIgODAuMzY1MTczLDg3LjQ1NjkxNyA4NC4yNzM5ODcsODIuMzUzOTY2IAoJQzg4LjQ5ODEwMCw3Ni44Mzk0MTcgOTAuNjMwNDAyLDcwLjg0OTU3MSA5MC45ODIzNDYsNjMuOTUyMDA3IAoJQzkxLjI4NzIwMSw1Ny45NzcyNjggOTQuNjIzNDA1LDU0LjU1MjgzMCAxMDEuNDIyNzkxLDUyLjUyNTAwOSAKCUMxMDQuMzg5NjEwLDU5Ljg2NDMwMCAxMDQuMzQ5MzY1LDY3LjMxMDU0NyAxMDIuMzQ3MDk5LDc1LjI1ODU5OCAKeiIvPgo8L3N2Zz4='; diff --git a/src/core/resources/nfts/collections.ts b/src/core/resources/nfts/collections.ts index f6491c2775..194de09d0e 100644 --- a/src/core/resources/nfts/collections.ts +++ b/src/core/resources/nfts/collections.ts @@ -1,3 +1,4 @@ +import { validateAndFormatAddress } from '@orb-labs/orby-core'; import { useInfiniteQuery } from '@tanstack/react-query'; import { Address, Chain } from 'viem'; @@ -89,7 +90,9 @@ async function nftCollectionsQueryFunction({ if (shouldPrefilterPolygonContract) { const polygonContractAddress = polygonContractAddressString.split('.')[1]; - return polygonAllowList[polygonContractAddress.toLowerCase()]; + return polygonAllowList[ + validateAndFormatAddress(polygonContractAddress) + ]; } else { return true; } diff --git a/src/core/resources/transactions/transaction.ts b/src/core/resources/transactions/transaction.ts index 9f91d58643..ddf9e80848 100644 --- a/src/core/resources/transactions/transaction.ts +++ b/src/core/resources/transactions/transaction.ts @@ -94,12 +94,11 @@ const fetchTransactionDataFromProvider = async ({ chainId: number; hash: Hash; account: Address; -}): Promise => { +}): Promise => { const provider = getProvider({ chainId }); const transaction = await provider.getTransaction(hash); - if (!transaction) - throw `getCustomChainTransaction: couldn't find transaction`; + if (!transaction) return undefined; const decimals = 18; // assuming every chain uses 18 decimals const value = formatUnits(transaction.value, decimals); diff --git a/src/core/state/currentSettings/currentAddress.ts b/src/core/state/currentSettings/currentAddress.ts index 549a8a1cb0..51301f6eb6 100644 --- a/src/core/state/currentSettings/currentAddress.ts +++ b/src/core/state/currentSettings/currentAddress.ts @@ -7,13 +7,18 @@ import { withSelectors } from '../internal/withSelectors'; interface PersistedAddressState { currentAddress: Address; + currentAddresses: Address[]; setCurrentAddress: (address: Address) => void; + setCurrentAddresses: (addresses: Address[]) => void; } const persistedAddressStore = createStore( (set) => ({ currentAddress: '' as Address, + currentAddresses: [], setCurrentAddress: (newAddress) => set({ currentAddress: newAddress }), + setCurrentAddresses: (currentAddresses) => + set({ currentAddresses: currentAddresses }), }), { persist: { @@ -25,13 +30,18 @@ const persistedAddressStore = createStore( interface RapidAddressState { currentAddress: Address; + currentAddresses: Address[]; setCurrentAddress: (address: Address) => void; + setCurrentAddresses: (addresses: Address[]) => void; } export const currentAddressStore = create((set) => ({ currentAddress: // Default to the persisted current address persistedAddressStore.getState().currentAddress || ('' as Address), + currentAddresses: + // Default to the persisted current address + persistedAddressStore.getState().currentAddresses || [], setCurrentAddress: (newAddress) => { if (newAddress !== persistedAddressStore.getState().currentAddress) { set({ currentAddress: newAddress }); @@ -39,13 +49,21 @@ export const currentAddressStore = create((set) => ({ persistedAddressStore.getState().setCurrentAddress(newAddress); } }, + setCurrentAddresses: (newAddresses) => { + set({ currentAddresses: newAddresses }); + // Automatically persist in the background to the persisted store + persistedAddressStore.getState().setCurrentAddresses(newAddresses); + }, })); // Synchronize currentAddress with persistedAddress once rehydrated persistedAddressStore.subscribe((state) => { // If persistedAddress changes and currentAddress is still the default, update it if (currentAddressStore.getState().currentAddress === ('' as Address)) { - currentAddressStore.setState({ currentAddress: state.currentAddress }); + currentAddressStore.setState({ + currentAddress: state.currentAddress, + currentAddresses: state.currentAddresses, + }); } }); diff --git a/src/core/state/rainbowChains/index.ts b/src/core/state/rainbowChains/index.ts index 8ab2a7fd01..4f7b1154a8 100644 --- a/src/core/state/rainbowChains/index.ts +++ b/src/core/state/rainbowChains/index.ts @@ -72,7 +72,7 @@ export const rainbowChainsStore = createStore( addAllCustomRPC: (rpcs: { rpcUrl: string; chainId: ChainId }[]) => { const rainbowChains = get().rainbowChains; rpcs.forEach(({ chainId, rpcUrl }) => { - const rainbowChain = rainbowChains[chainId] || { + let rainbowChain = rainbowChains[chainId] || { chains: [], activeRpcUrl: '', }; @@ -81,6 +81,9 @@ export const rainbowChainsStore = createStore( ); if (!currentRpcs.includes(rpcUrl)) { + const chains = getInitialRainbowChains(); + rainbowChain = chains[chainId]; + rainbowChain.chains.push({ ...rainbowChain.chains[0], rpcUrls: { diff --git a/src/core/types/rpcMethods.ts b/src/core/types/rpcMethods.ts index ab6e139a00..95d3c99330 100644 --- a/src/core/types/rpcMethods.ts +++ b/src/core/types/rpcMethods.ts @@ -2,6 +2,8 @@ export enum rpcMethods { eth_chainId = 'eth_chainId', eth_accounts = 'eth_accounts', eth_sendTransaction = 'eth_sendTransaction', + signTransaction = 'signTransaction', + signAndSendTransaction = 'signAndSendTransaction', eth_signTransaction = 'eth_signTransaction', personal_sign = 'personal_sign', eth_signTypedData = 'eth_signTypedData', diff --git a/src/core/utils/ethereum.ts b/src/core/utils/ethereum.ts index ee2b5d5704..c5d29585c1 100644 --- a/src/core/utils/ethereum.ts +++ b/src/core/utils/ethereum.ts @@ -2,6 +2,7 @@ import { isAddress } from '@ethersproject/address'; import { Mnemonic, isValidMnemonic } from '@ethersproject/hdnode'; import { TransactionResponse } from '@ethersproject/providers'; import { parseEther } from '@ethersproject/units'; +import { validateAndFormatAddress } from '@orb-labs/orby-core'; import BigNumber from 'bignumber.js'; import omit from 'lodash/omit'; import { Address } from 'viem'; @@ -69,7 +70,10 @@ export const hasPreviousTransactions = async ( data: { addresses: Record }; }; - return parsedResponse?.data?.addresses[address.toLowerCase()] === true; + return ( + parsedResponse?.data?.addresses[validateAndFormatAddress(address)] === + true + ); } catch (e) { return false; } diff --git a/src/core/utils/nfts.ts b/src/core/utils/nfts.ts index b0647e79ca..e642b3cc0e 100644 --- a/src/core/utils/nfts.ts +++ b/src/core/utils/nfts.ts @@ -1,3 +1,5 @@ +import { validateAndFormatAddress } from '@orb-labs/orby-core'; + import { ChainName } from '../types/chains'; import { PolygonAllowListDictionary, @@ -193,7 +195,9 @@ export function filterSimpleHashNFTs( ): ValidatedSimpleHashNFT[] { return nfts .filter((nft) => { - const lowercasedContractAddress = nft.contract_address?.toLowerCase(); + const lowercasedContractAddress = validateAndFormatAddress( + nft.contract_address, + ); const network = getNetworkFromSimpleHashChain(nft.chain); const isMissingRequiredFields = @@ -245,7 +249,9 @@ export function simpleHashNFTToUniqueAsset( nft: ValidatedSimpleHashNFT, ): UniqueAsset { const collection = nft.collection; - const lowercasedContractAddress = nft.contract_address?.toLowerCase(); + const lowercasedContractAddress = validateAndFormatAddress( + nft.contract_address, + ); const marketplace = nft.collection.marketplace_pages?.[0]; @@ -259,7 +265,8 @@ export function simpleHashNFTToUniqueAsset( const standard = nft.contract.type; - const isPoap = nft.contract_address.toLowerCase() === POAP_NFT_ADDRESS; + const isPoap = + validateAndFormatAddress(nft.contract_address) === POAP_NFT_ADDRESS; const poapDropId = !isPoap ? null : extractPoapDropId(nft.external_url || ''); return { @@ -340,7 +347,9 @@ export const getUniqueAssetImagePreviewURL = (asset: UniqueAsset) => { }; export const isENS = (asset: UniqueAsset) => { - const lowercasedContractAddress = asset.asset_contract.address?.toLowerCase(); + const lowercasedContractAddress = validateAndFormatAddress( + asset.asset_contract.address, + ); return lowercasedContractAddress === ENS_NFT_CONTRACT_ADDRESS; }; diff --git a/src/core/utils/orb.ts b/src/core/utils/orb.ts index 5f0c66dcea..b30b19f24f 100644 --- a/src/core/utils/orb.ts +++ b/src/core/utils/orb.ts @@ -3,9 +3,16 @@ import { FungibleTokenAmount, OnchainOperation, OperationDataFormat, - SignedOperation, StandardizedBalance, + VMType, + getVirtualEnvironment, } from '@orb-labs/orby-core'; +import { + AddressLookupTableAccount, + Connection, + TransactionMessage, + VersionedTransaction, +} from '@solana/web3.js'; import BigNumber from 'bignumber.js'; import { providers } from 'ethers'; import _ from 'lodash'; @@ -77,9 +84,17 @@ export const convertStandardizedBalanceToParsedUserAsset = ( export const convertStandardizedBalanceToParsedUserAssets = ( balances: StandardizedBalance[], ): ParsedUserAsset[] => { - return balances.map((balance) => - convertStandardizedBalanceToParsedUserAsset(balance), - ); + return balances + .map((balance) => convertStandardizedBalanceToParsedUserAsset(balance)) + ?.sort((a, b) => { + if (a.isNativeAsset && b.isNativeAsset) { + return 0; + } else if (a.isNativeAsset) { + return -1; + } + + return 1; + }); }; export const convertFungibleTokenAmountToParsedUserAsset = ( @@ -149,9 +164,105 @@ export const convertTokenBalancesOnChainsToParsedUserAssets = ( ); }; +export const getWalletVirtualEnvironment = ( + address: string, +): VMType | undefined => { + const startsWith0xLen42HexRegex = /^0x[0-9a-fA-F]{40}$/; + const solanaRegex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/; + if (startsWith0xLen42HexRegex.test(address)) { + return VMType.EVM; + } else if (solanaRegex.test(address)) { + return VMType.SVM; + } + + return undefined; +}; + +export async function signSVMTransaction( + txRpcUrl: string, + data: string, + from?: string, +): Promise { + const keypair = await keychainManager.getKeyPair(from as Address); + const connection = new Connection(txRpcUrl); + + const originalTransaction = VersionedTransaction.deserialize( + Buffer.from(data, 'hex'), + ); + + // Fetch the lookup tables from the blockchain + const lookupTableAddresses = + originalTransaction.message.addressTableLookups.map( + (lookup) => lookup.accountKey, + ); + const lookupTableAccounts = + await connection.getMultipleAccountsInfo(lookupTableAddresses); + + // Create the necessary lookup table account objects + const addressLookupTableAccounts = lookupTableAccounts.map((account, i) => { + return new AddressLookupTableAccount({ + key: lookupTableAddresses[i], + state: AddressLookupTableAccount.deserialize(account.data), + }); + }); + + const originalMessage = TransactionMessage.decompile( + originalTransaction.message, + { addressLookupTableAccounts }, + ); + + const { blockhash } = await connection.getLatestBlockhash(); + const versionedMergedTxMessage = new TransactionMessage({ + payerKey: originalMessage.payerKey, + recentBlockhash: blockhash, + instructions: originalMessage.instructions, + }).compileToV0Message(); + + const versionMergedTx = new VersionedTransaction(versionedMergedTxMessage); + + console.log( + 'originalMessage', + originalTransaction, + keypair, + txRpcUrl, + from, + keypair.publicKey?.toString(), + ); + + originalMessage.instructions.forEach((instruction, idx) => { + console.log(`Instruction ${idx + 1}:`); + + // Iterate through the keys in each instruction to check for 'isSigner: true' + instruction.keys.forEach((key) => { + if (key.isSigner) { + console.log(`Account ${key.pubkey.toBase58()} is a signer`); + } else { + console.log(`Account ${key.pubkey.toBase58()} is NOT a signer`); + } + }); + }); + + versionMergedTx.sign([ + { + publicKey: keypair.publicKey, + secretKey: keypair.secretKey, + }, + ]); + + return Buffer.from(versionMergedTx.serialize())?.toString('hex'); +} + export async function signOperation( operation: OnchainOperation, -): Promise { +): Promise { + if (getVirtualEnvironment(operation.chainId) == VMType.SVM) { + return signSVMTransaction( + operation.txRpcUrl, + operation.data, + operation.from, + ); + } + const provider = new providers.JsonRpcProvider(operation.txRpcUrl); const signer = await keychainManager.getSigner(operation.from as Address); const wallet = signer.connect(provider); @@ -173,8 +284,7 @@ export async function signOperation( // eslint-disable-next-line no-await-in-loop const tx = await wallet.populateTransaction(txData); - const signedTx = await signer.signTransaction(tx); - return { type: operation.type, signature: signedTx }; + return await signer.signTransaction(tx); } else { const parsedData = JSON.parse(operation.data) as { domain: TypedDataDomain; @@ -182,6 +292,7 @@ export async function signOperation( message: Record; }; + delete parsedData.types['EIP712Domain']; // @ts-ignore const signature = await wallet.signTypedData( parsedData.domain, @@ -189,6 +300,6 @@ export async function signOperation( parsedData.message, ); - return { type: operation.type, signature, data: operation.data }; + return signature; } } diff --git a/src/core/utils/signMessages.tsx b/src/core/utils/signMessages.tsx index 83853fa53a..6c2e88eb94 100644 --- a/src/core/utils/signMessages.tsx +++ b/src/core/utils/signMessages.tsx @@ -41,6 +41,15 @@ export const getSigningRequestDisplayDetails = ( message, msgData: message, address: getAddress(address) as Address, + orbyCallData: '', + }; + } + case 'signTransaction': { + const message = payload?.params?.[0] as string; + return { + message, + msgData: message, + orbyCallData: message, }; } default: { @@ -69,6 +78,7 @@ export const getSigningRequestDisplayDetails = ( msgData: sanitizedMessageData, address: getAddress(address) as Address, typedData: true, + orbyCallData: JSON.stringify(sanitizedMessageData), }; } } diff --git a/src/core/utils/transactions.ts b/src/core/utils/transactions.ts index 820eebf77a..3e8f291759 100644 --- a/src/core/utils/transactions.ts +++ b/src/core/utils/transactions.ts @@ -6,6 +6,7 @@ import { TransactionResponse, } from '@ethersproject/providers'; import { formatUnits } from '@ethersproject/units'; +import { validateAndFormatAddress } from '@orb-labs/orby-core'; import { isString } from 'lodash'; import { Address } from 'viem'; @@ -117,7 +118,9 @@ export const getDataForNftTransfer = ( asset: UniqueAsset, ): string | undefined => { if (!asset.id || !asset.asset_contract?.address) return; - const lowercasedContractAddress = asset.asset_contract.address.toLowerCase(); + const lowercasedContractAddress = validateAndFormatAddress( + asset.asset_contract.address, + ); const standard = asset.asset_contract?.schema_name; let data: string | undefined; if ( diff --git a/src/entries/background/handlers/handleProviderRequest.ts b/src/entries/background/handlers/handleProviderRequest.ts index dd6d380e4e..542142c4df 100644 --- a/src/entries/background/handlers/handleProviderRequest.ts +++ b/src/entries/background/handlers/handleProviderRequest.ts @@ -1,7 +1,4 @@ -import { - AddEthereumChainProposedChain, - handleProviderRequest as rnbwHandleProviderRequest, -} from '@rainbow-me/provider'; +import { AddEthereumChainProposedChain } from '@rainbow-me/provider'; import { Chain, UserRejectedRequestError } from 'viem'; import { event } from '~/analytics/event'; @@ -32,6 +29,8 @@ import { getProvider } from '~/core/wagmi/clientToProvider'; import { IN_DAPP_NOTIFICATION_STATUS } from '~/entries/iframe/notification'; import { RainbowError, logger } from '~/logger'; +import { rnbwHandleProviderRequest } from './rnbwHandleProviderRequest'; + const MAX_REQUEST_PER_SECOND = 10; const MAX_REQUEST_PER_MINUTE = 90; let minuteTimer: NodeJS.Timeout | null = null; @@ -103,6 +102,7 @@ const messengerProviderRequest = async ( const { addPendingRequest } = pendingRequestStore.getState(); // Add pending request to global background state. addPendingRequest(request); + console.log('[messengerProviderRequest] request', request); let ready = isInitialized(); while (!ready) { @@ -120,13 +120,19 @@ const messengerProviderRequest = async ( url: WELCOME_URL, }); } + + console.log('[messengerProviderRequest] payload 00'); // Wait for response from the popup. const payload: unknown | null = await new Promise((resolve) => // eslint-disable-next-line no-promise-executor-return - messenger.reply(`message:${request.id}`, async (payload) => - resolve(payload), - ), + messenger.reply(`message:${request.id}`, async (payload) => { + console.log('[messengerProviderRequest] payload', payload); + return resolve(payload); + }), ); + + console.log('[messengerProviderRequest] payload 11', payload); + if (!payload) { throw new UserRejectedRequestError(Error('User rejected the request.')); } @@ -222,6 +228,8 @@ const skipRateLimitCheck = (method: string) => 'eth_chainId', 'eth_accounts', 'eth_sendTransaction', + 'signTransaction', + 'signAndSendTransaction', 'eth_signTransaction', 'personal_sign', 'eth_signTypedData', diff --git a/src/entries/background/handlers/rnbwHandleProviderRequest.ts b/src/entries/background/handlers/rnbwHandleProviderRequest.ts new file mode 100644 index 0000000000..e1435430e8 --- /dev/null +++ b/src/entries/background/handlers/rnbwHandleProviderRequest.ts @@ -0,0 +1,511 @@ +import { + Provider, + StaticJsonRpcProvider, + TransactionRequest, +} from '@ethersproject/providers'; +import { recoverPersonalSignature } from '@metamask/eth-sig-util'; +import { validateAndFormatAddress } from '@orb-labs/orby-core'; +import { ActiveSession } from '@rainbow-me/provider/dist/references/appSession'; +import { AddEthereumChainProposedChain } from '@rainbow-me/provider/dist/references/chains'; +import { errorCodes } from '@rainbow-me/provider/dist/references/errorCodes'; +import { + CallbackOptions, + IProviderRequestTransport, + ProviderRequestPayload, + RequestError, +} from '@rainbow-me/provider/dist/references/messengers'; +import { + deriveChainIdByHostname, + getDappHost, + isValidUrl, +} from '@rainbow-me/provider/dist/utils/apps'; +import { normalizeTransactionResponsePayload } from '@rainbow-me/provider/dist/utils/ethereum'; +import { toHex } from '@rainbow-me/provider/dist/utils/hex'; +import { Address, isAddress, isHex } from 'viem'; + +interface WalletPermissionsParams { + eth_accounts: object; +} + +const buildError = ({ + id, + message, + errorCode, +}: { + id: number; + errorCode: { + code: number; + name: string; + }; + message?: string; +}): { id: number; error: RequestError } => { + return { + id, + error: { + name: errorCode.name, + message, + code: errorCode.code, + }, + }; +}; + +export const rnbwHandleProviderRequest = ({ + providerRequestTransport, + getFeatureFlags, + checkRateLimit, + isSupportedChain, + getActiveSession, + removeAppSession, + getChainNativeCurrency, + getProvider, + messengerProviderRequest, + onAddEthereumChain, + onSwitchEthereumChainNotSupported, + onSwitchEthereumChainSupported, +}: { + providerRequestTransport: IProviderRequestTransport; + getFeatureFlags: () => { custom_rpc: boolean }; + checkRateLimit: ({ + id, + meta, + method, + }: { + id: number; + meta: CallbackOptions; + method: string; + }) => Promise<{ id: number; error: Error } | undefined>; + isSupportedChain: (chainId: number) => boolean; + getActiveSession: ({ host }: { host: string }) => ActiveSession; + removeAppSession?: ({ host }: { host: string }) => void; + getChainNativeCurrency: (chainId: number) => any | undefined; + getProvider: (options: { chainId?: number }) => Provider; + messengerProviderRequest: ( + request: ProviderRequestPayload, + ) => Promise; + onAddEthereumChain: ({ + proposedChain, + callbackOptions, + }: { + proposedChain: AddEthereumChainProposedChain; + callbackOptions?: CallbackOptions; + }) => { chainAlreadyAdded: boolean }; + onSwitchEthereumChainNotSupported: ({ + proposedChain, + callbackOptions, + }: { + proposedChain: AddEthereumChainProposedChain; + callbackOptions?: CallbackOptions; + }) => void; + onSwitchEthereumChainSupported: ({ + proposedChain, + callbackOptions, + }: { + proposedChain: AddEthereumChainProposedChain; + callbackOptions?: CallbackOptions; + }) => void; +}) => + providerRequestTransport?.reply(async ({ method, id, params }, meta) => { + try { + const rateLimited = await checkRateLimit({ id, meta, method }); + if (rateLimited) { + return buildError({ + id, + message: 'Rate Limit Exceeded', + errorCode: errorCodes.LIMIT_EXCEEDED, + }); + } + + const url = meta?.sender?.url || ''; + const host = (isValidUrl(url) && getDappHost(url)) || ''; + const activeSession = getActiveSession({ host }); + + let response: unknown = null; + + switch (method) { + case 'eth_chainId': { + response = activeSession ? toHex(activeSession.chainId) : '0x1'; + break; + } + case 'eth_coinbase': { + response = validateAndFormatAddress(activeSession?.address) || ''; + break; + } + case 'eth_accounts': { + response = activeSession + ? [validateAndFormatAddress(activeSession.address)] + : []; + break; + } + case 'eth_blockNumber': { + const provider = getProvider({ chainId: activeSession?.chainId }); + const blockNumber = await provider.getBlockNumber(); + response = toHex(blockNumber); + break; + } + case 'eth_getBalance': { + const p = params as Array; + const provider = getProvider({ chainId: activeSession?.chainId }); + const balance = await provider.getBalance(p?.[0] as string); + response = toHex(balance); + break; + } + case 'eth_getTransactionByHash': { + const p = params as Array; + const provider = getProvider({ chainId: activeSession?.chainId }); + const transaction = await provider.getTransaction(p?.[0] as string); + const normalizedTransaction = + normalizeTransactionResponsePayload(transaction); + const { + gasLimit, + gasPrice, + maxFeePerGas, + maxPriorityFeePerGas, + value, + } = normalizedTransaction; + response = { + ...normalizedTransaction, + gasLimit: toHex(gasLimit), + gasPrice: gasPrice ? toHex(gasPrice) : undefined, + maxFeePerGas: maxFeePerGas ? toHex(maxFeePerGas) : undefined, + maxPriorityFeePerGas: maxPriorityFeePerGas + ? toHex(maxPriorityFeePerGas) + : undefined, + value: toHex(value), + }; + break; + } + case 'eth_call': { + const p = params as Array; + const provider = getProvider({ chainId: activeSession?.chainId }); + response = await provider.call(p?.[0] as TransactionRequest); + break; + } + case 'eth_estimateGas': { + const p = params as Array; + const provider = getProvider({ chainId: activeSession?.chainId }); + const gas = await provider.estimateGas(p?.[0] as TransactionRequest); + response = toHex(gas); + break; + } + case 'eth_gasPrice': { + const provider = getProvider({ chainId: activeSession?.chainId }); + const gasPrice = await provider.getGasPrice(); + response = toHex(gasPrice); + break; + } + case 'eth_getCode': { + const p = params as Array; + const provider = getProvider({ chainId: activeSession?.chainId }); + response = await provider.getCode(p?.[0] as string, p?.[1] as string); + break; + } + case 'eth_sendTransaction': + case 'signTransaction': + case 'signAndSendTransaction': + case 'eth_signTransaction': + case 'personal_sign': + case 'eth_signTypedData': + case 'eth_signTypedData_v3': + case 'eth_signTypedData_v4': { + // If we need to validate the input before showing the UI, it should go here. + const p = params as Array; + if (method === 'eth_signTypedData_v4') { + // we don't trust the params order + let dataParam = p?.[1]; + if (!isAddress(p?.[0] as Address)) { + dataParam = p?.[0]; + } + + const data = + typeof dataParam === 'string' ? JSON.parse(dataParam) : dataParam; + + const { + domain: { chainId }, + } = data as { domain: { chainId: string } }; + + if ( + chainId !== undefined && + Number(chainId) !== Number(activeSession?.chainId) + ) { + return buildError({ + id, + message: 'Chain Id mismatch', + errorCode: errorCodes.INVALID_REQUEST, + }); + } + } + + response = await messengerProviderRequest({ + method, + id, + params, + meta, + }); + break; + } + case 'wallet_addEthereumChain': { + const p = params as Array; + const proposedChain = p?.[0] as AddEthereumChainProposedChain; + const proposedChainId = Number(proposedChain.chainId); + const featureFlags = getFeatureFlags(); + if (!featureFlags.custom_rpc) { + const supportedChain = isSupportedChain?.(proposedChainId); + if (!supportedChain) { + return buildError({ + id, + message: 'Chain Id not supported', + errorCode: errorCodes.INVALID_REQUEST, + }); + } + } else { + const { + chainId, + rpcUrls: [rpcUrl], + nativeCurrency: { name, symbol, decimals }, + blockExplorerUrls: [blockExplorerUrl], + } = proposedChain; + + // Validate chain Id + if (!isHex(chainId)) { + return buildError({ + id, + message: `Expected 0x-prefixed, unpadded, non-zero hexadecimal string "chainId". Received: ${chainId}`, + errorCode: errorCodes.INVALID_INPUT, + }); + } else if (Number(chainId) > Number.MAX_SAFE_INTEGER) { + return buildError({ + id, + message: `Invalid chain ID "${chainId}": numerical value greater than max safe value. Received: ${chainId}`, + errorCode: errorCodes.INVALID_INPUT, + }); + // Validate symbol and name + } else if (!rpcUrl) { + return buildError({ + id, + message: `Expected non-empty array[string] "rpcUrls". Received: ${rpcUrl}`, + errorCode: errorCodes.INVALID_INPUT, + }); + } else if (!name || !symbol) { + return buildError({ + id, + message: + 'Expected non-empty string "nativeCurrency.name", "nativeCurrency.symbol"', + errorCode: errorCodes.INVALID_INPUT, + }); + // Validate decimals + } else if ( + !Number.isInteger(decimals) || + decimals < 0 || + decimals > 36 + ) { + return buildError({ + id, + message: `Expected non-negative integer "nativeCurrency.decimals" less than 37. Received: ${decimals}`, + errorCode: errorCodes.INVALID_INPUT, + }); + // Validate symbol length + } else if (symbol.length < 2 || symbol.length > 6) { + return buildError({ + id, + message: `Expected 2-6 character string 'nativeCurrency.symbol'. Received: ${symbol}`, + errorCode: errorCodes.INVALID_INPUT, + }); + // Validate symbol against existing chains + } else if (isSupportedChain?.(Number(chainId))) { + const knownChainNativeCurrency = getChainNativeCurrency( + Number(chainId), + ); + if (knownChainNativeCurrency?.symbol !== symbol) { + return buildError({ + id, + message: `nativeCurrency.symbol does not match currency symbol for a network the user already has added with the same chainId. Received: ${symbol}`, + errorCode: errorCodes.INVALID_INPUT, + }); + } + // Validate blockExplorerUrl + } else if (!blockExplorerUrl) { + return buildError({ + id, + message: `Expected null or array with at least one valid string HTTPS URL 'blockExplorerUrl'. Received: ${blockExplorerUrl}`, + errorCode: errorCodes.INVALID_INPUT, + }); + } + const { chainAlreadyAdded } = onAddEthereumChain({ + proposedChain, + callbackOptions: meta, + }); + + if (!chainAlreadyAdded) { + response = await messengerProviderRequest({ + method, + id, + params, + meta, + }); + } + + // PER EIP - return null if the network was added otherwise throw + if (!response) { + return buildError({ + id, + message: 'User rejected the request.', + errorCode: errorCodes.TRANSACTION_REJECTED, + }); + } else { + response = null; + } + } + break; + } + case 'wallet_switchEthereumChain': { + const p = params as Array; + const proposedChain = p?.[0] as AddEthereumChainProposedChain; + const supportedChainId = isSupportedChain?.( + Number(proposedChain.chainId), + ); + if (!activeSession) { + (await messengerProviderRequest({ + method: 'eth_requestAccounts', + id, + params, + meta, + })) as { address: Address; chainId: number }; + } else if (!supportedChainId) { + onSwitchEthereumChainNotSupported?.({ + proposedChain, + callbackOptions: meta, + }); + return buildError({ + id, + message: 'Chain Id not supported', + errorCode: errorCodes.INVALID_REQUEST, + }); + } else { + onSwitchEthereumChainSupported?.({ + proposedChain, + callbackOptions: meta, + }); + } + response = null; + break; + } + case 'wallet_watchAsset': { + const featureFlags = getFeatureFlags(); + + if (!featureFlags.custom_rpc) { + throw new Error('Method not supported'); + } else { + const { + type, + options: { address, symbol, decimals }, + } = params as unknown as { + type: string; + options: { + address: Address; + symbol?: string; + decimals?: number; + }; + }; + if (type !== 'ERC20') { + return buildError({ + id, + message: 'Method supported only for ERC20', + errorCode: errorCodes.METHOD_NOT_SUPPORTED, + }); + } + + if (!address) { + return buildError({ + id, + message: 'Address is required', + errorCode: errorCodes.INVALID_INPUT, + }); + } + + let chainId: number | null = null; + if (activeSession) { + chainId = activeSession?.chainId; + } else { + chainId = deriveChainIdByHostname(host); + } + + response = await messengerProviderRequest({ + method, + id, + params: [ + { + address, + symbol, + decimals, + chainId, + }, + ], + meta, + }); + // PER EIP - true if the token was added, false otherwise. + response = !!response; + break; + } + } + case 'eth_requestAccounts': { + if (activeSession) { + response = [validateAndFormatAddress(activeSession.address)]; + break; + } + const { address } = (await messengerProviderRequest({ + method, + id, + params, + meta, + })) as { address: Address; chainId: number }; + response = [validateAndFormatAddress(address)]; + break; + } + case 'personal_ecRecover': { + const p = params as Array; + response = recoverPersonalSignature({ + data: p?.[0] as string, + signature: p?.[1] as string, + }); + break; + } + case 'wallet_revokePermissions': { + if ( + !!removeAppSession && + (params?.[0] as WalletPermissionsParams)?.eth_accounts + ) { + removeAppSession?.({ host }); + response = null; + } + throw new Error('next'); + } + default: { + try { + if (method?.substring(0, 7) === 'wallet_') { + // Generic error that will be hanlded correctly in the catch + throw new Error('next'); + } + // Let's try to fwd the request to the provider + const provider = getProvider({ + chainId: activeSession?.chainId, + }) as StaticJsonRpcProvider; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + response = await provider.send(method, params as any[]); + } catch (e) { + return buildError({ + id, + message: 'Method not supported', + errorCode: errorCodes.METHOD_NOT_SUPPORTED, + }); + } + } + } + return { id, result: response }; + } catch (error) { + return buildError({ + id, + message: (error as Error).message, + errorCode: errorCodes.INTERNAL_ERROR, + }); + } + }); diff --git a/src/entries/background/index.ts b/src/entries/background/index.ts index 62d582abd9..37dd8b68f9 100644 --- a/src/entries/background/index.ts +++ b/src/entries/background/index.ts @@ -26,6 +26,8 @@ const inpageMessenger = initializeMessenger({ connect: 'inpage' }); unifyBalancesOnApps( '/', `${process.env.ORBY_BASE_URL}/${process.env.ORBY_PRIVATE_API_KEY}`, + true, + 6_000_00, ); handleInstallExtension(); diff --git a/src/entries/inpage/RainbowProvider.ts b/src/entries/inpage/RainbowProvider.ts new file mode 100644 index 0000000000..55e5a70959 --- /dev/null +++ b/src/entries/inpage/RainbowProvider.ts @@ -0,0 +1,287 @@ +import { + ChainIdHex, + Ethereum, + RPCMethod, +} from '@rainbow-me/provider/dist/references/ethereum'; +import { + IMessenger, + IProviderRequestTransport, + RequestArguments, + RequestError, + RequestResponse, +} from '@rainbow-me/provider/dist/references/messengers'; +import { toHex } from '@rainbow-me/provider/dist/utils/hex'; +import { isVersionedTransaction } from '@solana/wallet-adapter-base'; +import { + SolanaSignInInput, + SolanaSignInOutput, +} from '@solana/wallet-standard-features'; +import { + PublicKey, + SendOptions, + Transaction, + TransactionSignature, + VersionedTransaction, +} from '@solana/web3.js'; +import { EventEmitter } from 'eventemitter3'; + +import { SolanaProvider } from '../wallet-standard/src/window'; + +export class RainbowProvider extends EventEmitter implements SolanaProvider { + chainId: ChainIdHex | undefined; + connected = false; + isRainbow = true; + isReady = true; + isMetaMask = true; + networkVersion = '1'; + selectedAddress: string | undefined; + publicKey?: PublicKey | null = null; + providers: (RainbowProvider | Ethereum)[] | undefined = undefined; + + requestId = 0; + + private backgroundMessenger?: IMessenger; + private providerRequestTransport?: IProviderRequestTransport; + + [key: string]: unknown; + + constructor({ + backgroundMessenger, + providerRequestTransport, + onConstruct, + }: + | { + backgroundMessenger?: IMessenger; + providerRequestTransport?: IProviderRequestTransport; + onConstruct?: ({ + emit, + }: { + emit: (event: string, ...args: unknown[]) => void; + }) => void; + } + | undefined = {}) { + super(); + this.backgroundMessenger = backgroundMessenger; + this.providerRequestTransport = providerRequestTransport; + onConstruct?.({ emit: this.emit.bind(this) }); + + // EIP-6963 RainbowInjectedProvider in announceProvider was losing context + this.bindMethods(); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async connect(options?: { + onlyIfTrusted?: boolean; + }): Promise<{ publicKey?: PublicKey }> { + if (!this.providerRequestTransport) throw new Error('No transport'); + this.backgroundMessenger?.send( + 'rainbow_prefetchDappMetadata', + window.location.href, + ); + + // eslint-disable-next-line no-plusplus + const id = this.requestId++; + const response = await this.providerRequestTransport?.send( + { + id, + method: 'eth_requestAccounts', + params: [{ chainId: 101 }], + }, + { id }, + ); + + if (response?.result?.[0]) { + this.publicKey = new PublicKey(response.result[0]); + } else { + this.publicKey = undefined; + } + + return { publicKey: this.publicKey }; + } + + async disconnect(): Promise { + this.publicKey = null; + } + + async signAndSendTransaction( + transaction: T, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + options?: SendOptions, + ): Promise<{ signature: TransactionSignature }> { + // eslint-disable-next-line no-plusplus + const id = this.requestId++; + + if (isVersionedTransaction(transaction)) { + const response = await this.providerRequestTransport?.send( + { + id, + method: 'signAndSendTransaction', + params: [Buffer.from(transaction.serialize())?.toString('hex')], + }, + { id }, + ); + + return { signature: response?.result[0] }; + } + + return { signature: undefined as unknown as string }; + } + + async signTransaction( + transaction: T, + ): Promise { + // eslint-disable-next-line no-plusplus + const id = this.requestId++; + + if (isVersionedTransaction(transaction)) { + const response = await this.providerRequestTransport?.send( + { + id, + method: 'signTransaction', + params: [Buffer.from(transaction.serialize())?.toString('hex')], + }, + { id }, + ); + + if (response?.result[0]) { + // @ts-ignore + return VersionedTransaction.deserialize(Buffer.from(data, 'hex')) as T; + } + } else { + console.error('only support versioned transaction for now'); + } + + // Simulate signing a transaction + return undefined as unknown as T; + } + + async signAllTransactions( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + transactions: T[], + ): Promise { + throw new Error('Method not implemented.'); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async signMessage(message: Uint8Array): Promise<{ signature: Uint8Array }> { + throw new Error('Method not implemented.'); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async signIn(input?: SolanaSignInInput): Promise { + throw new Error('Method not implemented.'); + } + + bindMethods() { + for (const key of Object.getOwnPropertyNames(Object.getPrototypeOf(this))) { + const value = this[key]; + if (typeof value === 'function' && key !== 'constructor') { + this[key] = value.bind(this); + } + } + } + + /** + * @deprecated – This method is deprecated in favor of the RPC method `eth_requestAccounts`. + * @link https://eips.ethereum.org/EIPS/eip-1102#providerenable-deprecated + **/ + async enable() { + return this.request({ method: 'eth_requestAccounts' }); + } + + isConnected() { + return this.connected; + } + + async handleChainChanged(chainId: ChainIdHex) { + this.chainId = chainId; + this.networkVersion = parseInt(this.chainId, 16).toString(); + this.emit('chainChanged', toHex(String(chainId))); + } + + async request({ + method, + params, + }: RequestArguments): Promise { + if (!this.providerRequestTransport) throw new Error('No transport'); + this.backgroundMessenger?.send( + 'rainbow_prefetchDappMetadata', + window.location.href, + ); + // eslint-disable-next-line no-plusplus + const id = this.requestId++; + const response = await this.providerRequestTransport?.send( + { + id, + method, + params, + }, + { id }, + ); + + console.log('rainbow_prefetchDappMetadata', id, response); + + if (response.id !== id) return; + if (response.error) throw response.error; + + switch (method) { + case 'eth_requestAccounts': + this.selectedAddress = response.result[0]; + this.connected = true; + break; + case 'eth_chainId': + this.chainId = response.result; + this.networkVersion = parseInt(this.chainId, 16).toString(); + break; + default: + break; + } + + return response.result; + } + + /** @deprecated – This method is deprecated in favor of `request`. */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async sendAsync( + args: RequestArguments, + callback: (error: RequestError | null, response: RequestResponse) => void, + ) { + try { + const result = await this.request(args); + callback(null, { + id: args.id!, + jsonrpc: '2.0', + result, + }); + } catch (error: unknown) { + callback(error as Error, { + id: args.id!, + jsonrpc: '2.0', + error: { + code: (error as RequestError).code, + message: (error as RequestError).message, + name: (error as RequestError).name, + }, + }); + } + } + + /** @deprecated – This method is deprecated in favor of `request`. */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async send( + methodOrPayload: string | RequestArguments, + paramsOrCallback: Array, + ) { + if ( + typeof methodOrPayload === 'string' && + Array.isArray(paramsOrCallback) + ) { + return this.request({ + method: methodOrPayload as RPCMethod, + params: paramsOrCallback, + }); + } else { + return this.request(methodOrPayload as RequestArguments); + } + } +} diff --git a/src/entries/inpage/index.ts b/src/entries/inpage/index.ts index 85ce63987f..3c5c75f58c 100644 --- a/src/entries/inpage/index.ts +++ b/src/entries/inpage/index.ts @@ -1,4 +1,3 @@ -import { RainbowProvider } from '@rainbow-me/provider'; import { uuid4 } from '@sentry/utils'; import _ from 'lodash'; import { EIP1193Provider, announceProvider } from 'mipd'; @@ -12,6 +11,9 @@ import { toHex } from '~/core/utils/hex'; import { injectNotificationIframe } from '../iframe'; import { IN_DAPP_NOTIFICATION_STATUS } from '../iframe/notification'; +import { initialize } from '../wallet-standard/src'; + +import { RainbowProvider } from './RainbowProvider'; declare global { interface Window { @@ -45,6 +47,7 @@ const rainbowProvider = new RainbowProvider({ // here we don't need to listen to anything so we don't need these listeners if (isValidUrl(window.location.href)) { const host = getDappHost(window.location.href); + console.log('host', host); messenger?.reply(`accountsChanged:${host}`, async (address) => { emit('accountsChanged', [address]); }); @@ -72,7 +75,7 @@ if (shouldInjectProvider()) { announceProvider({ info: { icon: RAINBOW_ICON_RAW_SVG, - name: 'Rainbow', + name: 'OrbyPlayground', rdns: 'me.rainbow', uuid: uuid4(), }, @@ -151,6 +154,8 @@ if (shouldInjectProvider()) { }, }); + initialize(rainbowProvider); + window.dispatchEvent(new Event('ethereum#initialized')); backgroundMessenger.reply( diff --git a/src/entries/popup/App.tsx b/src/entries/popup/App.tsx index 2b4b29854d..50f15faf63 100644 --- a/src/entries/popup/App.tsx +++ b/src/entries/popup/App.tsx @@ -1,10 +1,9 @@ -import { Account, AccountType, VMType } from '@orb-labs/orby-core'; +import { Account, AccountType } from '@orb-labs/orby-core'; import { OrbyProvider } from '@orb-labs/orby-react'; import { QueryClientProvider } from '@tanstack/react-query'; import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'; import { isEqual } from 'lodash'; import * as React from 'react'; -import { useMemo } from 'react'; import { WagmiProvider } from 'wagmi'; import { analytics } from '~/analytics'; @@ -23,6 +22,7 @@ import { } from '~/core/state'; import { useCurrentThemeStore } from '~/core/state/currentSettings/currentTheme'; import { POPUP_DIMENSIONS } from '~/core/utils/dimensions'; +import { getWalletVirtualEnvironment } from '~/core/utils/orb'; import { WagmiConfigUpdater, wagmiConfig } from '~/core/wagmi'; import { Box, ThemeProvider } from '~/design-system'; @@ -98,23 +98,21 @@ export function App() { const { currentTheme } = useCurrentThemeStore(); const isFullScreen = useIsFullScreen(); - const { currentAddress } = useCurrentAddressStore(); + const { currentAddresses } = useCurrentAddressStore(); + + const orbyConfig = React.useMemo(() => { + const accounts = currentAddresses?.map((address) => { + const vm = getWalletVirtualEnvironment(address); + return new Account(address, AccountType.EOA, vm!, undefined); + }); - const orbyConfig = useMemo(() => { return { instancePrivateAPIKey: process.env.ORBY_PRIVATE_API_KEY as string, instancePublicAPIKey: process.env.ORBY_PUBLIC_API_KEY as string, appName: 'Rainbow', - accounts: [ - new Account( - currentAddress?.toLowerCase(), - AccountType.EOA, - VMType.EVM, - undefined, - ), - ], + accounts, }; - }, [currentAddress]); + }, [currentAddresses]); return ( <> diff --git a/src/entries/popup/components/CommandK/useCommands.tsx b/src/entries/popup/components/CommandK/useCommands.tsx index d90e34481e..3c672d6913 100644 --- a/src/entries/popup/components/CommandK/useCommands.tsx +++ b/src/entries/popup/components/CommandK/useCommands.tsx @@ -102,27 +102,27 @@ export const getStaticCommandInfo = (): CommandInfo => { to: ROUTES.SEND, type: SearchItemType.Shortcut, }, - swap: { - actionLabel: actionLabels.open, - hideForWatchedWallets: true, - name: getCommandName('swap'), - page: PAGES.HOME, - searchTags: getSearchTags('swap'), - shortcut: shortcuts.home.GO_TO_SWAP, - symbol: 'arrow.triangle.swap', - symbolSize: 15.5, - type: SearchItemType.Shortcut, - }, - bridge: { - actionLabel: actionLabels.open, - hideForWatchedWallets: true, - name: getCommandName('bridge'), - page: PAGES.HOME, - searchTags: getSearchTags('bridge'), - symbol: 'arrow.turn.up.right', - symbolSize: 15.5, - type: SearchItemType.Shortcut, - }, + // swap: { + // actionLabel: actionLabels.open, + // hideForWatchedWallets: true, + // name: getCommandName('swap'), + // page: PAGES.HOME, + // searchTags: getSearchTags('swap'), + // shortcut: shortcuts.home.GO_TO_SWAP, + // symbol: 'arrow.triangle.swap', + // symbolSize: 15.5, + // type: SearchItemType.Shortcut, + // }, + // bridge: { + // actionLabel: actionLabels.open, + // hideForWatchedWallets: true, + // name: getCommandName('bridge'), + // page: PAGES.HOME, + // searchTags: getSearchTags('bridge'), + // symbol: 'arrow.turn.up.right', + // symbolSize: 15.5, + // type: SearchItemType.Shortcut, + // }, myWallets: { actionLabel: actionLabels.view, name: getCommandName('my_wallets'), @@ -384,30 +384,30 @@ export const getStaticCommandInfo = (): CommandInfo => { symbolSize: 14.5, type: SearchItemType.Shortcut, }, - swapToken: { - actionLabel: actionLabels.open, - hideForWatchedWallets: true, - hideFromMainSearch: true, - name: getCommandName('swap_token'), - page: PAGES.TOKEN_DETAIL, - searchTags: getSearchTags('swap'), - shortcut: shortcuts.home.GO_TO_SWAP, - symbol: 'arrow.triangle.swap', - symbolSize: 15.5, - type: SearchItemType.Shortcut, - }, - bridgeToken: { - actionLabel: actionLabels.open, - hideForWatchedWallets: true, - hideFromMainSearch: true, - name: getCommandName('bridge'), - page: PAGES.TOKEN_DETAIL, - searchTags: getSearchTags('swap'), - shortcut: shortcuts.home.GO_TO_SWAP, - symbol: 'arrow.turn.up.right', - symbolSize: 15.5, - type: SearchItemType.Shortcut, - }, + // swapToken: { + // actionLabel: actionLabels.open, + // hideForWatchedWallets: true, + // hideFromMainSearch: true, + // name: getCommandName('swap_token'), + // page: PAGES.TOKEN_DETAIL, + // searchTags: getSearchTags('swap'), + // shortcut: shortcuts.home.GO_TO_SWAP, + // symbol: 'arrow.triangle.swap', + // symbolSize: 15.5, + // type: SearchItemType.Shortcut, + // }, + // bridgeToken: { + // actionLabel: actionLabels.open, + // hideForWatchedWallets: true, + // hideFromMainSearch: true, + // name: getCommandName('bridge'), + // page: PAGES.TOKEN_DETAIL, + // searchTags: getSearchTags('swap'), + // shortcut: shortcuts.home.GO_TO_SWAP, + // symbol: 'arrow.turn.up.right', + // symbolSize: 15.5, + // type: SearchItemType.Shortcut, + // }, hideToken: { actionLabel: actionLabels.activateCommand, hideForWatchedWallets: true, diff --git a/src/entries/popup/components/CommandK/utils.ts b/src/entries/popup/components/CommandK/utils.ts index 86c0e88288..0a5d8e31cf 100644 --- a/src/entries/popup/components/CommandK/utils.ts +++ b/src/entries/popup/components/CommandK/utils.ts @@ -1,3 +1,4 @@ +import { validateAndFormatAddress } from '@orb-labs/orby-core'; import * as React from 'react'; import { analytics } from '~/analytics'; @@ -199,7 +200,7 @@ const calculateCommandRelevance = ( command.type === SearchItemType.Contact ) { const normalizedAddress = command.address - ? command.address.toLowerCase() + ? validateAndFormatAddress(command.address) : ''; const normalizedWalletName = command.walletName ? command.walletName.toLowerCase() diff --git a/src/entries/popup/components/FlyingRainbows/FlyingRainbows.tsx b/src/entries/popup/components/FlyingRainbows/FlyingRainbows.tsx index bd9112a487..cc5d33ed59 100644 --- a/src/entries/popup/components/FlyingRainbows/FlyingRainbows.tsx +++ b/src/entries/popup/components/FlyingRainbows/FlyingRainbows.tsx @@ -63,7 +63,7 @@ export function FlyingRainbows({ children }: { children: React.ReactNode }) { right="0" bottom="0" > - - */} + {/* - */} + {/* - */} + {/* - */} + {/* + /> */} {children} diff --git a/src/entries/popup/components/ImportWallet/ImportWalletSelection.tsx b/src/entries/popup/components/ImportWallet/ImportWalletSelection.tsx index 1a9b77cc49..710ad6dcf2 100644 --- a/src/entries/popup/components/ImportWallet/ImportWalletSelection.tsx +++ b/src/entries/popup/components/ImportWallet/ImportWalletSelection.tsx @@ -44,6 +44,7 @@ const derivedAccountsFromSecret = async (secret: string) => { if (current[secret]) return current[secret]; const accounts = await deriveAccountsFromSecret(secret); + console.log('derivedAccountsFromSecret', accounts); derivedAccountsStore.set({ ...current, [secret]: accounts }); return accounts || ([] as Address[]); @@ -107,16 +108,16 @@ export const useImportWalletsFromSecrets = () => { export const ImportWalletSelection = ({ onboarding = false }) => { const navigate = useRainbowNavigate(); const setCurrentAddress = useCurrentAddressStore.use.setCurrentAddress(); + const setCurrentAddresses = useCurrentAddressStore.use.setCurrentAddresses(); const secrets = useImportWalletSessionSecrets(); const accountsToImport = useDeriveAccountsFromSecrets(secrets); const { importSecrets, isImporting } = useImportWalletsFromSecrets(); - const { isLoading: walletsSummaryIsLoading, walletsSummary } = - useWalletsSummary({ - addresses: accountsToImport, - }); + const { walletsSummary } = useWalletsSummary({ + addresses: accountsToImport, + }); const handleEditWallets = () => { navigate( @@ -130,6 +131,7 @@ export const ImportWalletSelection = ({ onboarding = false }) => { const onImport = () => importSecrets({ secrets }).then(() => { setCurrentAddress(accountsToImport[0]); + setCurrentAddresses(accountsToImport); if (onboarding) navigate(ROUTES.CREATE_PASSWORD, { state: { backTo: ROUTES.IMPORT__SEED }, @@ -137,8 +139,7 @@ export const ImportWalletSelection = ({ onboarding = false }) => { else navigate(ROUTES.HOME); }); - const isReady = - !!accountsToImport?.length && !isImporting && !walletsSummaryIsLoading; + const isReady = !!accountsToImport?.length && !isImporting; const hasRecentlyUsedWallet = useMemo( () => diff --git a/src/entries/popup/components/ImportWallet/ImportWalletSelectionEdit.tsx b/src/entries/popup/components/ImportWallet/ImportWalletSelectionEdit.tsx index 8f3447c6f1..cca66a62e3 100644 --- a/src/entries/popup/components/ImportWallet/ImportWalletSelectionEdit.tsx +++ b/src/entries/popup/components/ImportWallet/ImportWalletSelectionEdit.tsx @@ -61,6 +61,7 @@ const emptyArray: unknown[] = []; export function ImportWalletSelectionEdit({ onboarding = false }) { const navigate = useRainbowNavigate(); const setCurrentAddress = useCurrentAddressStore.use.setCurrentAddress(); + const setCurrentAddresses = useCurrentAddressStore.use.setCurrentAddresses(); const { state } = useLocation(); const accountsToImport: Address[] = state.accountsToImport || emptyArray; @@ -89,6 +90,7 @@ export function ImportWalletSelectionEdit({ onboarding = false }) { (a) => !accountsIgnored.includes(a), ); setCurrentAddress(importedAccounts[0]); + setCurrentAddresses(importedAccounts); if (onboarding) { navigate(ROUTES.CREATE_PASSWORD, { state: { backTo: ROUTES.IMPORT__SEED }, diff --git a/src/entries/popup/components/ImportWallet/ImportWalletViaPrivateKey.tsx b/src/entries/popup/components/ImportWallet/ImportWalletViaPrivateKey.tsx index 25b3e2826c..8fe9d948da 100644 --- a/src/entries/popup/components/ImportWallet/ImportWalletViaPrivateKey.tsx +++ b/src/entries/popup/components/ImportWallet/ImportWalletViaPrivateKey.tsx @@ -45,6 +45,7 @@ const ImportWalletViaPrivateKey = () => { const [isAddingWallets, setIsAddingWallets] = useState(false); const [secrets, setSecrets] = useState(['']); const setCurrentAddress = useCurrentAddressStore.use.setCurrentAddress(); + const setCurrentAddresses = useCurrentAddressStore.use.setCurrentAddresses(); const [validity, setValidity] = useState< { valid: boolean; too_long: boolean; type: string | undefined }[] @@ -116,6 +117,7 @@ const ImportWalletViaPrivateKey = () => { secrets[0], )) as Address; setCurrentAddress(address); + setCurrentAddresses([address]); setIsAddingWallets(false); // workaround for a deeper issue where the keychain status @@ -136,7 +138,14 @@ const ImportWalletViaPrivateKey = () => { } } } - }, [isAddingWallets, navigate, onboarding, secrets, setCurrentAddress]); + }, [ + isAddingWallets, + navigate, + onboarding, + secrets, + setCurrentAddress, + setCurrentAddresses, + ]); const handleKeyDown = useCallback( (e: KeyboardEvent) => { diff --git a/src/entries/popup/components/SwitchMenu/SwitchNetworkMenu.tsx b/src/entries/popup/components/SwitchMenu/SwitchNetworkMenu.tsx index 3c18dada17..c4fb00701f 100644 --- a/src/entries/popup/components/SwitchMenu/SwitchNetworkMenu.tsx +++ b/src/entries/popup/components/SwitchMenu/SwitchNetworkMenu.tsx @@ -1,3 +1,4 @@ +import { VMType, getVirtualEnvironment } from '@orb-labs/orby-core'; import React, { useCallback, useMemo, useRef } from 'react'; import { Chain } from 'viem'; @@ -55,6 +56,7 @@ export const SwitchNetworkMenuSelector = ({ onNetworkSelect, onShortcutPress, onlySwapSupportedNetworks = false, + vmType, }: { selectedValue?: string; highlightAccentColor?: boolean; @@ -64,6 +66,7 @@ export const SwitchNetworkMenuSelector = ({ onNetworkSelect?: (event?: Event) => void; onShortcutPress: (chainId: string) => void; onlySwapSupportedNetworks?: boolean; + vmType?: VMType; }) => { const { trackShortcut } = useKeyboardAnalytics(); const { chains: userChains } = useUserChains(); @@ -72,10 +75,11 @@ export const SwitchNetworkMenuSelector = ({ () => userChains.filter((chain) => onlySwapSupportedNetworks - ? supportedSwapChainIds.includes(chain.id) - : true, + ? supportedSwapChainIds.includes(chain.id) && + (!vmType || vmType == getVirtualEnvironment(BigInt(chain.id))) + : !vmType || vmType == getVirtualEnvironment(BigInt(chain.id)), ), - [onlySwapSupportedNetworks, userChains], + [onlySwapSupportedNetworks, userChains, vmType], ); const { MenuRadioItem } = useMemo(() => { @@ -235,6 +239,7 @@ interface SwitchNetworkMenuProps { marginRight?: Space; onOpenChange?: (open: boolean) => void; onlySwapSupportedNetworks?: boolean; + vmType?: VMType; } export const SwitchNetworkMenu = ({ @@ -247,6 +252,7 @@ export const SwitchNetworkMenu = ({ marginRight, onOpenChange, onlySwapSupportedNetworks, + vmType, }: SwitchNetworkMenuProps) => { const triggerRef = useRef(null); const { chains } = useUserChains(); @@ -329,6 +335,7 @@ export const SwitchNetworkMenu = ({ showDisconnect={!!onDisconnect} disconnect={onDisconnect} onlySwapSupportedNetworks={onlySwapSupportedNetworks} + vmType={vmType} /> diff --git a/src/entries/popup/components/WatchWallet/WatchWallet.tsx b/src/entries/popup/components/WatchWallet/WatchWallet.tsx index 5fe855dd83..560deaaac5 100644 --- a/src/entries/popup/components/WatchWallet/WatchWallet.tsx +++ b/src/entries/popup/components/WatchWallet/WatchWallet.tsx @@ -266,6 +266,7 @@ export const WatchWallet = ({ ); const setCurrentAddress = useCurrentAddressStore.use.setCurrentAddress(); + const setCurrentAddresses = useCurrentAddressStore.use.setCurrentAddresses(); const save = useSavedEnsNames.use.save(); const [renameAccount, setRenameAccount] = useState
(); @@ -286,6 +287,7 @@ export const WatchWallet = ({ save(ensName, address); } setCurrentAddress(importedAddresses[0]); + setCurrentAddresses(importedAddresses); if (!onboarding && !ensName) setRenameAccount(address); else onFinishImporting?.(); } @@ -294,6 +296,7 @@ export const WatchWallet = ({ ensName, address, setCurrentAddress, + setCurrentAddresses, onboarding, onFinishImporting, save, diff --git a/src/entries/popup/handlers/ledger.ts b/src/entries/popup/handlers/ledger.ts index eea1a521c8..0387267b0a 100644 --- a/src/entries/popup/handlers/ledger.ts +++ b/src/entries/popup/handlers/ledger.ts @@ -12,6 +12,7 @@ import AppEth, { ledgerService } from '@ledgerhq/hw-app-eth'; import type Transport from '@ledgerhq/hw-transport'; import TransportWebHID from '@ledgerhq/hw-transport-webhid'; import { SignTypedDataVersion, TypedDataUtils } from '@metamask/eth-sig-util'; +import { validateAndFormatAddress } from '@orb-labs/orby-core'; import { Address } from 'viem'; import { i18n } from '~/core/languages'; @@ -88,7 +89,10 @@ export async function signTransactionFromLedger( const parsedTx = parse(serializedTransaction); - if (parsedTx.from?.toLowerCase() !== address?.toLowerCase()) { + if ( + validateAndFormatAddress(parsedTx.from) !== + validateAndFormatAddress(address) + ) { throw new Error('Transaction was not signed by the right address'); } diff --git a/src/entries/popup/handlers/trezor.ts b/src/entries/popup/handlers/trezor.ts index a7e5238e09..c219b8d532 100644 --- a/src/entries/popup/handlers/trezor.ts +++ b/src/entries/popup/handlers/trezor.ts @@ -12,6 +12,7 @@ import { serialize, } from '@ethersproject/transactions'; import { SignTypedDataVersion, TypedDataUtils } from '@metamask/eth-sig-util'; +import { validateAndFormatAddress } from '@orb-labs/orby-core'; import transformTypedDataPlugin from '@trezor/connect-plugin-ethereum'; import { Address } from 'viem'; @@ -79,7 +80,10 @@ export async function signTransactionFromTrezor( }); const parsedTx = parse(serializedTransaction); - if (parsedTx.from?.toLowerCase() !== address?.toLowerCase()) { + if ( + validateAndFormatAddress(parsedTx.from) !== + validateAndFormatAddress(address) + ) { throw new Error('Transaction was not signed by the right address'); } @@ -134,7 +138,10 @@ export async function signMessageByTypeFromTrezor( hex: true, }); - if (response.payload.address.toLowerCase() !== address.toLowerCase()) { + if ( + validateAndFormatAddress(response.payload.address) !== + validateAndFormatAddress(address) + ) { throw new Error( 'Trezor returned a different address than the one requested', ); @@ -185,7 +192,10 @@ export async function signMessageByTypeFromTrezor( throw new Error('Trezor returned an error'); } - if (response.payload.address.toLowerCase() !== address.toLowerCase()) { + if ( + validateAndFormatAddress(response.payload.address) !== + validateAndFormatAddress(address) + ) { throw new Error( 'Trezor returned a different address than the one requested', ); diff --git a/src/entries/popup/hooks/send/useAllFilteredWallets.ts b/src/entries/popup/hooks/send/useAllFilteredWallets.ts index 7258a2abd9..2da09571fd 100644 --- a/src/entries/popup/hooks/send/useAllFilteredWallets.ts +++ b/src/entries/popup/hooks/send/useAllFilteredWallets.ts @@ -1,3 +1,4 @@ +import { validateAndFormatAddress } from '@orb-labs/orby-core'; import { useMemo } from 'react'; import { Address } from 'viem'; @@ -16,7 +17,7 @@ const filterWallets = ( return accounts.filter( ({ address, name, ensName, walletName }) => ensName?.toLowerCase().includes(filter) || - address?.toLowerCase().includes(filter) || + validateAndFormatAddress(address).includes(filter) || name?.toLowerCase().includes(filter) || walletName?.toLowerCase().includes(filter), ); diff --git a/src/entries/popup/hooks/useAppSession.ts b/src/entries/popup/hooks/useAppSession.ts index 3141933a2a..a7af70461e 100644 --- a/src/entries/popup/hooks/useAppSession.ts +++ b/src/entries/popup/hooks/useAppSession.ts @@ -1,4 +1,3 @@ -import { Account, AccountType, VMType } from '@orb-labs/orby-core'; import { removeConnectedAppSession } from '@orb-labs/orby-core-mini'; import { connectAppSession, useOrby } from '@orb-labs/orby-react'; import * as React from 'react'; @@ -24,7 +23,7 @@ export function useAppSession({ host = '' }: { host?: string }) { getActiveSession, } = useAppSessionsStore(); - const { baseMainnetClient } = useOrby(); + const { baseMainnetClient, accountCluster } = useOrby(); const activeSession = getActiveSession({ host }); const clearAppHasInteractedWithNudgeSheet = @@ -32,6 +31,10 @@ export function useAppSession({ host = '' }: { host?: string }) { const updateAppSessionAddress = React.useCallback( ({ address }: { address: Address }) => { + if (!accountCluster?.accountClusterId) { + return; + } + storeUpdateActiveSession({ host, address }); messenger.send(`accountsChanged:${host}`, address); messenger.send( @@ -39,16 +42,19 @@ export function useAppSession({ host = '' }: { host?: string }) { appSessions[host].sessions[address], ); - const account = new Account( - address?.toLowerCase(), - AccountType.EOA, - VMType.EVM, - undefined, + connectAppSession( + accountCluster?.accountClusterId, + host, + baseMainnetClient, ); - - connectAppSession([account], host, baseMainnetClient); }, - [appSessions, baseMainnetClient, host, storeUpdateActiveSession], + [ + appSessions, + baseMainnetClient, + host, + storeUpdateActiveSession, + accountCluster?.accountClusterId, + ], ); const addSession = React.useCallback( @@ -63,6 +69,10 @@ export function useAppSession({ host = '' }: { host?: string }) { chainId: number; url: string; }) => { + if (!accountCluster?.accountClusterId) { + return; + } + const sessions = storeAddSession({ host, address, chainId, url }); messenger.send(`accountsChanged:${host}`, address); if (Object.keys(sessions).length === 1) { @@ -72,16 +82,13 @@ export function useAppSession({ host = '' }: { host?: string }) { }); } - const account = new Account( - address?.toLowerCase(), - AccountType.EOA, - VMType.EVM, - undefined, + connectAppSession( + accountCluster?.accountClusterId, + host, + baseMainnetClient, ); - - connectAppSession([account], host, baseMainnetClient); }, - [baseMainnetClient, storeAddSession], + [accountCluster?.accountClusterId, storeAddSession, baseMainnetClient], ); const updateAppSessionChainId = React.useCallback( @@ -118,15 +125,12 @@ export function useAppSession({ host = '' }: { host?: string }) { const disconnectSession = React.useCallback( ({ address, host }: { address: Address; host: string }) => { const newActiveSession = removeSession({ host, address }); - if (newActiveSession) { - const account = new Account( - address?.toLowerCase(), - AccountType.EOA, - VMType.EVM, - undefined, + if (newActiveSession && accountCluster?.accountClusterId) { + connectAppSession( + accountCluster?.accountClusterId, + host, + baseMainnetClient, ); - - connectAppSession([account], host, baseMainnetClient); messenger.send(`accountsChanged:${host}`, newActiveSession?.address); messenger.send(`chainChanged:${host}`, newActiveSession?.chainId); } else { @@ -135,7 +139,12 @@ export function useAppSession({ host = '' }: { host?: string }) { clearAppHasInteractedWithNudgeSheet({ host: host }); } }, - [baseMainnetClient, clearAppHasInteractedWithNudgeSheet, removeSession], + [ + accountCluster?.accountClusterId, + baseMainnetClient, + clearAppHasInteractedWithNudgeSheet, + removeSession, + ], ); const disconnectAppSession = React.useCallback(() => { diff --git a/src/entries/popup/hooks/useInfiniteTransactionList.ts b/src/entries/popup/hooks/useInfiniteTransactionList.ts index 7f5ab798ba..4a324c048f 100644 --- a/src/entries/popup/hooks/useInfiniteTransactionList.ts +++ b/src/entries/popup/hooks/useInfiniteTransactionList.ts @@ -108,6 +108,10 @@ export const useInfiniteTransactionList = ({ currency: currency, }); + if (!transaction) { + return; + } + let description = ''; const formattedAddress = truncateAddress(transaction.to || '0x'); if (ac.category == Category.SEND) { diff --git a/src/entries/popup/hooks/useSearchCurrencyLists.ts b/src/entries/popup/hooks/useSearchCurrencyLists.ts index 515820f4db..3b7fc2d1d4 100644 --- a/src/entries/popup/hooks/useSearchCurrencyLists.ts +++ b/src/entries/popup/hooks/useSearchCurrencyLists.ts @@ -1,4 +1,5 @@ import { isAddress } from '@ethersproject/address'; +import { validateAndFormatAddress } from '@orb-labs/orby-core'; import { uniqBy } from 'lodash'; import { rankings } from 'match-sorter'; import { useCallback, useMemo } from 'react'; @@ -64,7 +65,7 @@ const filterBridgeAsset = ({ asset?: SearchAsset; filter?: string; }) => - asset?.address?.toLowerCase()?.startsWith(filter?.toLowerCase()) || + validateAndFormatAddress(asset?.address)?.startsWith(filter?.toLowerCase()) || asset?.name?.toLowerCase()?.startsWith(filter?.toLowerCase()) || asset?.symbol?.toLowerCase()?.startsWith(filter?.toLowerCase()); diff --git a/src/entries/popup/hooks/useUserAssetsBalance.ts b/src/entries/popup/hooks/useUserAssetsBalance.ts index 95ae1cba90..c519e85404 100644 --- a/src/entries/popup/hooks/useUserAssetsBalance.ts +++ b/src/entries/popup/hooks/useUserAssetsBalance.ts @@ -1,4 +1,4 @@ -import { usePortfolioOverview } from '@orb-labs/orby-react'; +import { useGetPortfolioOverview } from '@orb-labs/orby-react'; import { useCallback } from 'react'; import { Address } from 'viem'; @@ -83,15 +83,15 @@ export function useUserAssetsBalance(args?: { : undefined; const { testnetMode } = useTestnetModeStore(); - const { portfolioOverview } = usePortfolioOverview(testnetMode); + const { fungibleTokenOverview } = useGetPortfolioOverview(testnetMode); return { amount: totalAssetsBalance, - display: portfolioOverview + display: fungibleTokenOverview ? convertAmountToNativeDisplay( convertRawAmountToDecimalFormat( - portfolioOverview?.totalValueInFiat?.toRawAmount()?.toString(), - portfolioOverview?.totalValueInFiat?.currency.decimals, + fungibleTokenOverview?.totalValueInFiat?.toRawAmount()?.toString(), + fungibleTokenOverview?.totalValueInFiat?.currency.decimals, ), currentCurrency, ) diff --git a/src/entries/popup/hooks/useWalletsSummary.ts b/src/entries/popup/hooks/useWalletsSummary.ts index af550e7166..70a57baa90 100644 --- a/src/entries/popup/hooks/useWalletsSummary.ts +++ b/src/entries/popup/hooks/useWalletsSummary.ts @@ -1,3 +1,4 @@ +import { validateAndFormatAddress } from '@orb-labs/orby-core'; import { useMemo } from 'react'; import { Address } from 'viem'; @@ -43,7 +44,7 @@ const parseAddressSummary = ({ | undefined; }): WalletSummary => { const addressData = - addysSummary?.data.addresses[address.toLowerCase() as Address]; + addysSummary?.data.addresses[validateAndFormatAddress(address) as Address]; const summaryByChain = addressData?.summary_by_chain; const chainIds = Object.keys(summaryByChain || {}).map((id) => diff --git a/src/entries/popup/pages/messages/ApproveAppRequest.tsx b/src/entries/popup/pages/messages/ApproveAppRequest.tsx index a3616111e7..b8ac51ac3d 100644 --- a/src/entries/popup/pages/messages/ApproveAppRequest.tsx +++ b/src/entries/popup/pages/messages/ApproveAppRequest.tsx @@ -115,6 +115,8 @@ export const ApproveAppRequest = () => { [handleRequestAction, pendingRequest?.id], ); + console.log('[approveRequest] pendingRequest', pendingRequest); + switch (pendingRequest?.method) { case 'wallet_addEthereumChain': return ( @@ -155,6 +157,7 @@ export const ApproveAppRequest = () => { /> ); + case 'signTransaction': case 'personal_sign': case 'eth_signTypedData': case 'eth_signTypedData_v3': @@ -171,6 +174,7 @@ export const ApproveAppRequest = () => { /> ); + case 'signAndSendTransaction': case 'eth_sendTransaction': return ( void; + onlySwapSupportedNetworks?: boolean; + setSelectedChainId: (selectedChainId: ChainId) => void; }) => { const setCurrentAddress = useCurrentAddressStore.use.setCurrentAddress(); const { sortedAccounts } = useAccounts(); const { trackShortcut } = useKeyboardAnalytics(); const menuTriggerRef = useRef<{ triggerMenu: () => void }>(null); + const { chains: userChains } = useUserChains(); + const onOpenChange = useCallback((isOpen: boolean) => { isOpen && analytics.track(event.dappPromptConnectWalletClicked); }, []); const onValueChange = useCallback( (address: string) => { + const oldVM = getWalletVirtualEnvironment(selectedWallet); + const newVM = getWalletVirtualEnvironment(address); + + if (oldVM != newVM) { + const chains = userChains.filter((chain) => + onlySwapSupportedNetworks + ? supportedSwapChainIds.includes(chain.id) && + (!newVM || newVM == getVirtualEnvironment(BigInt(chain.id))) + : !newVM || newVM == getVirtualEnvironment(BigInt(chain.id)), + ); + + setSelectedChainId(chains[0].id); + } + setCurrentAddress(address as Address); setSelectedWallet(address as Address); analytics.track(event.dappPromptConnectWalletSwitched); }, - [setCurrentAddress, setSelectedWallet], + [ + selectedWallet, + setCurrentAddress, + setSelectedWallet, + userChains, + setSelectedChainId, + onlySwapSupportedNetworks, + ], ); useKeyboardShortcut({ @@ -283,9 +313,11 @@ export const BottomDisplayNetwork = ({ export const BottomSwitchNetwork = ({ selectedChainId, setSelectedChainId, + vmType, }: { selectedChainId: ChainId; setSelectedChainId: (selectedChainId: ChainId) => void; + vmType?: VMType; }) => { return ( @@ -306,6 +338,7 @@ export const BottomSwitchNetwork = ({ triggerComponent={ } + vmType={vmType} /> ); diff --git a/src/entries/popup/pages/messages/RequestAccounts/RequestAccountsActions.tsx b/src/entries/popup/pages/messages/RequestAccounts/RequestAccountsActions.tsx index 3ddf69f7e4..629596534e 100644 --- a/src/entries/popup/pages/messages/RequestAccounts/RequestAccountsActions.tsx +++ b/src/entries/popup/pages/messages/RequestAccounts/RequestAccountsActions.tsx @@ -1,3 +1,4 @@ +import { VMType } from '@orb-labs/orby-core'; import { Address } from 'viem'; import { DAppStatus } from '~/core/graphql/__generated__/metadata'; @@ -22,6 +23,7 @@ export const RequestAccountsActions = ({ appName, loading = false, dappStatus, + vmType, }: { appName?: string; selectedWallet: Address; @@ -32,6 +34,7 @@ export const RequestAccountsActions = ({ onRejectRequest: () => void; loading?: boolean; dappStatus?: DAppStatus; + vmType?: VMType; }) => { const isScamDapp = dappStatus === DAppStatus.Scam; return ( @@ -42,12 +45,14 @@ export const RequestAccountsActions = ({ diff --git a/src/entries/popup/pages/messages/RequestAccounts/index.tsx b/src/entries/popup/pages/messages/RequestAccounts/index.tsx index c78b6b359f..c7a11cd1fa 100644 --- a/src/entries/popup/pages/messages/RequestAccounts/index.tsx +++ b/src/entries/popup/pages/messages/RequestAccounts/index.tsx @@ -1,4 +1,5 @@ -import { useCallback, useState } from 'react'; +import { connectAppSession, useOrby } from '@orb-labs/orby-react'; +import { useCallback, useMemo, useState } from 'react'; import { Address } from 'viem'; import { analytics } from '~/analytics'; @@ -10,6 +11,7 @@ import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode'; import { ProviderRequestPayload } from '~/core/transports/providerRequestTransport'; import { ChainId } from '~/core/types/chains'; import { getDappHostname } from '~/core/utils/connectedApps'; +import { getWalletVirtualEnvironment } from '~/core/utils/orb'; import { Row, Rows, Separator } from '~/design-system'; import { RainbowError, logger } from '~/logger'; @@ -17,7 +19,7 @@ import { RequestAccountsActions } from './RequestAccountsActions'; import { RequestAccountsInfo } from './RequestAccountsInfo'; interface ApproveRequestProps { - approveRequest: (payload: { address: Address; chainId: number }) => void; + approveRequest: (payload: { address: string; chainId: number }) => void; rejectRequest: () => void; request: ProviderRequestPayload; } @@ -39,6 +41,8 @@ export const RequestAccounts = ({ ?.chainId; const addSession = useAppSessionsStore.use.addSession(); + const { baseMainnetClient, accountCluster } = useOrby(); + const { testnetMode } = useTestnetModeStore(); const [selectedChainId, setSelectedChainId] = useState( (requestedChainId ? Number(requestedChainId) : undefined) || @@ -46,9 +50,24 @@ export const RequestAccounts = ({ ); const [selectedWallet, setSelectedWallet] = useState
(currentAddress); - const onAcceptRequest = useCallback(() => { + const vmType = useMemo(() => { + return getWalletVirtualEnvironment(selectedWallet); + }, [selectedWallet]); + + const onAcceptRequest = useCallback(async () => { try { setLoading(true); + + if (!accountCluster?.accountClusterId) { + return; + } + + await connectAppSession( + accountCluster?.accountClusterId, + dappMetadata?.appHost || '', + baseMainnetClient, + ); + approveRequest({ address: selectedWallet, chainId: selectedChainId, @@ -76,13 +95,15 @@ export const RequestAccounts = ({ setLoading(false); } }, [ + accountCluster?.accountClusterId, + dappMetadata?.appHost, + dappMetadata?.appHostName, + dappMetadata?.appName, + baseMainnetClient, approveRequest, selectedWallet, selectedChainId, addSession, - dappMetadata?.appHost, - dappMetadata?.appHostName, - dappMetadata?.appName, dappUrl, ]); @@ -122,6 +143,7 @@ export const RequestAccounts = ({ appName={appName} loading={loading} dappStatus={dappMetadata?.status} + vmType={vmType} /> diff --git a/src/entries/popup/pages/messages/SendTransaction/SendTransactionsInfo.tsx b/src/entries/popup/pages/messages/SendTransaction/SendTransactionsInfo.tsx index 43c4c3e09a..20411e6341 100644 --- a/src/entries/popup/pages/messages/SendTransaction/SendTransactionsInfo.tsx +++ b/src/entries/popup/pages/messages/SendTransaction/SendTransactionsInfo.tsx @@ -46,7 +46,6 @@ import { MaliciousRequestWarning, getDappStatusBadge, } from '../DappScanStatus'; -import { SimulationOverview } from '../Simulation'; import { CopyButton, TabContent, Tabs } from '../Tabs'; import { SimulationError, @@ -104,9 +103,6 @@ const InfoRow = ({ const Overview = memo(function Overview({ chainId, - simulation, - status, - error, metadata, }: { chainId: ChainId; @@ -123,7 +119,7 @@ const Overview = memo(function Overview({ return ( - + {/* {i18n.t('simulation.title')} @@ -133,7 +129,7 @@ const Overview = memo(function Overview({ error={error} /> - + */} {chainId && chainName && ( { + return selectedGasToken?.standardizedTokenId + ? { standardizedTokenId: selectedGasToken.standardizedTokenId } + : undefined; + }, [selectedGasToken]); + + const { operationSet, virtualNode, aggregateFee, isLoading } = useGetOperationsToExecuteTransaction( - activeSession?.address?.toLowerCase(), + validateAndFormatAddress(activeSession?.address), activeSession?.chainId ? BigInt(activeSession.chainId) : undefined, txRequest.to as string, txRequest.data as string, txRequest.value ? BigInt(txRequest.value.toString()) : undefined, - selectedGasToken.standardizedTokenId - ? { standardizedTokenId: selectedGasToken.standardizedTokenId } - : undefined, + gasToken, ); const operationStatusesUpdated = useCallback( - ( + async ( statusSummary: OperationStatusType, finalTransactionStatus?: OperationStatus, + // eslint-disable-next-line @typescript-eslint/no-unused-vars statuses?: OperationStatus[], ) => { + if (statusSummary == OperationStatusType.WAITING_PRECONDITION) { + return; + } + if ( - operations && activeSession && - statuses && + [OperationStatusType.FAILED, OperationStatusType.NOT_FOUND].includes( + statusSummary, + ) + ) { + approveRequest(finalTransactionStatus?.hash); + setWaitingForDevice(false); + setLoading(false); + } else if ( + activeSession && [OperationStatusType.SUCCESSFUL, OperationStatusType.PENDING].includes( statusSummary, ) ) { - const activeChainId = chainIdToUse( - connectedToHardhat, - connectedToHardhatOp, - activeSession.chainId, - ); + const virtualNode = getCachedVirtualNode( + activeSession.address, + BigInt(activeSession.chainId), + ) as unknown as PublicClient; - analytics.track(event.dappPromptSendTransactionApproved, { - chainId: activeChainId, - dappURL: dappMetadata?.appHost || '', - dappName: dappMetadata?.appName, - }); + let transactionHash = finalTransactionStatus?.hash; + if (request.method != 'signAndSendTransaction') { + const receipt = await virtualNode?.waitForTransactionReceipt({ + hash: finalTransactionStatus?.hash as Hex, + }); + transactionHash = receipt?.transactionHash; + } - approveRequest(finalTransactionStatus?.hash); + approveRequest(transactionHash); setWaitingForDevice(false); setLoading(false); } }, - [ - activeSession, - approveRequest, - setWaitingForDevice, - connectedToHardhat, - connectedToHardhatOp, - dappMetadata?.appHost, - dappMetadata?.appName, - operations, - ], + [activeSession, approveRequest, getCachedVirtualNode, request.method], ); const onAcceptRequest = useCallback(async () => { @@ -167,9 +180,11 @@ export function SendTransaction({ const { operationResponses, success } = await virtualNode.sendOperationSet( - accountCluster.accountClusterId, + accountCluster, operationSet, signOperation, + undefined, + signOperation, ); if (!success) { @@ -182,6 +197,7 @@ export function SendTransaction({ const ids = operationResponses ?.map((op) => op.id) .filter((id) => !_.isUndefined(id)); + baseMainnetClient?.subscribeToOperationStatuses( ids, operationStatusesUpdated, diff --git a/src/entries/popup/pages/messages/SignMessage/SignMessageInfo.tsx b/src/entries/popup/pages/messages/SignMessage/SignMessageInfo.tsx index 6563b90a2a..5a81d99a3b 100644 --- a/src/entries/popup/pages/messages/SignMessage/SignMessageInfo.tsx +++ b/src/entries/popup/pages/messages/SignMessage/SignMessageInfo.tsx @@ -127,8 +127,6 @@ export const SignMessageInfo = ({ const tabLabel = (tab: string) => i18n.t(tab, { scope: 'simulation.tabs' }); - console.log('SignMessageInfo', operationSet); - return ( { + return selectedGasToken?.standardizedTokenId + ? { standardizedTokenId: selectedGasToken.standardizedTokenId } + : undefined; + }, [selectedGasToken]); const { operations, operationSet, virtualNode, isLoading, aggregateFee } = - useGetOperationsToSignTypedData( - request?.method == 'personal_sign' - ? '' - : JSON.stringify(requestPayload.msgData), + useGetOperationsToSignTransactionOrSignTypedData( + requestPayload.orbyCallData ?? '', + undefined, + undefined, activeSession?.address?.toLowerCase(), activeSession?.chainId ? BigInt(activeSession.chainId) : undefined, - selectedGasToken.standardizedTokenId - ? { standardizedTokenId: selectedGasToken.standardizedTokenId } - : undefined, + gasToken, ); const operationStatusesUpdated = useCallback( @@ -98,30 +105,49 @@ export function SignMessage({ // eslint-disable-next-line @typescript-eslint/no-unused-vars _statuses?: OperationStatus[], ) => { - if ( - requestPayload.address && + if (statusSummary == OperationStatusType.WAITING_PRECONDITION) { + return; + } else if ( + activeSession && + [OperationStatusType.FAILED, OperationStatusType.NOT_FOUND].includes( + statusSummary, + ) + ) { + approveRequest(undefined); + } else if ( + activeSession && [OperationStatusType.SUCCESSFUL, OperationStatusType.PENDING].includes( statusSummary, ) ) { - const result = await wallet.signTypedData( - requestPayload.msgData, - requestPayload.address, - ); + let result = null; + // this is for signing Solana Transaction + if (request.method == 'signTransaction') { + const txRpcUrl = getVirtualNodeRpcUrl( + activeSession?.address, + BigInt(activeSession?.chainId), + ); - analytics.track(event.dappPromptSignTypedDataApproved, { - dappURL: dappMetadata?.appHost || '', - dappName: dappMetadata?.appName, - }); + result = await signSVMTransaction( + txRpcUrl.virtualNodeRpcUrl, + requestPayload.msgData, + activeSession?.address, + ); + } else { + result = await wallet.signTypedData( + requestPayload.msgData, + activeSession?.address, + ); + } approveRequest(result); } }, [ + activeSession, approveRequest, - dappMetadata?.appHost, - dappMetadata?.appName, - requestPayload.address, + getVirtualNodeRpcUrl, + request.method, requestPayload.msgData, ], ); @@ -161,9 +187,11 @@ export function SignMessage({ const { success, operationResponses } = await virtualNode.sendOperationSet( - accountCluster.accountClusterId, + accountCluster, operationSet, signOperation, + undefined, + signOperation, ); if (!success) { @@ -173,7 +201,6 @@ export function SignMessage({ } if (operationResponses && operationResponses.length === 0) { - console.error('No operation responses'); operationStatusesUpdated(OperationStatusType.SUCCESSFUL); } else { const ids = operationResponses diff --git a/src/entries/popup/pages/send/index.tsx b/src/entries/popup/pages/send/index.tsx index b99ad0880b..c20bfa1c75 100644 --- a/src/entries/popup/pages/send/index.tsx +++ b/src/entries/popup/pages/send/index.tsx @@ -384,6 +384,12 @@ export function Send() { return txToAddress; }, [nft, txToAddress]); + const gasToken = useMemo(() => { + return selectedGasToken?.standardizedTokenId + ? { standardizedTokenId: selectedGasToken.standardizedTokenId } + : undefined; + }, [selectedGasToken]); + const { operations, operationSet, virtualNode, aggregateFee, isLoading } = useGetOperationsToExecuteTransaction( fromAddress as string, @@ -391,9 +397,7 @@ export function Send() { to, data as string, value ? BigInt(value.toString()) : undefined, - selectedGasToken.standardizedTokenId - ? { standardizedTokenId: selectedGasToken.standardizedTokenId } - : undefined, + gasToken, ); const { @@ -494,9 +498,11 @@ export function Send() { const { operationResponses, primaryOperationStatus } = await virtualNode.sendOperationSet( - accountCluster.accountClusterId, + accountCluster, operationSet, signOperation, + undefined, + signOperation, ); setOperationSetStatus({ diff --git a/src/entries/popup/pages/swap/SwapReviewSheet/SwapReviewSheet.tsx b/src/entries/popup/pages/swap/SwapReviewSheet/SwapReviewSheet.tsx index b3c07ded93..a1cc3077a5 100644 --- a/src/entries/popup/pages/swap/SwapReviewSheet/SwapReviewSheet.tsx +++ b/src/entries/popup/pages/swap/SwapReviewSheet/SwapReviewSheet.tsx @@ -320,11 +320,12 @@ const SwapReviewSheetWithQuote = ({ const operationStatusesUpdated = useCallback( async ( statusSummary: OperationStatusType, + // eslint-disable-next-line @typescript-eslint/no-unused-vars finalTransactionStatus?: OperationStatus, + // eslint-disable-next-line @typescript-eslint/no-unused-vars statuses?: OperationStatus[], ) => { if ( - statuses && !isSendingFinalTransaction && [OperationStatusType.SUCCESSFUL, OperationStatusType.PENDING].includes( statusSummary, @@ -379,9 +380,11 @@ const SwapReviewSheetWithQuote = ({ } const { operationResponses } = await virtualNode.sendOperationSet( - accountCluster.accountClusterId, + accountCluster, operationSet, signOperation, + undefined, + signOperation, ); const ids = operationResponses diff --git a/src/entries/popup/pages/swap/index.tsx b/src/entries/popup/pages/swap/index.tsx index 35df3cac86..3949e08ab7 100644 --- a/src/entries/popup/pages/swap/index.tsx +++ b/src/entries/popup/pages/swap/index.tsx @@ -593,6 +593,12 @@ export function Swap({ bridge = false }: { bridge?: boolean }) { fetchSwapTransaction(); }, [quoteData]); + const gasToken = useMemo(() => { + return selectedGasToken?.standardizedTokenId + ? { standardizedTokenId: selectedGasToken.standardizedTokenId } + : undefined; + }, [selectedGasToken]); + const { operationSet, virtualNode, aggregateFee, isLoading } = useGetOperationsToExecuteTransaction( swapTransaction?.from?.toLowerCase(), @@ -602,9 +608,7 @@ export function Swap({ bridge = false }: { bridge?: boolean }) { swapTransaction?.value ? BigInt(swapTransaction?.value.toString()) : undefined, - selectedGasToken.standardizedTokenId - ? { standardizedTokenId: selectedGasToken.standardizedTokenId } - : undefined, + gasToken, ); const { assetToSellNativeDisplay, assetToBuyNativeDisplay } = diff --git a/src/entries/popup/pages/swap/useSwapButton.tsx b/src/entries/popup/pages/swap/useSwapButton.tsx index fc513a21a1..4f5d293639 100644 --- a/src/entries/popup/pages/swap/useSwapButton.tsx +++ b/src/entries/popup/pages/swap/useSwapButton.tsx @@ -214,9 +214,11 @@ export const useSwapButton = ({ } const { operationResponses } = await virtualNode.sendOperationSet( - accountCluster.accountClusterId, + accountCluster, operationSet, signOperation, + undefined, + signOperation, ); const ids = operationResponses @@ -251,7 +253,11 @@ export const useSwapButton = ({ icon: ( - + @@ -350,7 +356,9 @@ export const useSwapButton = ({ buttonAction: () => showExplainerSheet({ show: true, - header: { icon: }, + header: { + icon: , + }, title: t('swap.explainers.fee_on_transfer_token.title'), description: [ @@ -402,14 +410,14 @@ export const useSwapButton = ({ icon: ( - + - + ), diff --git a/src/entries/popup/pages/unlock/index.tsx b/src/entries/popup/pages/unlock/index.tsx index 7b259dbbb7..79617e47be 100644 --- a/src/entries/popup/pages/unlock/index.tsx +++ b/src/entries/popup/pages/unlock/index.tsx @@ -185,7 +185,7 @@ export function Unlock() { {i18n.t('unlock.having_trouble')}
{i18n.t('unlock.contact')}  - 🌈 + 🔮
(null); const saveWalletName = useWalletNamesStore.use.saveWalletName(); const setCurrentAddress = useCurrentAddressStore.use.setCurrentAddress(); + const setCurrentAddresses = useCurrentAddressStore.use.setCurrentAddresses(); const [newWallet, setNewWallet] = useState(); useEffect(() => { @@ -60,6 +61,7 @@ export const CreateWalletPrompt = ({ const name = walletName.trim(); if (name) saveWalletName({ name, address }); setCurrentAddress(address); + setCurrentAddresses([address]); !fromChooseGroup ? navigate(ROUTES.HOME, { state: { isBack: true } }) : navigate( @@ -78,6 +80,7 @@ export const CreateWalletPrompt = ({ navigate, saveWalletName, setCurrentAddress, + setCurrentAddresses, fromChooseGroup, state?.password, walletName, diff --git a/src/entries/popup/pages/welcome/ImportOrCreateWallet.tsx b/src/entries/popup/pages/welcome/ImportOrCreateWallet.tsx index 183500afa0..8bf45d7522 100644 --- a/src/entries/popup/pages/welcome/ImportOrCreateWallet.tsx +++ b/src/entries/popup/pages/welcome/ImportOrCreateWallet.tsx @@ -48,6 +48,7 @@ export function ImportOrCreateWallet() { }, []); const setCurrentAddress = useCurrentAddressStore.use.setCurrentAddress(); + const setCurrentAddresses = useCurrentAddressStore.use.setCurrentAddresses(); const handleImportWalletClick = React.useCallback(async () => { const permissionsOk = await requestPermissionsIfNeeded(); @@ -62,6 +63,7 @@ export function ImportOrCreateWallet() { try { const newWalletAddress = await wallet.create(); setCurrentAddress(newWalletAddress); + setCurrentAddresses([newWalletAddress]); const seedPhrase = await wallet.exportWallet(newWalletAddress, ''); setImportWalletSecrets([seedPhrase]); navigate(ROUTES.SEED_BACKUP_PROMPT); @@ -71,7 +73,13 @@ export function ImportOrCreateWallet() { logger.error(new RainbowError(e?.name), { message: e?.message }); setLoading(false); } - }, [loading, navigate, requestPermissionsIfNeeded, setCurrentAddress]); + }, [ + loading, + navigate, + requestPermissionsIfNeeded, + setCurrentAddress, + setCurrentAddresses, + ]); return ( diff --git a/src/entries/popup/pages/welcome/index.tsx b/src/entries/popup/pages/welcome/index.tsx index e71746d959..4274028850 100644 --- a/src/entries/popup/pages/welcome/index.tsx +++ b/src/entries/popup/pages/welcome/index.tsx @@ -1,13 +1,13 @@ import { AnimatePresence, motion, useAnimationControls } from 'framer-motion'; import { useEffect, useState } from 'react'; +import rainbowOg from 'static/assets/rainbow/og-orblabs.png'; import { i18n } from '~/core/languages'; import { usePendingRequestStore } from '~/core/state'; import { useWalletBackupsStore } from '~/core/state/walletBackups'; import { Box, Stack, Text } from '~/design-system'; import { FlyingRainbows } from '../../components/FlyingRainbows/FlyingRainbows'; -import { LogoWithLetters } from '../../components/LogoWithLetters/LogoWithLetters'; import { ImportOrCreateWallet } from './ImportOrCreateWallet'; import { OnboardBeforeConnectSheet } from './OnboardBeforeConnectSheet'; @@ -38,7 +38,12 @@ export function Welcome() { > - + ) { + if (new.target === RainbowWalletAccount) { + Object.freeze(this); + } + + this.#address = address; + this.#publicKey = publicKey; + this.#chains = chains; + this.#features = features; + this.#label = label; + this.#icon = icon; + } +} diff --git a/src/entries/wallet-standard/src/icon.ts b/src/entries/wallet-standard/src/icon.ts new file mode 100644 index 0000000000..4721cbf0bf --- /dev/null +++ b/src/entries/wallet-standard/src/icon.ts @@ -0,0 +1,4 @@ +import type { WalletIcon } from '@wallet-standard/base'; + +export const icon: WalletIcon = + 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB3aWR0aD0iMTAwJSIgdmlld0JveD0iMCAwIDEyOCAxMjgiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDEyOCAxMjgiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8cGF0aCBmaWxsPSIjMDcwOTFGIiBvcGFjaXR5PSIxLjAwMDAwMCIgc3Ryb2tlPSJub25lIiAKCWQ9IgpNODcuMDAwMDAwLDEyOS4wMDAwMDAgCglDNTguMDAwMDAwLDEyOS4wMDAwMDAgMjkuNTAwMDAwLDEyOS4wMDAwMDAgMS4wMDAwMDAsMTI5LjAwMDAwMCAKCUMxLjAwMDAwMCw4Ni4zMzMzMzYgMS4wMDAwMDAsNDMuNjY2NjY4IDEuMDAwMDAwLDEuMDAwMDAwIAoJQzQzLjY2NjY2OCwxLjAwMDAwMCA4Ni4zMzMzMzYsMS4wMDAwMDAgMTI5LjAwMDAwMCwxLjAwMDAwMCAKCUMxMjkuMDAwMDAwLDQzLjY2NjY2OCAxMjkuMDAwMDAwLDg2LjMzMzMzNiAxMjkuMDAwMDAwLDEyOS4wMDAwMDAgCglDMTE1LjE2NjY2NCwxMjkuMDAwMDAwIDEwMS4zMzMzMzYsMTI5LjAwMDAwMCA4Ny4wMDAwMDAsMTI5LjAwMDAwMCAKTTc0LjAyMTExOCw3Ni42Mjc1MDIgCglDODEuNTI3NTQyLDY5LjQ4MjQ1MiA4MS42NTI2NTcsNTkuNTQ3MDg1IDc0LjMxMDI4MCw1My42NjQzNjQgCglDNjguNTg2MzI3LDQ5LjA3ODMzNSA2MC40NjE2MjAsNDkuNDM1MjA0IDU1LjE0MTI3MCw1NC41MDYzMjkgCglDNDkuODE5NDkyLDU5LjU3ODgxNSA0OS4wOTEzMTYsNjcuNzAwMTg4IDUzLjQyODgyNSw3My42MDUzNzcgCglDNTguMDk2OTA1LDc5Ljk2MDYwOSA2NS42NzkxNTMsODEuMjY4OTk3IDc0LjAyMTExOCw3Ni42Mjc1MDIgCk0zMi4yMzIxOTMsODQuNTE0NTcyIAoJQzQwLjg4MzE3NSwxMDAuMjYzNjQxIDYxLjU2MjY1MywxMDcuOTQ5NDcxIDc3LjIwOTc0NywxMDEuMjMxMTI1IAoJQzc1LjgzMjg0MCw5NC42Mjk1NTUgNzIuMzA4OTUyLDkxLjI1NzU0NSA2NS4yOTAzOTAsOTAuNzI2MDM2IAoJQzU4Ljc0NzQ1Miw5MC4yMzA1MzcgNTMuMDkwMzAyLDg4LjExMjI3NCA0Ny44Nzk0MTQsODQuMDc4NTE0IAoJQzQzLjA0NzExOSw4MC4zMzc4MzAgMzcuNzQ0MzYyLDgwLjI1NjI4NyAzMi4yMzIxOTMsODQuNTE0NTcyIApNODUuNzU1MzcxLDQ4LjEwMTkzNiAKCUM5MC4zOTIxNDMsNDkuNjUxNTM1IDk0Ljc1NzIxNyw0OS43MDgwMTIgOTguNTY5NTk1LDQ2LjA5NzM1MSAKCUM4OS4yOTc5NTgsMjkuNzg4Mjc3IDY3LjgzNTU5NCwyMS42ODIwNTggNTMuNjQ4NTcxLDI4Ljg5ODA3MSAKCUM1NS4yMzQzNDgsMzQuOTY2ODA1IDU4LjIzODU2NCwzOC42MjEzNTMgNjMuNzM5OTYwLDM5LjAyMzI5NiAKCUM3MS45MzI4MDAsMzkuNjIxODgzIDc4LjgzNzMxOCw0Mi42ODIxNzggODUuNzU1MzcxLDQ4LjEwMTkzNiAKTTMzLjU2NDE2Nyw0My4wODQ0OTkgCglDMjYuNTIwMjkyLDUzLjUyMDIyMiAyNS4wNjYyODgsNjQuNzM1MjQ1IDI4LjQ3NzQ5NSw3Ni43NDkxNjggCglDMzQuNTg1MzY1LDc1Ljc2OTU2MiAzOC41NjUxODIsNzIuNjkyMDQ3IDM5LjE0ODU3NSw2Ni45NjUxNjQgCglDMzkuOTUxMjE4LDU5LjA4NjEwMiA0Mi40NjA1MzcsNTIuMjg3NDIyIDQ3LjI3ODc3OCw0NS45NjE3OTYgCglDNTAuNjI4Mzg3LDQxLjU2NDI2NiA0OS41NTE4OTUsMzYuMDQwODk3IDQ1LjEwMjg0OCwzMS4zNDEzNDMgCglDNDEuMzg0MTE3LDM1LjA2NzY2NSAzNy42NzkyODMsMzguNzgwMDU2IDMzLjU2NDE2Nyw0My4wODQ0OTkgCk0xMDIuNDg0MDc3LDc0Ljg3NDI5OCAKCUMxMDQuMzQ5MzY1LDY3LjMxMDU0NyAxMDQuMzg5NjEwLDU5Ljg2NDMwMCAxMDEuNDIyNzkxLDUyLjUyNTAwOSAKCUM5NC42MjM0MDUsNTQuNTUyODMwIDkxLjI4NzIwMSw1Ny45NzcyNjggOTAuOTgyMzQ2LDYzLjk1MjAwNyAKCUM5MC42MzA0MDIsNzAuODQ5NTcxIDg4LjQ5ODEwMCw3Ni44Mzk0MTcgODQuMjczOTg3LDgyLjM1Mzk2NiAKCUM4MC4zNjUxNzMsODcuNDU2OTE3IDgwLjg3MDg4OCw5Mi44NTU2NTIgODUuMTY3NjQ4LDk4LjAwOTg0MiAKCUM5My42NTkzMzIsOTIuNjg3ODY2IDk5LjQxOTEyOCw4NS4zMjYxOTUgMTAyLjQ4NDA3Nyw3NC44NzQyOTggCnoiLz4KPHBhdGggZmlsbD0iI0Y3RjdGNyIgb3BhY2l0eT0iMS4wMDAwMDAiIHN0cm9rZT0ibm9uZSIgCglkPSIKTTczLjcxNDI2NCw3Ni44NDAxNzkgCglDNjUuNjc5MTUzLDgxLjI2ODk5NyA1OC4wOTY5MDUsNzkuOTYwNjA5IDUzLjQyODgyNSw3My42MDUzNzcgCglDNDkuMDkxMzE2LDY3LjcwMDE4OCA0OS44MTk0OTIsNTkuNTc4ODE1IDU1LjE0MTI3MCw1NC41MDYzMjkgCglDNjAuNDYxNjIwLDQ5LjQzNTIwNCA2OC41ODYzMjcsNDkuMDc4MzM1IDc0LjMxMDI4MCw1My42NjQzNjQgCglDODEuNjUyNjU3LDU5LjU0NzA4NSA4MS41Mjc1NDIsNjkuNDgyNDUyIDczLjcxNDI2NCw3Ni44NDAxNzkgCnoiLz4KPHBhdGggZmlsbD0iI0YzRjNGMyIgb3BhY2l0eT0iMS4wMDAwMDAiIHN0cm9rZT0ibm9uZSIgCglkPSIKTTMyLjQyMDYwMSw4NC4yMDQ5MTAgCglDMzcuNzQ0MzYyLDgwLjI1NjI4NyA0My4wNDcxMTksODAuMzM3ODMwIDQ3Ljg3OTQxNCw4NC4wNzg1MTQgCglDNTMuMDkwMzAyLDg4LjExMjI3NCA1OC43NDc0NTIsOTAuMjMwNTM3IDY1LjI5MDM5MCw5MC43MjYwMzYgCglDNzIuMzA4OTUyLDkxLjI1NzU0NSA3NS44MzI4NDAsOTQuNjI5NTU1IDc3LjIwOTc0NywxMDEuMjMxMTI1IAoJQzYxLjU2MjY1MywxMDcuOTQ5NDcxIDQwLjg4MzE3NSwxMDAuMjYzNjQxIDMyLjQyMDYwMSw4NC4yMDQ5MTAgCnoiLz4KPHBhdGggZmlsbD0iI0YzRjNGNCIgb3BhY2l0eT0iMS4wMDAwMDAiIHN0cm9rZT0ibm9uZSIgCglkPSIKTTg1LjM5Nzc5Nyw0Ny45NDE5NDAgCglDNzguODM3MzE4LDQyLjY4MjE3OCA3MS45MzI4MDAsMzkuNjIxODgzIDYzLjczOTk2MCwzOS4wMjMyOTYgCglDNTguMjM4NTY0LDM4LjYyMTM1MyA1NS4yMzQzNDgsMzQuOTY2ODA1IDUzLjY0ODU3MSwyOC44OTgwNzEgCglDNjcuODM1NTk0LDIxLjY4MjA1OCA4OS4yOTc5NTgsMjkuNzg4Mjc3IDk4LjU2OTU5NSw0Ni4wOTczNTEgCglDOTQuNzU3MjE3LDQ5LjcwODAxMiA5MC4zOTIxNDMsNDkuNjUxNTM1IDg1LjM5Nzc5Nyw0Ny45NDE5NDAgCnoiLz4KPHBhdGggZmlsbD0iI0YzRjNGNCIgb3BhY2l0eT0iMS4wMDAwMDAiIHN0cm9rZT0ibm9uZSIgCglkPSIKTTMzLjc2OTMxMCw0Mi43ODg0NzUgCglDMzcuNjc5MjgzLDM4Ljc4MDA1NiA0MS4zODQxMTcsMzUuMDY3NjY1IDQ1LjEwMjg0OCwzMS4zNDEzNDMgCglDNDkuNTUxODk1LDM2LjA0MDg5NyA1MC42MjgzODcsNDEuNTY0MjY2IDQ3LjI3ODc3OCw0NS45NjE3OTYgCglDNDIuNDYwNTM3LDUyLjI4NzQyMiAzOS45NTEyMTgsNTkuMDg2MTAyIDM5LjE0ODU3NSw2Ni45NjUxNjQgCglDMzguNTY1MTgyLDcyLjY5MjA0NyAzNC41ODUzNjUsNzUuNzY5NTYyIDI4LjQ3NzQ5NSw3Ni43NDkxNjggCglDMjUuMDY2Mjg4LDY0LjczNTI0NSAyNi41MjAyOTIsNTMuNTIwMjIyIDMzLjc2OTMxMCw0Mi43ODg0NzUgCnoiLz4KPHBhdGggZmlsbD0iI0YzRjNGMyIgb3BhY2l0eT0iMS4wMDAwMDAiIHN0cm9rZT0ibm9uZSIgCglkPSIKTTEwMi4zNDcwOTksNzUuMjU4NTk4IAoJQzk5LjQxOTEyOCw4NS4zMjYxOTUgOTMuNjU5MzMyLDkyLjY4Nzg2NiA4NS4xNjc2NDgsOTguMDA5ODQyIAoJQzgwLjg3MDg4OCw5Mi44NTU2NTIgODAuMzY1MTczLDg3LjQ1NjkxNyA4NC4yNzM5ODcsODIuMzUzOTY2IAoJQzg4LjQ5ODEwMCw3Ni44Mzk0MTcgOTAuNjMwNDAyLDcwLjg0OTU3MSA5MC45ODIzNDYsNjMuOTUyMDA3IAoJQzkxLjI4NzIwMSw1Ny45NzcyNjggOTQuNjIzNDA1LDU0LjU1MjgzMCAxMDEuNDIyNzkxLDUyLjUyNTAwOSAKCUMxMDQuMzg5NjEwLDU5Ljg2NDMwMCAxMDQuMzQ5MzY1LDY3LjMxMDU0NyAxMDIuMzQ3MDk5LDc1LjI1ODU5OCAKeiIvPgo8L3N2Zz4=' as const; diff --git a/src/entries/wallet-standard/src/index.ts b/src/entries/wallet-standard/src/index.ts new file mode 100644 index 0000000000..d1d16464cb --- /dev/null +++ b/src/entries/wallet-standard/src/index.ts @@ -0,0 +1,2 @@ +export * from './initialize'; +export { RainbowWalletAccount } from './account'; diff --git a/src/entries/wallet-standard/src/initialize.ts b/src/entries/wallet-standard/src/initialize.ts new file mode 100644 index 0000000000..bc4f58965b --- /dev/null +++ b/src/entries/wallet-standard/src/initialize.ts @@ -0,0 +1,7 @@ +import { registerWallet } from './register'; +import { RainbowWallet } from './wallet'; +import type { SolanaProvider } from './window'; + +export function initialize(solana: SolanaProvider): void { + registerWallet(new RainbowWallet(solana)); +} diff --git a/src/entries/wallet-standard/src/register.ts b/src/entries/wallet-standard/src/register.ts new file mode 100644 index 0000000000..89c1183d37 --- /dev/null +++ b/src/entries/wallet-standard/src/register.ts @@ -0,0 +1,83 @@ +// This is copied from @wallet-standard/wallet + +import type { + DEPRECATED_WalletsWindow, + Wallet, + WalletEventsWindow, + WindowRegisterWalletEvent, + WindowRegisterWalletEventCallback, +} from '@wallet-standard/base'; + +export function registerWallet(wallet: Wallet): void { + const callback: WindowRegisterWalletEventCallback = ({ register }) => + register(wallet); + try { + (window as WalletEventsWindow).dispatchEvent( + new RegisterWalletEvent(callback), + ); + } catch (error) { + console.error( + 'wallet-standard:register-wallet event could not be dispatched\n', + error, + ); + } + try { + (window as WalletEventsWindow).addEventListener( + 'wallet-standard:app-ready', + ({ detail: api }) => callback(api), + ); + } catch (error) { + console.error( + 'wallet-standard:app-ready event listener could not be added\n', + error, + ); + } +} + +class RegisterWalletEvent extends Event implements WindowRegisterWalletEvent { + readonly #detail: WindowRegisterWalletEventCallback; + + get detail() { + return this.#detail; + } + + get type() { + return 'wallet-standard:register-wallet' as const; + } + + constructor(callback: WindowRegisterWalletEventCallback) { + super('wallet-standard:register-wallet', { + bubbles: false, + cancelable: false, + composed: false, + }); + this.#detail = callback; + } + + /** @deprecated */ + preventDefault(): never { + throw new Error('preventDefault cannot be called'); + } + + /** @deprecated */ + stopImmediatePropagation(): never { + throw new Error('stopImmediatePropagation cannot be called'); + } + + /** @deprecated */ + stopPropagation(): never { + throw new Error('stopPropagation cannot be called'); + } +} + +/** @deprecated */ +export function DEPRECATED_registerWallet(wallet: Wallet): void { + registerWallet(wallet); + try { + ((window as DEPRECATED_WalletsWindow).navigator.wallets ||= []).push( + ({ register }) => register(wallet), + ); + } catch (error) { + console.error('window.navigator.wallets could not be pushed\n', error); + } +} diff --git a/src/entries/wallet-standard/src/solana.ts b/src/entries/wallet-standard/src/solana.ts new file mode 100644 index 0000000000..e1bada7bb9 --- /dev/null +++ b/src/entries/wallet-standard/src/solana.ts @@ -0,0 +1,40 @@ +// This is copied from @solana/wallet-standard-chains + +import type { Transaction, VersionedTransaction } from '@solana/web3.js'; +import type { IdentifierString } from '@wallet-standard/base'; + +/** Solana Mainnet (beta) cluster, e.g. https://api.mainnet-beta.solana.com */ +export const SOLANA_MAINNET_CHAIN = 'solana:mainnet'; + +/** Solana Devnet cluster, e.g. https://api.devnet.solana.com */ +export const SOLANA_DEVNET_CHAIN = 'solana:devnet'; + +/** Solana Testnet cluster, e.g. https://api.testnet.solana.com */ +export const SOLANA_TESTNET_CHAIN = 'solana:testnet'; + +/** Solana Localnet cluster, e.g. http://localhost:8899 */ +export const SOLANA_LOCALNET_CHAIN = 'solana:localnet'; + +/** Array of all Solana clusters */ +export const SOLANA_CHAINS = [ + SOLANA_MAINNET_CHAIN, + // SOLANA_DEVNET_CHAIN, + // SOLANA_TESTNET_CHAIN, + // SOLANA_LOCALNET_CHAIN, +] as const; + +/** Type of all Solana clusters */ +export type SolanaChain = (typeof SOLANA_CHAINS)[number]; + +/** + * Check if a chain corresponds with one of the Solana clusters. + */ +export function isSolanaChain(chain: IdentifierString): chain is SolanaChain { + return SOLANA_CHAINS.includes(chain as SolanaChain); +} + +export function isVersionedTransaction( + transaction: Transaction | VersionedTransaction, +): transaction is VersionedTransaction { + return 'version' in transaction; +} diff --git a/src/entries/wallet-standard/src/util.ts b/src/entries/wallet-standard/src/util.ts new file mode 100644 index 0000000000..efa84cbab8 --- /dev/null +++ b/src/entries/wallet-standard/src/util.ts @@ -0,0 +1,23 @@ +// This is copied from @wallet-standard/wallet + +export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean { + return arraysEqual(a, b); +} + +interface Indexed { + length: number; + [index: number]: T; +} + +export function arraysEqual(a: Indexed, b: Indexed): boolean { + if (a === b) return true; + + const length = a.length; + if (length !== b.length) return false; + + for (let i = 0; i < length; i++) { + if (a[i] !== b[i]) return false; + } + + return true; +} diff --git a/src/entries/wallet-standard/src/wallet.ts b/src/entries/wallet-standard/src/wallet.ts new file mode 100644 index 0000000000..49dccd50fb --- /dev/null +++ b/src/entries/wallet-standard/src/wallet.ts @@ -0,0 +1,351 @@ +import { + SolanaSignAndSendTransaction, + type SolanaSignAndSendTransactionFeature, + type SolanaSignAndSendTransactionMethod, + type SolanaSignAndSendTransactionOutput, + SolanaSignIn, + type SolanaSignInFeature, + type SolanaSignInMethod, + type SolanaSignInOutput, + SolanaSignMessage, + type SolanaSignMessageFeature, + type SolanaSignMessageMethod, + type SolanaSignMessageOutput, + SolanaSignTransaction, + type SolanaSignTransactionFeature, + type SolanaSignTransactionMethod, + type SolanaSignTransactionOutput, +} from '@solana/wallet-standard-features'; +import { Transaction, VersionedTransaction } from '@solana/web3.js'; +import type { Wallet } from '@wallet-standard/base'; +import { + StandardConnect, + type StandardConnectFeature, + type StandardConnectMethod, + StandardDisconnect, + type StandardDisconnectFeature, + type StandardDisconnectMethod, + StandardEvents, + type StandardEventsFeature, + type StandardEventsListeners, + type StandardEventsNames, + type StandardEventsOnMethod, +} from '@wallet-standard/features'; +import bs58 from 'bs58'; + +import { RainbowWalletAccount } from './account'; +import { icon } from './icon'; +import { + SOLANA_CHAINS, + type SolanaChain, + isSolanaChain, + isVersionedTransaction, +} from './solana'; +import { bytesEqual } from './util'; +import type { SolanaProvider } from './window'; + +export const RainbowNamespace = 'rainbow:'; + +export type RainbowFeature = { + [RainbowNamespace]: { + rainbow: SolanaProvider; + }; +}; + +export class RainbowWallet implements Wallet { + readonly #listeners: { + [E in StandardEventsNames]?: StandardEventsListeners[E][]; + } = {}; + readonly #version = '1.0.0' as const; + readonly #name = 'OrbyPlayground' as const; + readonly #icon = icon; + #account: RainbowWalletAccount | null = null; + readonly #rainbow: SolanaProvider; + + get version() { + return this.#version; + } + + get name() { + return this.#name; + } + + get icon() { + return this.#icon; + } + + get chains() { + return SOLANA_CHAINS.slice(); + } + + get features(): StandardConnectFeature & + StandardDisconnectFeature & + StandardEventsFeature & + SolanaSignAndSendTransactionFeature & + SolanaSignTransactionFeature & + SolanaSignMessageFeature & + SolanaSignInFeature & + RainbowFeature { + return { + [StandardConnect]: { + version: '1.0.0', + connect: this.#connect, + }, + [StandardDisconnect]: { + version: '1.0.0', + disconnect: this.#disconnect, + }, + [StandardEvents]: { + version: '1.0.0', + on: this.#on, + }, + [SolanaSignAndSendTransaction]: { + version: '1.0.0', + supportedTransactionVersions: ['legacy', 0], + signAndSendTransaction: this.#signAndSendTransaction, + }, + [SolanaSignTransaction]: { + version: '1.0.0', + supportedTransactionVersions: ['legacy', 0], + signTransaction: this.#signTransaction, + }, + [SolanaSignMessage]: { + version: '1.0.0', + signMessage: this.#signMessage, + }, + [SolanaSignIn]: { + version: '1.0.0', + signIn: this.#signIn, + }, + [RainbowNamespace]: { + rainbow: this.#rainbow, + }, + }; + } + + get accounts() { + return this.#account ? [this.#account] : []; + } + + constructor(rainbow: SolanaProvider) { + if (new.target === RainbowWallet) { + Object.freeze(this); + } + + this.#rainbow = rainbow; + + rainbow.on('connect', this.#connected, this); + rainbow.on('disconnect', this.#disconnected, this); + rainbow.on('accountChanged', this.#reconnected, this); + + this.#connected(); + } + + #on: StandardEventsOnMethod = (event, listener) => { + this.#listeners[event]?.push(listener) || + (this.#listeners[event] = [listener]); + return (): void => this.#off(event, listener); + }; + + #emit( + event: E, + ...args: Parameters + ): void { + // eslint-disable-next-line prefer-spread + this.#listeners[event]?.forEach((listener) => listener.apply(null, args)); + } + + #off( + event: E, + listener: StandardEventsListeners[E], + ): void { + this.#listeners[event] = this.#listeners[event]?.filter( + (existingListener) => listener !== existingListener, + ); + } + + #connected = () => { + const address = this.#rainbow.publicKey?.toBase58(); + if (address) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const publicKey = this.#rainbow.publicKey!.toBytes(); + + const account = this.#account; + if ( + !account || + account.address !== address || + !bytesEqual(account.publicKey, publicKey) + ) { + this.#account = new RainbowWalletAccount({ address, publicKey }); + this.#emit('change', { accounts: this.accounts }); + } + } + }; + + #disconnected = () => { + if (this.#account) { + this.#account = null; + this.#emit('change', { accounts: this.accounts }); + } + }; + + #reconnected = () => { + if (this.#rainbow.publicKey) { + this.#connected(); + } else { + this.#disconnected(); + } + }; + + #connect: StandardConnectMethod = async ({ silent } = {}) => { + if (!this.#account) { + await this.#rainbow.connect(silent ? { onlyIfTrusted: true } : undefined); + } + + this.#connected(); + + return { accounts: this.accounts }; + }; + + #disconnect: StandardDisconnectMethod = async () => { + await this.#rainbow.disconnect(); + }; + + #signAndSendTransaction: SolanaSignAndSendTransactionMethod = async ( + ...inputs + ) => { + if (!this.#account) throw new Error('not connected'); + + const outputs: SolanaSignAndSendTransactionOutput[] = []; + + if (inputs.length === 1) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const { transaction, account, chain, options } = inputs[0]!; + const { minContextSlot, preflightCommitment, skipPreflight, maxRetries } = + options || {}; + if (account !== this.#account) throw new Error('invalid account'); + if (!isSolanaChain(chain)) throw new Error('invalid chain'); + + const { signature } = await this.#rainbow.signAndSendTransaction( + VersionedTransaction.deserialize(transaction), + { + preflightCommitment, + minContextSlot, + maxRetries, + skipPreflight, + }, + ); + + outputs.push({ signature: bs58.decode(signature) }); + } else if (inputs.length > 1) { + for (const input of inputs) { + outputs.push(...(await this.#signAndSendTransaction(input))); + } + } + + return outputs; + }; + + #signTransaction: SolanaSignTransactionMethod = async (...inputs) => { + if (!this.#account) throw new Error('not connected'); + + const outputs: SolanaSignTransactionOutput[] = []; + + if (inputs.length === 1) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const { transaction, account, chain } = inputs[0]!; + if (account !== this.#account) throw new Error('invalid account'); + if (chain && !isSolanaChain(chain)) throw new Error('invalid chain'); + + const signedTransaction = await this.#rainbow.signTransaction( + VersionedTransaction.deserialize(transaction), + ); + + const serializedTransaction = isVersionedTransaction(signedTransaction) + ? signedTransaction.serialize() + : new Uint8Array( + (signedTransaction as Transaction).serialize({ + requireAllSignatures: false, + verifySignatures: false, + }), + ); + + outputs.push({ signedTransaction: serializedTransaction }); + } else if (inputs.length > 1) { + let chain: SolanaChain | undefined = undefined; + for (const input of inputs) { + if (input.account !== this.#account) throw new Error('invalid account'); + if (input.chain) { + if (!isSolanaChain(input.chain)) throw new Error('invalid chain'); + if (chain) { + if (input.chain !== chain) throw new Error('conflicting chain'); + } else { + chain = input.chain; + } + } + } + + const transactions = inputs.map(({ transaction }) => + VersionedTransaction.deserialize(transaction), + ); + + const signedTransactions = + await this.#rainbow.signAllTransactions(transactions); + + outputs.push( + ...signedTransactions.map((signedTransaction) => { + const serializedTransaction = isVersionedTransaction( + signedTransaction, + ) + ? signedTransaction.serialize() + : new Uint8Array( + (signedTransaction as Transaction).serialize({ + requireAllSignatures: false, + verifySignatures: false, + }), + ); + + return { signedTransaction: serializedTransaction }; + }), + ); + } + + return outputs; + }; + + #signMessage: SolanaSignMessageMethod = async (...inputs) => { + if (!this.#account) throw new Error('not connected'); + + const outputs: SolanaSignMessageOutput[] = []; + + if (inputs.length === 1) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const { message, account } = inputs[0]!; + if (account !== this.#account) throw new Error('invalid account'); + + const { signature } = await this.#rainbow.signMessage(message); + + outputs.push({ signedMessage: message, signature }); + } else if (inputs.length > 1) { + for (const input of inputs) { + outputs.push(...(await this.#signMessage(input))); + } + } + + return outputs; + }; + + #signIn: SolanaSignInMethod = async (...inputs) => { + const outputs: SolanaSignInOutput[] = []; + + if (inputs.length > 1) { + for (const input of inputs) { + outputs.push(await this.#rainbow.signIn(input)); + } + } else { + return [await this.#rainbow.signIn(inputs[0])]; + } + + return outputs; + }; +} diff --git a/src/entries/wallet-standard/src/window.ts b/src/entries/wallet-standard/src/window.ts new file mode 100644 index 0000000000..d99c195b36 --- /dev/null +++ b/src/entries/wallet-standard/src/window.ts @@ -0,0 +1,50 @@ +import { + type SolanaSignInInput, + type SolanaSignInOutput, +} from '@solana/wallet-standard-features'; +import type { + PublicKey, + SendOptions, + Transaction, + TransactionSignature, + VersionedTransaction, +} from '@solana/web3.js'; + +export interface RainbowEvent { + connect(...args: unknown[]): unknown; + disconnect(...args: unknown[]): unknown; + accountChanged(...args: unknown[]): unknown; +} + +export interface RainbowEventEmitter { + on( + event: E, + listener: RainbowEvent[E], + context?: any, + ): void; + off( + event: E, + listener: RainbowEvent[E], + context?: any, + ): void; +} + +export interface SolanaProvider extends RainbowEventEmitter { + publicKey?: PublicKey | null; + connect(options?: { + onlyIfTrusted?: boolean; + }): Promise<{ publicKey?: PublicKey }>; + disconnect(): Promise; + signAndSendTransaction( + transaction: T, + options?: SendOptions, + ): Promise<{ signature: TransactionSignature }>; + signTransaction( + transaction: T, + ): Promise; + signAllTransactions( + transactions: T[], + ): Promise; + signMessage(message: Uint8Array): Promise<{ signature: Uint8Array }>; + signIn(input?: SolanaSignInInput): Promise; +} diff --git a/static/allowlist.json b/static/allowlist.json index 565ba8e951..e94f63e3d4 100644 --- a/static/allowlist.json +++ b/static/allowlist.json @@ -6,6 +6,9 @@ "https://api-rpc-dev.orblabs.xyz", "https://addys.p.rainbow.me", "https://*.core.chainstack.com", + "https://cryptologos.cc", + "https://api/*", + "https://assets.coingecko.com", "ws://localhost:9090", "http://127.0.0.1:*", "https://*.g.alchemy.com", diff --git a/static/assets/rainbow/og-orblabs.png b/static/assets/rainbow/og-orblabs.png new file mode 100644 index 0000000000000000000000000000000000000000..c75dcaf53dc045d6efc2757fba23808e0cdf633a GIT binary patch literal 4534 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 literal 0 HcmV?d00001 diff --git a/static/assets/rainbow/og-orby.png b/static/assets/rainbow/og-orby.png new file mode 100644 index 0000000000000000000000000000000000000000..02c61565b607bab0e18229ea497d44aad48fe3e7 GIT binary patch literal 8282 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 P0tm{A8VWUXmSO(`4x<gsB2 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

a1ko6Zilr_+q_r!(I*z!A4n*>h)2@J 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 literal 0 HcmV?d00001 diff --git a/static/images/icon-16.png b/static/images/icon-16.png index e94b73398fc51780233680f16ad89ca7e6d57f9a..9a331736d9cf90ae223c35ff9ce1864f664b4bd4 100644 GIT binary patch delta 605 zcmV-j0;2t|2KEGyB!4|gL_t(|oTZb`OH@%D#m~KW-n;LmqoIK=;yEed*r*f{jQx-T zX{EF))QxR4qgMR|wX0<<-Sr2Afmt~gh?R~u8Yl%KWd<#B5mL@Pr}yq@q2p){6m(XX zd%yR5&;9WcmiCDMP^8qP5TY;w04OD33}KA4Wv@Y`|G%Jxf64l6uzu}#rqFS zsIIEQ%^Uq#T%1JT)n1q|2O)QvSlXj@W0b0v^$n+(%VxQ>v`pY20gFJC&8_E{o|)(J z@<*1JAEAmIrho0!(b67e;oK(7;rd`79M?s6_chcvcu1wjF_s#EpYxGSo<~pb0J<(+ z1R3m40912odY0eT)(Av7`t$`0no7T=5DteQkMi^CDuF|^omer!mWaxVDCXxEAjBrL zwsuy+Um-|^)|Mvx{ILND34p}}wo^wHsbObV597zq5Pzs(UF#&jI>Xu7cRX?O0)Z$2hk(oUOom}-m}ogmA#14J zSka(x50+iap5A_jK|qpR`Lx0(qhpLWCHd<0GzSK55vbhRY-x{THw!`t}cecYb`twxN{V$HP7r!Z3h# r9Z2bJ@v!Ta{s|@kFvg%nZAtwGVGbAKi0wz%00000NkvXXu0mjf?)D>9 delta 792 zcmV+z1Lyqq1g{2=B!BivL_t(|0b*nT0x>s0599!A>#;}i-+$)g;l{O&@m_7)M(o_i zWz4#<*`QWj+qP{Sn;RczrtXSXuz1g|G^?E6CHeOjk|5} z<^7MzwlnU$REd7Hw1(xP4dRG#)RZC+kIX@iL;Y*z9O8u_8h>Yt%XQDdg%7n&xwB51 zpVnE`|MNQN#b?4g7R!VfON3Bh5iAaeEwfxiX+%c6xM=@x&RX{CGB~@KCe;zm&X^_L z!}n>0=bCFwl@0djfW$cj00~UdLPY(~3v3op_>6ZiaLl&bNs@h}YunjK`uJb(|2S~V zATMmjJl&jSmw)z29DqYau#V7EI@f#-z1JD)XIN-WVphK=oq+A13{YP?g)Q|{smN{~ zTK|n*UO?0R`w7Y`rG%zpV`s+7c!th}?!1f6|E0uZ3v|8l5aE}ru=R4*i=8~(m|%w& z(XtjLpv~SBZ%!u*W>4+l)&8S-FCAvp_o-D4mC6dymVbb#@f~)em#F$bzG@`2iw2%3 z3Tx16$B8%lODz`qcktsMRX@xQ2Ti4h~oFn>oqXy{vj;%x8x0k|lbSMj2BY zTjn@QsCdZX71Ve0EWPy=Bzt)$9ikC5QB_4;o1?qTsD}~cIhfLV&YC}2j1?%s8H1|^ z*qKFiA}o44F#*s8QM5s{o}ombC{TgTw5lC(xqpS7&K9|IXlD&X3f5Yr#X22JYs5cp z=f}^-_~eUzK3Fllxw)LLiq;@s3PV?1hLcCc8E_>4h~TV4CR#XRAOsw21;Pxpk9a<= zbeC(^KK+iK|9=5`1ZOKat8q?5!0n9icR2cX6l|>!a0xQ_ zMj_Z>@9fUZ*N0tm^U`(u5~W94?X31c=X~>>?_9=ssYdZRnt%N~gMX0$ms+vKzA z{x88D>wy&_!RiixBnBx0BanlPN`jd*0AmcsK$65{#zuf#;UAG_!Nb~eQYoJ#i4moP zPa1%=mMDsdqZrS3S+jNpOBa8QnKSD#)-o_K$eA-|Iez>E1N~QUoGN@jMU+IPAM8Ju zp>Q0R%;*TU(|>EJuBqkY?!WPuV+Z+W`{D_wuk`it>Z`A_{b#$ldi5GXx{^4K3Z9Cq z1uxa82YpgyErM!&Y5qce`1gk>(_6vdd-u3aH^A_tePl^C(nQ%SbvD)xa{2d3M0cea6OUa!a`PK;wVS=M?E~d<%ewg!H@ayAKeTM{fnWYYgAR$ z5Jg#xDc*7>q`=@1$55V*Idl5YG`BQit>vvZJ9y&T-y;<~L^w9gW9!$jZry6E>vH(W zQGWNvJ2*~?Y&b$$c{#PU)m*xGfvMGXB(mHZ34bxxFrhT>rzA+v5!7has_#gWk|d`v z4As`QTu>+L)M)$9f1&&BSHFHuo?ol7bgNRSRzamUKC@N*1B2SIajT$GLAqr^3MK$3 zD{m20>ctm-dfWM_&eQT!GX>MNdd+w9nvZkJWwXjeNzyZ$w&gY9G^%a!a)sHP zB!6l9%R37IT1#s{1&ZqHr{{l5e0s5$Y?uKe)~;QJB$8OTj)&`d7-LD2gmr6I0uaYH zS+{ltzULx|hWZ)g$IirpE2512L1PR_l3w_qK@tN4gX0KG0T3b!xSa0%E9cIA%!ZB6a^k&Cd_PTDc?AbL4)gMkT{w=9>v_1Y zi{}OG`OO>b+xvTr^_bn*KzsWVk|f3$Ls!?C!oy8WL6J@fHkvEw=tAWf|hM+7U6J|N} zekT`tdKetK%G+-r;N9aVv7XQIWAE_wC2f3mZJ2dWJk5K@PZFdbB93zu%73PG3T}_% zbhBWp9(kl)U0rAI4Bj}phm}kw)aK1U5*TfNX}3PTbXiN6E*DfORiyh(ih;Zw6CBqi zyfsSYlz^YS@ElvVexIhs*%Qu=-Wp}joCQRQC0*{*d#RToJ?-{F@*pd*5#?c6>k;Q7 zqUbYfYG<%)*%I0oE})@dCVvJ?Uw0b{U^ zOD+saqMJZOzKx7eIE&*|;5c3$wvv*;q2y9}mjj77%Hg^e>rX9&6(YYaOOV7dQhXPv x(l&{$_^w``$3Yy&lfI{veNkV{{*M6s8$)bxisZV>VE_OC07*qoLT<<*vHb-PamE+qP}{g`W|Q zn6RF0+qN;bJyX|q)j?+NuMsx}w_>!?xv~D~TmioNPZ|uHS9dojZU1ee{esARzs!DH z<~V7B@HQpF1KSVoPYfa!QQGU7UPu1@yf43nTelurZ7_uU=6?ro-}?+h{q<^5#bK1b~Cr?|nuZ{w+e}&~-wQiVzYIywf`G zC0HU-s8WqG8YLMSpN)Tyhd${SwkG4|B{-S2>xs6(2>F25aDPfE!~+-OK|0vqhs-{j zQA_O1m-`QogMXj)3*WC<`pMeucEYG&n(0msM39LDZ4}#4jHBo?_4{q!h4z(q(XNi_ zF#CZvGrk~)xl~J&+(}M0jc;2yY^j-;)@oXNw}yrD{nX3)J$u6(94#Xiog`Dd3x-Tc z0zfg$74ZdaHN2l%$t+`OTKT%w#s^Jqn`h9rdShKoOn9qGpt-Dt%$*X{P3 zR?8zc+kYdDZyokQvjhJ8_FG*XW{#Gzy&Qztf8_bjJQ>Dq)>;CjD9NmN_`|0TZ@t8< zJ1OVYaBpOBeQa@K**sRw);H{pF1xe1Y*LO`TRP?N`2GC-%^lC4U3H|H*vo4F^FZKj zC8=pMhmt5sG`L#D z^mW_rJnybavum=tXVlGjhzHbW9Y6>qw~O}S%3+%6f~<5! zU4JxUo=mH<+*{qcY;y9B@qbT^F3dDrPk>hN*#}O$)lWTpdex)N*gW%Hz7yKa3M%?c z6j#@Hbe;L=5i4=Sx;t@-@Y}?$V2_Hxpgv{%=Uo%7Fy3h7!GVdq=r@lD@Z7=KjA- zDR-Hi^s8$Zea*%zZH#ZJZL5?XY<$Xj{hg8S+QfWi#jJbC`>lMCPg^20WsgxG;=HEp7qgPw}$J!d$H*sW}ANez{{O3XH>0Hdr{Lv zpK{USPnRu*HG4M9^N^3-JmJ6Yo>O8VQvw8mSox42Nl0jm*qIu>;RDS-`x9w`QKgyT zs`zu4*dj(>W+QLa|*q}}HnboqZzx%$Y{U5F2KVe}#X@cgU& z|L%F4&Hl5{w2yxLvq&--Dw~aX!A0#`&uQLtL9=t6VM(qXQ_F@MeK=XdWoc&^`~3AQ zmQCeQ#3HpzbKRR)b#=)+mi6r;7FN9Q){eC@x(|@Kj~Nd4n^LJlpYm=j4?} zTpM(+TXXTxpXTb;)150H>VI7OA%5@7%RG1Yyp3kbBJ2M5Kq90RLeht~DU(y{y!pD( zpZ|-+cYcXs0Qz52ZyupHvXP5NSJ#tdVZ`iWU?5W8xOHPJCj zQLE64?5s4)WE!%PRewUpq7899v#!XRA_7#z{>))WHXC{A217DgV>H%`i^Iew%&ryJXKSwCD_&R6`o~L`Xvg;IAb&HfXc`{(E>HQ6?^oKN zROcNrl8ARsBZ(0Ai;^aI<2?i>ss@dB(!*FwhDvs{jZtaz2-zu?VwFEkz`M;^Z~_grY=*2UbL;Wlf(SOp8&w`0W z1x1MWE@x04M0HLektC{6MKh9_S?@@`mSz>)*RNk_r2rV*R@ZSnu=;N0|J9U ziZYV#y|eb)tUoUaH8M7VGmg0elvv%IXUgZCE7Sj6KZ+M>d#QT0m9bZ!rLQYOPS@8u zc}Q&wlikY+^x28xP@s}o0UR>3zLj|Yd!1C!z&w!X{`U$LqXS3&_wo*pgwp(Xa)XQx zAO63S){zRJ|9vr=_WynH|C#4MulWBv#ia2v;3}}^kvX1B%g2XeBCQ^(uFs!2&*h-{ zFRRX6IKLi6T--}v+-kQn<8|OqKydL+Xzno9AL%dr%J!~&?oW7e^;cl*f%}FSC*v0n z<;zKt#<5RMci2~*r9-1Q zw>x?)cJr?uzkV0I&bn|>FyVf%x^P(HPVGNDzt9+8<#=k{!Hfq(k%8cze5`R)U*i9| z!Y3&%k<0(ZY1rw;=+XV)GIoRPMOS>e9&nhjw6j_Ukjk8neaM zhr*Ya8K;Ou(1gn_szJ>7ic=y~W+@)H6K|#_`bHJ)vyx# z>W7jN{D;f|-z(>55}OA;rJ&o<+qrpWaj7eTgE^NhRCHMF^?kpGjF&|3@>2+IOn47P zT|_be5!@koh<_J&w;-thk7IDo18&Z8lMSJu^z;>~yZ(OL&nh}|3B+rg_i_W98TK7UGrAQ#xfoq5m4cFi5PI!UpS}TX`+Zd1WnY-9%LFbJ^Q18CeHB-cF)Z@8KNd_-XgACvE8W z>&f&oJuSA0V5IqMQT$O5y0H_d5thF$-}pyg3a^7t`$4~^GC)-kB69b>o2cPF@<`qt zEF-fN(_#h*Je0Y6)%G`jAm^)x*O$H8gFEei6Y^%bCoVt);+@#sy~VHbsftKNGz$~e z{yDl`yx8fC#~puEVgTIVG?EJ*X&v`{e1X4K{3X-c;383A`{P@${SFW7fSmzc)fBet zwyR%SVVo~O#H#9S$8nph2vwQho+qP?5iZuR41~H1p#Lf%1TBie1|Il@j={um;jOWN zub;%FY3?yN@2tPSrdo)2cO~yn@wCSI&dUUN#QtNlW3%(Eul|wH%4Pknb|Tz3NSq!$ zGSXY*=}T8mB1Oly*>p0OXs~q{>@#xjtzQjM?jJX43aSvltzC_?69m;It74}HL3N3s zQ?HxE)5c#hi3DK5y}Qx%S6T0RIIs5?V9U6-x{o_B(3&t45-+ubeW+Ciusq7Jq1Woa{~w z2X1Cp_vTq9>Mb)PC@;Y>`(2T}H2ZIEsHjr5C`;Yes48_vfaP^}l|wnU(81cV$D>t9T* zNi&+nZ+GCm+~3sy?bUzj3zDfk^b9J6M}h$=zT)z`sK( zc;&7LbSGtGGClV1O**_-Uv6}?ze!Ow&m_4hs*Ii9eSmxA1Zy>UrP}PSX=9^zZR69> zgiASO51`+uUEOwsnX}Msj70$+7f6<@ei4x5W2~fIGDPyPecO;3%Y%T^5z10o(fR?S zormB~ltBWGf*_ZWrU>W4JfsywQ#dgdRzqGov!9m-*#kF3A@l1r-pX5!Jc7G>4I^+K zNiwC5)^RUey{e78^f7(M5qjSDT(~wryU#ztVEkiy`i{8%M~Jt%a{j@}zrxHL=P;Bo zq#>aP&a{k<<+l2;w>Fiz4K_6*6Q1f~ayRnqK(UboQiu3+3)Qh9h47ft>L zGcw)FKr9;;C=h-%5I$NRAbAL?)8I*k<|6Zkw# zw#<(o7FippOa2pJY(9K#io+@!;G%4auf#(W`NUpP^rl=?Ptqcc6&=J* zs-u90jZMd^XQS#(T4^l2Dv+M@JkKNsNtYwb1wi&*?8oXxVYQ&PSxuce*u{j$ z6Z`@Xei2F!O(tSco<%cF;lxHZ1b;7En*=EmD$ov+)1=^uMc>^K*mDyxQ z^TH(HA5rnu6$)*m859#3SSNWUGm5vlB{P_|1p>zwl^7Jwen?4ap_+FSQzq};Hm)Zk zkt%EV^iikz<^Ig6B4jI4(bFQ8Qq)fNn!~d? zy{g5DCophNc%Q053GG?W8}>p4^Jz6H2^Pz@W4@b5c>^oB4^rQwe#zMC9;h~ViU@DI zIl$9xQW8-XY)bYQP!U}^D+q=WKJU0EJCIqI}Y_wJT+qd*IZ}2 zh~S^kb>DjxjyF$KU&dc8jbXz{<48eRU-bi$8*6L~A~Lz3CUlWw^2YFl;wysVeX{eG z?eb=v@|F*Cr)&$RoC<*VWv2q*&t<3dOU~aun3cr$wGA}GyD5sPZwq5h6DFHTMIcN? zn2{FmQ|LI`%PMk>D`$18f#Z*JgI;2;arFVerLLJFp$-SJ&QB5@uFpCgyL`G;S;dua zN-^M7xolgg5R+Gi>JIIf8#prggQDh3A{3kScVh{d&mD4?Q)e~TOORd$nY=;Dyhr+H zdZ~FK_>3Oxbsw11NL>lWTaA0l_d@&{=k4irPzZX0tpaUAA-cLM`)7x5pYatx z(G9<{unp%O8y|9;ywK;Y*KekWR5lV_RccWHj-pi_yX3Gsr2pd|hYJZq6g@~~je@Q> z6_l>FSTvQTyT)3imYVY7L|YEXXM4V}`7DuGgfb{Y-`rucQGFkxiY^g)m9=_RHF}jb zdevBoJiqf;-f1!>#wq2MDY+d^HcI+%q4kB1pxkz490cUFdkweF<3Eg?PGPP-tOtwp zf&?rS+Pp*tn}X^g$xsFYP2*$nU0d%pPoOJ`e=-O;x!c(viqvhnx&j44FA`%W0#cn2|V&bN@^j?4CU?h>qrW^YhwVDvh z;RY-GWBr?7;Z>n-w2aal9tSM=h&oRy?j}aUK0dtW7p@9H`1yo3lVfe!$MlX0H7$l+ zcH>>`!oj3mCU%KhBZ?$gkeOjF3;Stc>`X=l`fny3WZ3W){^|8n9aTkEpJ|(%u9ndq zytauH5#2Trh!s)Q7w^(>>^Ui9%i(XH^lA_DsLoAa(jM&~Nygb=^hsaBk)%EOIkNvvc?e3uEl%LN`-9q&{fVpJ02gotsf zjxnl^01n`@R!bO1%8$w3I?8FJyQ@09vnUz66@i=cwobu498=5)8C(}@bT~J!<45Qm zcd3JW$IK7qSd=2c<9CuN7)52dL(xXBU24=>hVh4I7W&bD?qL4he)Y5X)!UL+ODiiI z+uP&<0*I#e_I`h{9tZvb2e}49P%g*vbuL&l5pyt>+qOD3;Oh6Bzzf=Oeu>HJlV2F> z1ZTzKcO2jj;?O_J)$y?{e-4>9vR4rY1j}GN)hEy%NdC7dfI<;eh&eY|ac|Flr+EDD zVR9C0%}mltz;~;Md^l)!DEaS*4a|1b8k!ZmyWLoOJdLi4!h2s9nNCVjD>KwBJ~r;; zHNNLL{>Od%=#nS>h1%;amYY9(ysDd>JSQAm6rWat5*hw zhU+&sRkf@!Z_a0LUZ45VackE%WjPhclk;M7jUtDQESHd z2*NL`bhuUal>sGO2%gEiBKH#zrCt7-b2h{R6)Mmm=Z@&%j$BdI!5IEOqws$?Lo%+S z&Xv_`=y=v*8Uk{rrD4n^bfhn0z^D1xZ-tC&-#lq&r(_=)9V(`eO7pSj^Vq=vowqIo z!qe^^WmNq}s&#e1WL&;lej%V*9%>>;ZpuqZW-2bzu`1oM+LNoA0qa;rRO_ZnDG9Y1 zE2dhi1laAkM0JApZA&PER~Bav{i%Z=+F_?_AC(vFbJaV24Ncm(YV=5$(q2fnEHe!& zAVLB=;!nIImzU96?c3{_WgRisrTyOBz4)us|8P?9_@i?@`(_VvYZ~X6Y$r^Q z)rLOHhwU8c^v)-(qr{tM^33Gzb^jDL8hRz6V6ic~V}p)@n}OD=^g@4NY=4BeQU zdy63&ZNd)O_htV60^02u!yzUfT_`*K)u1KMfPZ_Znb?4X*nnAWr$7IIe{H8be~&+K zhJV1USw=I}1Yf>^De_En|I=jI_DeV;$h1OnKH}W>aXJiLP~v=mRu4>#ExP6mwwrB@*Qbvj%vAfpg z)3h!AMb7Vcw+`NWSI%>in%q*4hgOH`1aCTevkF%eDKy_2icqrF)uW(`4!$((_ zut%?ta-wG(;2*5HZz)V^S#HDhZf5VZ1^p!&slH3!ylsVF#?@+KBb7^(zD8)wS&# z;<-5i!4|=R>+9E}qhqeVp5d)2(n`53b1dZ$Te?~!jqE^8O_$`p6t7v4xO$45Y3*s1 zbOu5sS{VrlIXXNXoy~E#m3hE0SnLx#vIP%ll2wC@tLFTrxwXm`k6%z&8s?Z-eu85z z@Fc2HI&4?Z6H|)0Rk5+?tfvz?+Y zxo>!>%5$ojHl?I$f#zH`YwG8d(+Q(Kqi###_YW$=hZx`vGHcX_91*)fjGf$`pQ3qsD<%$64ET~cw5oTY|XRO@|> z7;kwCTDU3wGXA?!C^mTt?1K#v-}=V2-vbaW?GM?DKfiu(cUqCBaBJ@H@HW`{?jnw4 z(rp%*#R>g7Vx8nUSVcSAUnG-lz59m?UvZBPSoude>HU!%K_MXqk}e0kNsK{5o-ZRn zFQ^r=muYWcY-lc&iN~tL#j5!(om4GpXrL?GKtYjJ8>GOB%=&!LU8YYaS?5xr;JM}S zb!%I!=Dk6c4faJ`$qef#M0Z}BlY)DuZY*|$aS+H4abq`UWEjx9QDV8(PP zseXb0&W{c0O=e|+dSOS4*Sdzj_f;bx9g?s?9-YNcy`QxO2wU3VT#* zgn&@AiVayag(dG@Dsov_eUR4>*C~ZcpNC^MY{&Psy{KFEa~DmMsLvZH_$x8F=w&3OS@5&1oHd})mC)=I`l4=m@^lzKNesxkPqz}&z`$tz zZUaI>^7-oKoUqO4nGZW6HHVimUniq1?B$DI38p*>SqwA^HJeCg23;_&p5C;UB&DK6 zZw~5H-Y1;?jC4W&&_gFChCc4ef0+e_lQ4G{drGQUM&AfbELcQZg8wkt=oj|eU9aSI zQMdhsOZY}Wi+J_6-G%zjXZS3nAWbu)6Y9F0>XcOn$I&}z-oG%=HbFXX_-`?SYG4im ze5yCy%&+y4W7|&)gy&O_5ROiUgzjoNq}M1&#Ix?0Unu(f=mil-+;in4O~($c~=uiHMoB;Ipiy`x^?W?soF6 zKYkzSfBgIWYFbK+Zo~G8&r_9N#x_yDLD95)b-GS>&AT>Z`x(EKZ6SyL6iiB8o0!Wt zlJg35)jF$}n@sTU9DRY(s!op<5n~kdtrw`MBO`oV+}w#)=>K>ns~3a*y6<~l2xeeT zvZ!A5RJgo}!92M+S5fet#T4waF(+EP+j(e+=<>a^315J;SFPqKMV^mk+$Mf_V) zdo7N61KgAy{!;Xp>}$09pE}4#e~&>{WWV$v@A_;7*&>zr)Y(hb*n={w9l$zV#7)i3 zT{GuI3tx!8f8meLm(pzhP}ew=<(P>_n4Myb0Up6To4$@buKpEZeLIYRVXAy7@CNDQ z+Gb8STCjUlIMFqouZYFdfQLACEn>KUX4L@vAR93^HdnlE##8UnJl^)9*!N-(={?a) zAVL=%(a?VT#GQCUH4|HGnho&4P3Zuv>)RPKJ{h2f(b#XXM0eXfGN$@QM}jiseScH^%%4t&cm~Y0QxGF-xP>qiFurSTmDSpM2S3t5kmc%hFO4xux@2 zmggUf?~QszaF?CR^lVe41Baf2#e5c?ObyQ4x;l;eMNXhL5X!Ej+Xc?JEYvjPRO{F0 z5{k9;ZK;X3okE-TQQP_D1x&&m3M9(Pzv`2L8jcqG$Syr}F37UOq~OYz=lm~i-7F%G zL^Y$%>{xZ|6B~`bFer~p;jluHpJk*Fo^nXP(@Li0`C`id#atwtMv=hAhH#JXlgtLw zaOqd2JQns39IV(yUo4}zqLvBTDJ1;b6C#?-+es$&|EE`-Oi8@HGh%}0n+Rd+{YD;7 zkdk6vz~IKmAPg|nL>9a+Q)g_ld7D~fgIL&g7y9uHYxsPq*po=s))CEtaZZoyz(|T= zcJ>(s<*w}&1>ViDy-H%a9`MjN9#*qFTj8IHURBth0t)ej zq82X^+s-*lSH-8;uQ$srr0pOK7uRDd5;nS4!xw|siZl6MYF^KU-wd>? zDNTQ3AP%BYu7r9%D^d*BDo17|*nN&n3lHpw2?-H3Ffv;I`;Ywd=eZ_c#_+K*Zr4B+ zUF6}dPwF^xMD=WWjueH@UvwcaRN(lwbqwT4d5b5@+>z!V*ULM8ladWSt^BV9cUH>` zFuA~9(1_2l8Ej~!HSD}8xYiDWr+!lJ7WzUQ9@4J+Ue3Ht&y;^gE&A_0<`)SYY+8JL zDRl{7@}_f8O%#>COp39uVc;oIHgPs}zX%1+GY6q37!nO;QxjI!@0 z%iLm+>&skRU85Ek^?#DMRcP7kk7_NrT|Zx*_F3ggL>4y}*X~ z1$j`sgFfZlvsqgaiKPj)G$*CN@DkjYk5RABnY;RGmNfQ{L(s%7xv;PWvJFD`eTc+V zSUS9u4+5 zp9p;tG`;1<+f$L}!<_H#yfcfD6TUVQmuia};m*XX4O?SpAa&GVQ0s9^t?bj~f(@H_ z*_GdH4$07r;s4;T%$nOf2O{SMB^D259M3N@bL*E~hlkKgPYUY(0Czps&{7(LpqVnD zIX7$LQcE4;xXzqE{a}**ZO-VG=;=S7hU>84E)xyB;cwk$?Vac6oTDmZCtJf`4IdDT zhcqLDs3U#Byu1v{kSQ-CRAjOHT)xXC%jyW-m%GQ->?((X$=n|szqIIH1!69_Q^nOL zL^?6P#y&XhoOSS~l!T~0#ZQ0Pkz+hwJALKL1 z=cd-yZU_D+#>ic0;6rmoqKT;?)F6n*B?K)rod>dJ8SR6|#^Tst@g@pOA*earnfAzZ zY&qVgZ|L@D6j5+p`hU%6*sp&6qnw&SDx{$kEtsRIXHB6%KfTrf``eJ<%$< ztmLs*i2j3qRk8-prnus=8i+>CmslWFW@? z11|138;x4|X84a&p(0CNH1APt_is(l8a0Y?B~@{ELlPAsu7p9u}IUQ;=u2w6rNaY3vaHiNEcPU-gfSwCk9>-jO?c1IqnN$%Lw+bIwq}=a%X5CZuvXaNUAt--109uC|bD zb~04>smvLvsp&O@JcX7?NxQz)>60WAeiR`o7HZmCPk_gKA~6M=jn)qq1+IpVoR4ot z?6Gb{-COD@IdWax0>aAo*bP>V%-f8lnaGC9x(`MO^|(3EH$JqEzEvSWg=q^AEGoBg zxxMMJOSX)<_755!W(U@ZysUyMW#cZyNw}Hlh1N*UQh?_x<>AJ^cood&ToUA*dAygD zd;GvZQ8?dx%Ez~l&U|k&O04Q@1EMfoSFsMQ=t#e`K~pqWV3ZB!&?G z@$hhP6?I_i??87b3lt5MV9q$T3RfEmi99x+ek5dBMh4t4MGHhy(X7=3@eU*6S3PCsrcPwQM6lOne85Gp)8b{<$_m$or&nzYBBrMY ziSqe#VB1jvEMMvA1gTT7|LZO+C^#?wj_^kv@iz{6-DE-W7SJ1)B0d?-3`Q$k#ts=Y zo0LwuUHeBi>0|ES-*SzBwbcE>+SwtoLFgqw6zZTorqrmYw+~-vx*$W+B_bZS%>E=K z6D~$>W<^V%TD@#HwOXccR5&XqDlknO-iwVH?vzn)Oen2uLC&7@{QhZ0LW)k!t0~C- zIf^87w!Z!)f%KdCHI{rriSiUvp4|eDH}+kBJcZNV_6xrDQM3)!PL|~PGWu2y^PaVc zgUoBI7EIG18FiPXWr^O2OaiHby*{Y<^`Slh3x@gku*2l-JuZ7^k3c-b+DRmItr<`R zLqcLdju|g}C>!kW;ixa)C?;&d1;Jw$rm5O#Mn=+SUh5>l5Jb!Kvbk&)y3r>#kp%_P zlvGq3Yc5ag6r30DeCVQL+I$gSK-*t{hZ4dy$q@9ma3!jxd2%AH{$6px6s7-YjXuwRPFCJu5GGEoKY!`5zUr^A8b;F|3-y22h>pbsX|O+k zCA37!DCzLKPPn3tb2>+nZv0O@4>?X^G3&ws|Mz9C{>>M0aV%nM%Pn44$Lk?PS$pT? zdzwLI4Bzx^lhAA+NUY`zFJdgZ3ftBD!F?q)=OE90kQ|TSm&;{~eVl29?8_9PLMT=yT4 z%OKZav)45sF(QMBbMNVY#aGNQ;U(YLy+VUs-+Q%+_(T=-NVj^sh64Vnal+jIvrsR| zfGF91!>C^n6cp^W;qBVm;#+BZgo})U-ZNyy{xVd;7{Z_Ad|Sw-ciQ$9_2A4b4E>(9 zGDschWWajn{5hs=dh2U0&#H|(q;!*Icz*U~(*(^fOuwL6=SI#{%g{7kUB*oa<*d7H z+>gC^aRf;e7Me4ZgdGNMjvfTEWN^v$Q0!PBj~nS)(}uGCqUZdzest|=^Pyc@vKg*i z2X!e~mdaq*J^xM`FE_vZt84!XEvD~@-yp?7Rltw>13R%ef=LnI zJm^h=C0X($%Mc{xYl!5*r=UG~C9rnfd?C$jKk83Dk)oVHdivd<+W-Q`nxw><0o)M^ zhig$YU<}V&1vgF{)5ZN|X2iYFo=U@#w@Da$I=h*C-2&Y^qmE}0H7QKHwJ)tjBb|OM zR$@#82=a}Q%sKDdZN8QEfRN2idP>Uu&EsRx?XBOBNkUu)F2+kqO-LzAnb??Hy&k@z zRMPI@C^Tshy7>j~rbHSfp=)O!UK9;ifqj5(gP+?KQ`qwMiU8LKtoPE-h*n$qwW z6@BW6$M?bdl=@VY-220jaagp3?Gpg)PBuS+-|a&MBz? z4TDM8D%0iJ`2+dU&rf)MdAV!!5GM}j+3`jn=Bd|5K;}US2}1uy7u-d+XoQ|Q(O~Qn z!8GcLh#9m9-rOWfQw9|G)Z}u}%mNPfe?4qq&SdQI{D-x@n&;T(I$hQAl#R;{{r`p5oH&GkfA)Zw8w)ev(li z!9aI!aRjy>DCss0dh@HRUaTR;s;)jBVZYqt{y2pHar2#E!XBwibg5Tj$XBT8%p*$iIwQXDNO(cuJ&Qr;dwyAR|LxHb=WafeU|o zrBnz$A%nI3%{4h5wQk7J;exr^@u!!z0TA++LIG(t9iDoWou&3^G#=}%tc*+`#Ho3`pmL7~$MYj(2-QM|>{Pj%*(OZTDb()zoUl$i3BfUu9U)$Y9qZae;y}hKKIB=h^ zL%6!Sg0aK4J_LQ^P}6i;hVUP}VFf2jk(+&GS1IAs5?a^i`uVCwvONP0(z6!h?p0!uFudgp_S-ubGhU;3+Ju2f4Ch^0<1n^h)%grT=Pj*m6-lLeHi<6Dd- z=_5lk5gVvJKL=L=g3hY0uqiGoCf2|DaPNEtdpLT;l$VbHI=9_R;a$&`_cF{W7B2-b zo2Qfc>@pMJx}_0)^~vZ!bzn+*(g$k|s=#O*pWhf5879gxD%XFWiSvJK7me9_>87rI z6}cc+6@sq`3|b)9KS4v8@^i3e5in5!1_LA&kh?$jCJQv?;kQ%Qb%`7ix4IsnZ$i9{ zeJx3%76&?vSJK8g&N4$E`E72$`yAf5Qhn%Tt|y#kcuV>fhnezzM`E73rxuyFxZpMw zhonW)vMh-g7;oDl)2pNu7A8n8>IV2!rljX*zM>Yoa_mZMVSQ$%ymuIIo^w>TLd9qX z%Io8>n43X z9VV+#fx^a*jGTTn;(Oi-kH^zxxJrYvl)rexs6+b#8hp2+n`$aZX(C9PyLzu5 z@t$*~)i;cU+sFg}&*_7eP7NRIAw+@8?(MkP?4Vy&rjH~R;9Ym&w2uP-OWJKGI}9~8 zHl$BP2O+ZsGDKqiQRZyT{>uGyXY6vk1~d52nof6z)KK%Z5ZKW7UCO>*>ncroDHCZ(yF5LKv? zz#E1O0i^t5i*xdF%^;-|I6iLlkIGpK(Sn^po$~$jPTZy=Ms-^B`|QrH+t9mBh0cIE zqCdR|U?|QS`%(h^)l}jni)auIe!}~O`22%=e~)repP7e`L=r(vjI5zi^Un0j1V98* zQd6(*?&1y%40xYxkXEaxxZG`^g-DVBgI~(5LfL;eZ>K8K&nr5;EcFM)hvuXm->649 zeKr6MYX;5u#sc>7zyC*M%htsjNx0R*_f+9Mc>PH)R64SO#vg-ZhXe?g+-^xtMcHrfk+H#* zDXy>6Js1fsa~=BQ9moip%Dn_&Uz6aDg(miFUt!bXR-)0+`4mEd(T8z*nQ6dL>pBe= zs2>ky|50eb)l-Q6;&OSa*=W3y5SjyFs`o)=O1B~83Jiq|j_u2~=3dUI)vK-j#(|H| zA1|C?eKDK#K=%pgi&zmz^?L8;BTnQvMZWL4m1zc&*^G_rLdNmMr#RW8{Fp)!n`C?f z^~nheShl$|Qt}Ws33fC`w!9i%f=J!^95^-0Z=zlcw-k}EpDm->;iU~!K|QMn~=|3x!t z%wAFr3=KblTsQdB&ZrIHYu7vVAwVAc`$G~d&2S@g^k^Qyw)#haOj>`s%cCPIEP>WZbGc-WQdEkj@ z3uI>U+_|ItMMq$2lA^AK2Y5o%o#$7xiNxpUrni$iDmp9)@!80Ht()o>J$4QKBO}=D zfugVGzyQ;qQ3PNi9WG`74EwOlXIm$zOQiGiRPrHfDhb$!r$!u8&9w(*ata{#Nr!e2 z_==(P%%L$w#N*+riu&DJJ)55vXR4?O%$OAz>gpT`@fGKv?nP@ny;y9F#Z{1j*iJgN zV35YjtLf46?P+p+#!Qw4W5=G>xp5F<{^a)y)$$ZX>GAqQO0J00f+z-~n~LFuAlWI>QR7a%~B37PhBKBO_JMJO$-G z!qj-yNj#e;(aE7o%O2|~EMGDuRkA2oy$H+%g_rS0e+%VeV?Vy<09|yUI)Mpxx4toF z_2V?<3g5iV5Jp8dGBV2Nz)aS()silBd}jR$NH0K=vG@ggIoR4Bb|R~vD&s^hhR|g; z()qUCJ;PrKRk{D}EOv=(DFG3&m4fuOUiSZbF5h-rs0!|aj(kZ%Pg;hr&Txq2p&QQK zbc#nAZwLvxx^Q(kA?a3_)QSHSk?)ubflCxAv1sO{AOKeA_ynMxJYF^tY}+Vyins6x8geMqyDp}aca?#5|;Zyvf4Zx+Bowa}PF%TL&l9S?>e z^)fZVYDUQ|DZ<^#7FM#N;-8h-krM&EN{=)yeE!$3u8DOqpo5A*IIpfDR#(=Jk8uG@ z-y1#CO(maVmmSx5g*5@MvCpy@(F+KW;{PrCy(m(ex+Y5vziTbwY%wREGeA ziysW7LfDY(wzN&nA6p2vCplK=84^Kzq_x|=#$vYwiJ|J`b-ySrb)O~-WBR%|2^R_0c($MV%;G#tuZbMcExtP)B*GmR0+!`lU!VUwx%RW>oAlHeDPz#L z^KZFSJ-@v@o@CHnaJ>~>SG~7rJXJW6s{kRsby?tMT*dPJ@KB{WbDVB3R&P^EY{^}= zlT`W;{|=6FX>6%Cbowx!J|-XT-V9SOW;>`QX#?eo!4exA~xkxXJ z7qPJC#A<1e5qZB9_A1Ele(6yB#Qy%qWhIvSg{eltU7(kFY#nQpUZHB_5k?l~GIeXf zRC(+VL>1ZUh8A;lcj0&S43p}odBcH+2eIP1*8sHMj(M%3+5t?+5%y(_=9Lzs=-x$v z3KfHuaLIPwFA4o+=+yg6HBqe(yI&yXZdy|;S-Uuwx!%C>}E2$ zA+L$2&XQwCyEq~Af7D^!2R>tF=63z}7s{<_v~fO&9%oEt4(OO|fzK+noQASL&0_Cn zT#Q`Iio$tJRbP8>(ybXtG%WUGV*H0IiA+}l8(3iK4a=f1#uy$EMc zCk7J}xSo`KZ@{-B{cEAx`3mw9V?cdX&8{J4lXYzG}c_D+Six(*yVaR@c_^iVvVR5Q;OS|D}9fw z`My|)#!)5EZk7A+2OPH$&+EV0BP>*;{r$Jcf6v)l1Me{arl@~#m;}&Bx@-hxQj0yi z*Yo+;=8>+p-y83laYk#bn1nrF;|+ZNHgJ@SQ-W1@;p$DDh%{oZ&-?B5r5-ELIeoXH zSleC5kF7CBF{yj@F?`^U&os)+I59O>EM@|LOP|^%-Be&GKW)tVN#p+WhZX0U4_=}4 z{t>ekC-gZ!HN9FBzUgx7>JpA|sSkaDScfk$j_0Fy2TCuS|0(^%bjfPq#&O1T(^3Kf zI68GsweIFtEt713v6p{i{_8#b>gcbD4EU8=DCubX2Zw72M0KJ<)VfRL<B2jK&yzCcc@0=h_Sum|g#Al@XCTKVM6|&TNTIkt zPAP#)3MSABCUEy=$PrVlg7-0tw1ptT)>2!o`{myUe;b4$68ELSimMHay4=Z-?bFxM zX0e%1!(vmbe}26d3%L6@SIu3kSD9e>8sI!XF(9jM$AMgZtV+(^#96mOG07Jue=~2t z>xQfr$$UQ&a2E(djSfBfQi?cubeB@}yKHQxbPvImYC<1m&_*(yibi%Z$yHzAh*93; zdP7hc4&A+fuE5Ge8SCQj|MBr!+>53Nhz-7W|7!;WFwY^i=PyQh0ls&pLjgk5**gPd3~6E$;UH$6;P?! zDNtgE<2zY1aa@m}7r3ay1yTf{ZvebJpyo{d!SYFcO%hQHNr+E@{4H(HOHWh&t)hlu zA4k-Uz;BY{0j~ujzrcLEfS_*GP z+wm*%)_vxCAxCC;-woEa_#Z(pQ@}oXNm|)?iwA3cG3HsVVqKX6)yIElqLfef+!quy z{xFG>=G4E2)mh=&6~O7KM!65r>0W)6QV;8v_|;(1S@=^@nhBt+(@O2jwEquBX8{!D z*M;$AU0RfGX%_g?(jkp>cXuu+NJt|g(kUSV(%s!CptPXWk|H6Ebbps`#&JexoN?cM zx%Zs&JkReWFyKSULm%O*q)L-;IKzWD$J>D05LK0w_Z!LYOKqAJ$78McG=-RsY}c}8HG7dLPjOx-L$?#59S@o9`lpGm zVk}v5G99LftGJITl-_|VX3Aw0+4pmmw*H*n))$_DGk6w#^RpW%NKc@n>RVx8uxqfJ zyJj~m9zk%I6*@QWPm9Rpk}Oxqq;a*uO(M>kUtaDl*JEnwXpzt$ZV?n8-1Cgc%Ay!4 z@(`R>Ok-}^n{lD$zkZR@Kyib%%H7odsB0YGqnZfh6X>ZxxaAYhLbCJiwXNoqVZwNl zwyDoCp#y1ZXaux{my36jSGIe3&~0|m`uRY)UiIO|DdOe~Y1HxN6o2g~eH+=V=_d0+ zV!o}iaxARC*6`HhrqEMk;p5Vu>pN%FE|%7^R>Lgr(kl<5>Ou}!LJ{Ue7R*jQ`Kz^X zjd^ZVdV?d1M26Upms^*iOEV`XFn;taA(g4~d=FE>45#F;_NST>I#}iFSULu)!yl?A zATBPhhd{)PVX}&eppO}#kBQJ0_7$bl!d(A>Ymi2(q*Xws6I1WK#2;Fv_Rq)7WUcEc z4Az*%io`q*pdlP_@4Q>7c1ToMC0d^oH2Sg$|M|Mx@5FG_h_8SX4e2gqV}g7c8LI9R zKnpyrULXI>b+FbhBc;vj<2tDc?f0=b*`3c6@KGx9w?x0V@qYdOU8y3PZ$6uUDd^&z zT8`N2m&kbiYV49SU7F$NzHn60yY1=s@LuP$+7H_%%h?Po*XlvGokc&FUy7oDDiyxs z&_h(qEpCs=F26(6QCPh=i&^;DF(?8CJS}F=uc?27anw>rz}w}Z`)Advrk@vG@_r3D zuVhXoLF^?IBpD4`-EX8vwN;bT>WWmyhBtde>|*(?rLyXe7z(qA%kITt+Rg3DW%Q?P>>3hlw-)~nJm&4Ut zKhO4OySw|p4_6?Ok&$5fG1%LSGQ9(ZLJg)v9UXFIt)-YCGmqUeP}u%dYQscPwhURJ z?BW-5yf1iXGT%qe$C9wjl`)7C&34Zr-$@W`OWgBsNW zY{}ST*+mMn5)$cBf{Ui;1j(EC|Nfw=nznV549davK9LL`!S#;0lAk}DX2Z_$5Req| zXd`GCMRBr}4qu5fKw9;sYZF-eDh}7eCvhS31Ns5k8!q2MV5u98Gx?q3g_W`khflm^ zz0yw2F0YK0A%!SuMT+JjKq?w8XH{A_JM(gX?HitzWdss7Yh{IH@87wXI(&h44=yoKWPuqmnDno6dvKHUv?~OC z;ttJOs%O?w)pE*(SuBYM3~HSB+8FfiHfRJ9*Y@ekGiYiOHS%&}m$w`vR8psz`xUjq z`T999T<0rPw)@7K;iK^?txW-9#x&`5#NqGX|JQf1j3}@+A#CZ)rW+p1Whcx#J6g)jBnZ{GWZBp}H#drKjrk~#00de1Q z&HTR;yJ5k|y_3fIeI;71n@mP|=ACHpo65u4DprbP-$ar$x_dR}mO9*g8kRerC+#Sl zoeV@+&QR#BG|y#ANtTZG$tchP8$g>0-NyCxwa@VtQCc)(5^q1qOL1^ky9EYDeNLa} zX{ks5EOxQiHSFy|lq0y*xJIzn74fp^5MUWrGR(Ew&v>b-ULO=UIAFGa&a6r(5_;1~ zh4w}4Wj?K{>VqX>=Hm8z`S5HHb08TL{H0;#0xniDpU}N9CZzkB=23K+|C=_$Y6K5zIYhnA}nmWSGT_w#utQ z61J%c-}yDkmN79lFLe{6*glzfafPD8GP3DmuYLxcRD58d0r$KFUDiZQAb z%#!6LpmI^V71<*|!80ZH*=jpw`Y0g58z$Ezh z+1WIv!=5|mkDq=w%FfAka1QhumfA7RpA!z}zq_uVxfJeXE!JL-|6#aidYEuO>b#_p zeEXq~(T?GgCYd2bH3X@5aGVB&Ql{8>c zF4h9yhw%_L>&O%@2#W&y!H>Py&Zj-5)gbTnO|ls&TQ18=(`48h;M_QG-}sKQz+slN zDahJ!jS-AhQtiGOf;3Hn4}22hH-0cxmh~HpTnu*2dta6HH<;{YTKWZwPw0WF$w?%B zkwSiRsrP{5S7KZ{$+G4aFDiuOKbBzRE1sH^C*_g>%70?_=Q2t6TnS%62NwHFLZ3C*pm!__qtr5E zgZU9X<<4;Wv_j9BLPoPUN_2%mJ>*pWErUf)x*()@?+tWO`EbGM6%)qfT^^0h@5kh} z*EE@uEy`Gq&_WtbirduHky;`G2tkb<3IA`8iCgtDx_cKLP1}f|gBapjpzCqNfDB~C zMQYAP3O*sw7P7jZ%!6QVHoqZz9c}_&a5(5OlOql^;m5ke1wl;-&95l76gW^NYYF)! zrdkR71)jvRZ@#KYqs#3jUx`QW`jBN+AQ|I)RsYtRJnVYf%n?bpXNJIGr~I|hWw!VR zMZ~rxmNI+1mM}$9GYF0O)7u3zmVq_ef|RY(tF7~rPyPM$PLs9N6hiU|HBG4bgkhD+z`XG zC@m;5+c?#0L($Y!m(;L&#F9=kX`Fj>UcR7bYzf90_w?v&6mfHRZ3Mg;A)SlQJcYA! zM>#B)ZhllRSauVvL@Kln=O64g_BzPl73}-pxlBH8fTxY=CbWI@q>V|iF>CtEMClCtCV*oTpBM#>3g$*J|-%cSs<^8$s)u&`IC_AKx;f8!Io@q zc_?9%Jxej80N+81xBx&0*2yMQ*w8bCa$W?fUfUYjR&r@#xrQ(bP)Z$bi?bq&1Iz;F z=rk|}?N^F%gKJ6)W92+@U^DM>+Xys&erIi*${s~DcNTx*Ityfa1rr$9McZ%vJxY*4 zbAJ6n7i`X#aa|$e!k8%IBYg4h_b%9SuVHHaqIWbg{FQ z=!od-v zMm8j%AD{5)%Bj4B?uD_-X97dNzGc=bdY{{Kw+dzz7bqcWCnqc02ZVC5$G>HY<$xsh zUFpnMlroNwuD)H-R1$@T%UW8(eeT(m64!-Zv2F>$g9CTGg!$?7ruH;@>FP@~ZmWL{ z(Kf!!lvlER5$(Iz$%NL#Tq63UQg!&xgm#oQNo*`V-)6bdWAgrwxhFcK*e@lWFoO03 z^m&o!p<0*+B7ywtO_dhk^OW%$>L}}!x(^NxR)3uk4SkmRw0UvymjHB*F~HQgZi&Ki z!Wc^wvu)Xvq-SP<+k`{adSpz{=Qma&V3Js%!fT`E^yQX;=J#KqvvKxB-1|hM_Ap^1 zm6yrqFUo~>G9O(Qh*a?p_y2o@U?@w$yu2w4i;%KWe6Op6LTgS8&s*sR6-`|H(<7v- z>!I1*@G@}2lxP~Hxl2QMN*!CY+qkxoj4-U4h;{{H|JblHv?RknB-}^01%cW(Cp}tbA)@@$w;})HT%G_>M^+gC|=NpnuB2sBcI*z0Sb`;R=+|`P0TN`P9l;Q zqv9X&aeAc#7VPtnJ~T8OwT0m-A#Ckf(L1_mV<)Snd)AzjOB4y{d!cW5a=G|)LkTNI zo2w#w6HEu!5)J%9?uP21G_j~C1EDs?)V5BC<~54N0Yqm4LGpekGMq1W>{iV(Qt3*! z^M#_ww9*p%ZvSS}k&4_QZvD(I5cbs=NpfmHq#dkTthP8vc8(4j?dT*1PMyz;dz}wH zLW4B$hdnP6hfK>xiO+{@kV)0(APz}h)*0t}sLj~q;9sVI@1Izow^zb#m4_x{3iQ#t0PkcP@j)EmM-n*Uz1V_!TlFLxm)k!FW}VlTbBTscpk z@tSdPnOsj1F;l5ZLGwh-#ag7BAJ#h`&?W6$IWKJLu2=tu>?MO}znsGh;Z^!EWJF5d zD^B@@ixh{OELOSb+c#Y>FmeG|U)kyz#@`b|6OI~OpHoVGZOVg5cvhC}I!&}MtR&HI zt4szW7%e?bRAbuxYGYwe;b*1$MYe1m_g}mhY}M@fziD>`SlcK<)H??3d1W+G;1Trw zmaRRfK-T&c*z4a_Q%iQZ&)80s?lw0V{9Vi0=yxIb#-_Ywh?b zllQ*pYTfAxF@C~4PY#`E7mp2cab!(4^^2lO#=*D8S#YwKUub`wi~C0DfdLeFx4}3M z75JV8@3IqSCcLsC#Hvy?etCCXBD8o5bczgWff^+ufYeC2()OTo&+r*w zzBsw!kPeyV%WcH@#y257Zq56L{XNe&mB(-Etrt+7ca9UeNbof^+tXuM3ioDf`(sBM z-O0*F?~blTcS|Fx5BksFeOnQP0xZpKAV~wcP590ZtFo&rSC(pGkq~h->c%lD_4~UR!;QnG}?7ymmSj*N9gB2-^nNZc9jclXzyV1@5`V9Ml9iVWgr;z{xcU0 zGB8Sm2NMtf&xflUeQ#m_t#8AY86yr?z#;g|@*ovIkC-Y@Zh6N%%a)q8Rh81cjvL=p zE36fl>AFH=Ci$O}&A}VHI`Nsc=lgwYVH>(4=(n8q89Ra9c2HKvBCRsgR(&>Fo6!1W9uT}SuT9Tq z!@|&73rWR5#Vqx8iaI0H^ky+D1I?&-BJcMn=>NB%R0rL%-YwsscUuDgPNmm-)nc3D z^%o++FFt7!Y3rb{WQ;gWCM+Ghd|nS%lLsNI!79AR%fb=?6BDhr-HMO6eCL0@9!h~1 zVa90Yzk{U60?SF$z0(VbT6=B4e6A&S7o1kP_e(sppRcCxAaFQ$kc7G_Mk^VUc>bB)8A zE|GzS$SMgx+l2(UGn2oQt{su0qoV^04XSB}{#-hChP>92K8=Fo;))rsKU7{^epTF# z!z(OJe%ghWiYj5WbI#{Ef~@aLSa(UM*yE>yRk&U@!yxLV$^_^OJ+B(2G<08T;OcXv z;X?IE5CFiW7a!&}EZ&}knfI$wHDRkGkmsPij9dfvbQ$>h=Rl24vO>FY-`<27L_ndu zkWqeX<86TCSyB;^PC84o!32ujdffSyV4!adn45tAC&u8Q9B{=3t2YRfDQSw-W@^nw zw2;`fRcGDG9!6z#2=Rm^CsaGCrn6=uYu zuXGL_^cnvIhBgP>ULavo^*!gy?fe3XDElJbuV|$zKO7%s56JmB*h^qdVl+a^r&J&P z8Rw1glr_MBVx>ZIZ!OiT$UpnNE{rqUdr~wdUnYfXW!g|)D-U@1yw zx#<=5cMsQ>Rloq^fFVYOaMXJ_wU1B&ChnCa8QA;cp-Xi4Yw4qn;H?nZu=Z&PwG^Y45AhsW5X zcI~&t|2ijd>MpA^>{Dv5cvCIx@Pj5~PZzsgL{=%XFK)cQ3^>SAx?SAmW$AFvx+?A!&h0#RDQ-J- zh3iyxuwlQtibD1eE-i zKC^Lmw)X#>scyIduK6GK!60FDXCC~(?8$Z2#4fGTXhuqwry8l`RI`*sN)~&GxeD?K zESDb&mBkm=#G~R`Bivl?ja%G5^X4kG?R&Mye)oZKjPT{Cxchm$jXw4;6w_x(VeAJ} z)PHDo^p}QH$5e5C+q3hb$o(aOBuI7l_GsLN4S2e0VMW2+aQq$z;nNXFP|_MNPkH*a z>Z>g-s2Kzhr9jzcn%(jBAm|bsjHtrFn@SoV`K_KX>d$*0>nCaC1+-Wk`Pj`J)fTAy z)Js@LMc@0BC*24dvtN#c3XgTa$ARKrFjV6HUc;7f6DLh91Jlv3j*Wc)3FImE#j*7{ ztVhMCVY>b7YGkTu2iDt8-1XGjbVFz?XMN;+Nv}_&NDaS_eVf8(&C-haw36sG&MfMq zt1e?i|4~(b6_Ie>--e(#Mt5D7_45khI zS-Ikr=Dd`^GXwm}?ZDmpWq+Sz!0PISKn9HR;#(SddrPuraKx`9hwMk)i?kX}SOtq6 zA_Jy$#yPIK_HIUE&$cJEkely*zhRiqegY>j%w<&~STHtQ19ANvv3Tz6(HcV{{i-y8 zLsBFY2oZR4VNhHgr5o_XOBA2;^bm{wUYe$rf2Lb?$HgiClR^vz_z= zeT$6F%~NKM<@j>Qn${iw`~s`jFgP1SpQG@`=1ivdg}|f=HZr9?giz>NnDIH>tGuYX z9m5VJi9P7X5hK8fmj&t$Z1T-hGOw?si>lamo0}K^PO;(1m;n1hyAB1EApJ}lRjXk< zLiXX_Y(sqqAS?+SD&;ZW$#lfS`5Z5}q zmA>N@&2EECTBXvHox3}#Cr_RNcSSI;|L-0g0zPh(6bqe^sTbtRNy#m z7a6??ukJf2kqAFQO+8EVwZT~s$4`F3vM81YbBO)S2Gmqw`+so$8&|1UC}}k{+nSU# zYfvOs3TY?@<2?PTdK|DPCU#Qg(t_itwB=}WP(GiyS0pABl2x)up%%r{%?t^JUVkJe zZHiq(_h*%Qk+kIcenOSHs0*vn#g3a+tHCHFiLqwDvK1J42KJodzzm#FHoQ!e`F+xV z&z6=}&d$)LkQBT7suPY{1K{|eAJxq7zxw*6wP@kg7h1VtW*M#8iII@=-jHMfd2l|Z zD0FXnQH;yAFPKQDf?Bo;@oN_WUfcnszz223wUd*_h6qCoP1g6T6QR2zp2x}(c9u`J z9iQvF>@FGwX;f3YSN@;t7KpO1c!Z#X-W|M#YLMJKHhMpu9N$ovQ>WAWrjsM$Ey~p` zhiPYE_!>zh__^!%5d~B!K8=2s7N2(sP)xzs@0h3!+ZSfeDkmH!R5P!81?b)kVl`J` zQe=8&a~(C9PN88+{sOfn~rQ(8{Uk~Jz20n0F?e53a*7FRE z?URZmb9=!Fl)b#UC{Om@pmkhmB^`zevwi6kM)&_4I1Oo!oW-GY>gos*_opzrsNxTG zyngXy47znS{wBX;vo8oowyR`^2x&gG3F>T8}} zZ^pTLdc;qQ!(NPA^#&G8_I~U&nVkrgmECLPCgiK^x(tj6^fcn^v4$jcFP3j@@;4Gc zQzSY!xk1dC{T7qhudyZl4I1ha%UwY|tDzSWq!`R$Y4 z_N3ioVtuA}e_oX_<3++0>Kp>S8G8Du-_2L6G15rygp;{6DePyg+R>^I08z7pjkUqZ zq(Q_aMK-E@SgGJZ(}PWbzW{TiQ1-K?1hyik4p(Vj9Bd}7<=q7dz-5b*y*{?kG6{y0 z^zzq0Hg2Wy&Ed2bRJvR!B{U2ZJ9sD$2eDqY8gMj(y}f;=43|DZH>r6#`wNeTmZ^~6 z_ScYV4CTd#EqLsV*)H=gI;)`mYgewEnQNCdkCKu3<&K=U$L%ydLL2o$y5&SeuQ9W# z2BerzP-f2()#ktfe!m#r!}cD6F!b`SIJ@$3+HYmCUGS6pRw6Ws@mjnQTPARXgM8j0 zS+~66899~jTV#Y;>9_^SN4yDJ1Jp^j@J9O!UCR4y%Dmh*98`7z6Bgh}006qFx-o_K zce#_{Zwhk%_%r@MEh!Px53sy!= z;+0HWhXfCOg<$so8T(2hFGojllJZWRn-1k7yaaLg`X^tIS^N2;v2DqauCyo4(@g^X zZkmY_LytRMK$Ao&7m4n3y8#1+j+*A?=)j<0Q8n7|Fo6%ejZ|a1b`_5fqi|f`4vcTS zcRY|@rQWa4rW8aukZZv%kJ2HM4X$n+@E=%LFGcHdY<#IP0Tq6)@Ge^@TOS*_-Jm@~ zaJqwdGahD%^lBN@Tyb6WvF(VpIy?)xs4v&@ej)I;wPtbqO)DaDEf_nd>z zsyaw~ejeg@H|xbz+Kyp9KWzI(=La6`@Y6P#_C-ls;?m5~qIBh#q-qi1 z_a;p$PK}Wa2&IvRI3MCiLrpT)Kr=xa-z&vu`2PNa{0jSv9x}IL?`Xb662c(l@>7I+ zO)}-E%5W=i0j+{54fy;Jj0#=AG*AN4fUB9!kRkT*QmgdEmGrCoJ(e9t01CaOe8f{&aX$5uDe6vZ+>?kXQyg)R(t_X8$?#pjS=PXF&|56>4xf(%W7D5 z8iK6W!h$k))@W@yFb`GL{HK^=WAiPAmoDXVHj9+#$>~+|P)50d-z%+jdd}CY)J;ee z|GndZG-Oq$g1#^H~8_#4<5f^|UyF;J{V4Tj2*w{Kri zQtbj)zvUzrFzdOcg0;u{=d-^!|$=ZDT-Vo`PZA&O@WlRPcL!QNvt-Un9TEqbCaIbHV?v9 zh)g?tLUbP#-y#oI*~TLi>^+(425M^=g2qE8_ps!!No{W$!r!k|1|2NoL=XQkh7vXk z%hPLkX{t6r3W9Zc9a7X3_cmlkG&>vkM@55j$JW>_+t6mjSv zx3yDn$1)-MdB;e`9~+z{vL)z;e3!Rb^1+-px1kb&PLeD4)8u7zIYF7~657RCR=nhv z-rgi~gXlh|+c03g*zmD^HASw;YHx}c)A*_eNyk8kqlZE9qLSC~QnE3Mj<@?NQ147- zvaa}(dPzt?L&^{nZC(g0cp7fv6!mwHa%M(L+12i$^kDx0NW$t^(-jK9?8J|kNax`q zfzshuvey`KK5Q0M6oL!uJ9T+h9P*p5<7xNQMoo%@dbBiGz%s9MTd-pB+!VUA&ghE% z>9{|oB#MuMBO3%2q9N%ZtFsx(O5Z0Tv@u_lcckASll)d4Q3b7JVN^$i_e)vuclKvM za#=y>1RU$&9k6UP&1bgH_#^6S!AqY1%Moccw+Y`;M9D)9EA`eeAP_VvDZn&+90NCc zbMCO3b-yZr)K6~7){$l7Q-H}pfw>=855~qw%YR|ZXV58Vrs#xcAzA)$3Z+F8<&OR~ zRm`vp)kF6jm*a}TiCW(0_osm>>D)%j?cusVeK^c#ZM$5rUIfqnoWBW}J+NDrrdQKE zL3;rVmFC{AG0gx(u(bsNQIh zr>gPN?mOoirdQ4t-G(g2*RkH6Y8NvH9x4|*R{*@Z$LoAhEK;o4T@pVt-!rp|O~pb> zF8`lIXa;n% z!hZ4{zT3V1{m;lfpk{ua*`ZUJiZ~h$SS80ZtQcvY-?;>6{3u+G1oFUeCjUbR-`%Z^{2=Pph(e-GEg!AG`OfH8biu9@=}d zyt^uNm+A2@Rcy_buCGG|X%>mxUZB{I%?R}E{Y(0`Vi|Jz5ev%%oV6e^5E+RMQtT3+ zHszCeYr=Xj&H&RD#xvF;K`bCk&n9wPZyuh{$u;UsyE?v>HN9n?gd9SGRx?4KD{J)s z`B)Rfh*mb%iZoUTHUWCij4V2_o78e*upvF&-3d6Snetios_z_8+ig8cn#i@1Ys;x{ z^>tD+dOoJ57GOqvp=Ce1{?HBb3(G5qZq&eC3sQ7&gn0Vk(-U{GA1afpu6DOQ!HGDN znL62~-wEJS1fQ?x0=<;xuEJnVZ4{h;w;Qy~C}>ALYK>R6xACdP1NdPpkDCRWt?#N+ zjGp|Oxf#uw2*$F+H{SV}FyAt~uj4l})@PZ;+H;U|MTs_vlUb@VZ5+#TK*%giWiI37yeSkTz>2+`<7p5 zQKUTX$$clWZj0hM;{e|6;_Bk&>l*@8R%6o#nTbUe#8#h*jspiN0<~K+?g9&?zQFBP z=0 zH8TD3_B?1;#O<6|uy^ofZV><1PMiY+0$rProAe+_R|L~s-!BZyc>8r|82{;F1J^Qo zWTekEFh2r2$Xm&$Z+jsMN$Hx!RWHWYjm|IImv`o~vcFkuw@S||2s!1qw)Ol})Ji8> z+F&iNyn+Sq;BL_nJ>ONLL(|a#m*C33_apbz;(-IeEL1dQ)J~U)3wNQ(qvPf>qztPG zix?+!yj^9*p5&@}TkX77<+30G5(o7`$%k?@H^C7D=grkQ?P9vV*Q4A! z=K(ssDd&ZSfAJF`@Or8A6`gyv3}e1m;;m~6coCeARt76c(}r3;hDi*bPYVfTWS9vb z=!MZjDP5~Z<b1@`OysIWyV$FrYw0M-!TI`Txreo%)ms69UL+S{KM}EtIURGiR^J)ITKn zm+0{4uYkh@@?Z}}EU&tklNk|IjO7H=%I;m1A zsuXwk4)TG&WO*ll`G9_$hbB?J!E?RwR%Fc}tj}AG=7zdV=rLLV$l03*1yO_c9~>H2ogAqzRkZT+D1P@y{hV^-w@c5|wZHQa z^RQq-byhy%AXR8zZfr_JZ!@;*B7(bd;Eq=~%$N8d+g+)V)2f$Zm4BAd(Qe5eV?z-X z?|i_7+wvGfAywqCerfy7~4>Ad|Pn|#(S!UQFhjc5LGw|Z>_ zY989x$ruT{vCqtb^|jOF;7Ypmx0t-p1eu%lSK-mo*d2l(8y8LDkl9#d&cwsrXvqE;U3zzK z4+wwnFnY$G0IS~B?I#0*#fz=jDAKN=8NIvRn|+4tUneh;i(Q2#h6-p<_}#b^wt80o zd?jeAr?FRpc61$I%lZ%oE2J=3EH0>qM&KWA1@oai5uMj4L&C zjC&$E6?`%|JG?TxFv9$`4yk8?&l{bbvI$UgRQ)oW9p1XS&lW8rMJ9awm)Td(Ig$9H z=V;3Q!R^}>ykq=7dei* z+r#klq4%_UPG&W6kU#ZTVh5{le@a;~%z;1@WwAk}KDEyzzNYo`j{gu2UlYfPYjrO> z^j5o09q7yDW2daw=XiL;zP_=;!+?xny87>5lH)gQi^=G|ERRrF4%*U7nQq5l7clYjXIoaucjrHd zrLL&%K*rNRFJfko*P2p3D0v+Hv1^ze=7Yf}@)&u$Vnf;yZ13^)D{J)GxU*EixVm}IrS3Plz)M| zxkHiUB_-p>qOxGRVP626=pTDtiA4rnoaszsAKzwh-~OxDq|U-s=xGM@d8RKW(m`6e zU&Lf;ipJiQC1=6HPZP04v_xoWR~i%oajofo{GI&G%xTu~6@1BVHQn(bWYm#5CG9-A zFqEz5SB{)JQ#@D@{c`ws4zl$raGFql2aI0PS+sb`j<;P0;#s9*=g)PP3Re|b9?^oz zacb^yaj~7??VE`61M!u$;_;T2j<2s;!=|yw0cS0t;HMbz6V+p!?{Sbpm{U_< zt>16r$xe#9-sX7v9~Y*sH@4k+!Nl{d<2&;CQ`?Q+tc$fJ6z}LT5M_sIf0FgQ$5d8z zXGBrtyqe8e;%ToKMh&&-N>oevoN!A-K1Iu_D?Tcsy zgXjplN7?g2S9Aakp_M4Pxo8m)y2u{?Ki%lNe+zZ2Ub3*;nJ_dCotdlGYw_{H&77&~ z{2q@N0<;w^0F5vOwX62;hR=3j?H@zT`=KvcpY?^YqgjAGfk1 znOSmLRrdT9pXfyE+E}oV`q;bZx@=OvO4ruC0#M~S4p%{5_~sB3^aFi^@i~Wor|bZP zKeka`Q|-T4tWVQ~fwsLYpcn`jS!|uE8?zQ0=Eo0*v zR0(P9|42dj9>!1!!&pD{8|@wsSazdcyye-jQwU(-rLk7YLQHILw{=$qknC=rYIU+_ zok&v9mQ?sIP|4FQnDLrr-7b+ZOJ2Va2^&PaJ^ItN+3&XilAX(+W-v&M(CKI>U!Nx} zj+(Yr=Aaz6bIF!U1iCv6T|iS9gEgh|`c6mLffMAiU+C^~T>N21yu8%a>&Y)`pvzp2 zC8g-u_KYD)CQ0!5nh|`TKzV-g{ruA{|SnuXI z=io5s{_^R~=JLH{<8v1~aGYgIn2?Vsw)f02a1)~?zZ{8nf6=M$GJTVOcpnI6fr~Bi z%s%y+dm=&53hbb>A6UH`*4;~8)vy0C3?`njXl4L!KOnRKg2mdQIrW1NtR7aW8_KKV zAe8|5j|19JIvVb~L^o?IE%B={9;Z)pCp8nzV0rA4D`2$lJ5)l}Qd>vTA@;B5`{C#Z zw=&Vea!?QT!P45*6$R@6hw&p;3ca<$&RH~~c`HF1I|W){GY;o9Np#98pA!*m-=+&IaNa6ez_$_jmMn7n>qC$J9Y9;`et?jhBt{*@%EUz(cZxxd)HW z;^w|@9K-i9Rb8jt9hL9NyS;a?Ts-d=uaJT5h@5$=s>V_GD(WvYw!10JkXKv(e)!-E z7=AJ+?f@aAslU0u94%bEflI|NJtlJNbpG2hp}~KN`Z+>0jls`{E1QV>-vqFoz&hy+`(Hq{?C&S~cSB!R z)(3KnV`FMZ>pLW%>Hss9hsQCsmy7)WX0Kn4c~dU*!*#j;EZxG91%a{h$b-z4^Pt!; zkG8cpCSdmh8(6So*Fbm%!h#QY)iFR{4vdOE23rB%yjPSg4wUdTqe^cMNC({ziPE#&b!dD(sq@R^30d)$N(+%NjQlZ)XC zQg8o`Q{vFDp=ZWJpo{RxRKZPk8R?LLJO_l*80GKFU*5h@9(V-En12jH8SqT!b0F-M zWr?#Zjvq|d#ZB^-f0bqTULEv$ohPD*e6ZJ=|MhWfxPHl2^3a6IzXmiOfOGNjQ}*gq z@%TgF_~PV}hl7K|%}+$SmO~>A3pYwZns*MGS~uT?mitJup%hUQ{Y6cL@a}d+d=*{| zKaWcm3xy9G=Crqrg`B;1ZnE47rheO}4;B)b zm`Ijl)&FJZ;21kxALf9!6YTCQb7jVGlf?rn8MBzV#A|^9E>_w)i3our6r(h<9@}6C z0!;fh+kLZVVoy)9`y5To#f&Vw^nah(f)~@{V@~$!?hPdX{-n@)ybcG_8~|y7wJzj( zGc&-+q+hLsflt6yN0vM^$LpkvLa=*1HX z{}L7|jnnV$3@A?r>KP%(F!63ArIbqFy_kX?I$}Q#0;xdFP)KO5pK3$ud0cAMgJsOR zD0L79`*op#d_W6->h|hY%$F}F%5ZwH;pPt+5pUvvoXT+G`44GTbn~Wr9>{d-s7sxG@t_oVdIOmE7bizNs%o6T*NzCI&$uN> z1i?#1gqGQ-#;2HeUaP_IAWA_C#lB@i{z+auZj{6C5Z1lB(<=tBmeivgR&Y4|BG;Cg zHpWY>&=rN}ZwM+Dwz5?$R{>kgGk>+lC$N{drUugO7)U(hPVa!ME>nTD(0`q(Ej<_$Q@8LR3GjGz=q#B)xn)lBme}g{D12eAQH9?SgIP{zflPy9G5a6 zkb7p0I5D2-MPtso>@Rv!Po{iG9lv~-fh&x;K|RCNC#Iwf73E)dXAGj0!9P?N zHG&=rzKb}IC%Wr+ij4(vYdq*Q!`!VWx>@ik;>sI+YttZANyEF&8vhayTld%2qd^z5 zXCb(o57@HsK(188(-&4D3>RLaK4w;X_C7BwSw1xrawH_=#n@ulgGSZZZ26&}I9To; z6u-Mv^S_fhy>l&Y)*cpBptXr$;0whM_5YlC)BOkKM-d&J9=+$3;>5p~-jR44OLq7D z?&#I9c!8+aJp;ijYu??0EBQaQwA+J@vv&F2h)hvT$0rr}KkQKhoHtdpL?7cFjX6*t z3x5Brhm>y%oqvE4CGZn;L1h%7aZzunJxTHTnpJ#GC3g_r(GdqFPs{40YE2)|_+@S$ zejq1d2 zU|lk+k&CxNCqnsb=%+BVTfpn+;b9}g^OX}yP(z9Q|D6RNJOq4cmi$?kKvfW4zaSMj z3sa_HrzHs_a6&eSyJYbG3zZK=mUkYl$0EAhI|uuzS*EwG)i^pTZ~W#OyulA20Jxw3 z_G7T*(a-wLz0C%h7Bx@1e{D#Xkws+BqJzU8{!6scE%p9o(Z|hb61fL>MDDU&S}Stl zWD7Q~eS7(anX2|_Yqi7Yg!y;@uKAE(#3le0{NH>0XjC2d_UtiGhLZT-T#!9frN!~o z02>$%kc@TOhaZwF|C8-oe#v@lz5WXymsbOI!zw4-cEo}u!^SF0#zrK2l6RzsPsf$XOL?VI;tln>VMRYc<(K2k_Rg??mW+&>WJ550jVXS)+PS1i+vvJMs` z6e=*$O>9Yj$e@?p=TN#f7S?kxxsVej9M6I{QE*zWji!x1y&VcDo)yy8+3h;eCs1Cpk2zH&x7$i|orET%Sf&+TS zVwhvHiSt%S+sYbaF7FEG-T&08w|DwsMs>VXLTbdm zf;>UE78)wZMUs`+`+QVW@jq_%|H=&VI%Plf%=41d^e~3-&Bl+q@Zr<7O%g`Zt-knx zuW4TbJ6UkZ)dzig{(nl^MFocs!Ggfs_h7%BX?1jn@91$1@w($W;AR0E_(o%FM{0iR z=C^;8zG=XZ1<2;#V9&Wj%5-cRuj(q)iGd?s!XOP$;cN^vY`(mjI^?p+cvQJKbhf#lFx@sm;DbS|SzS2WBc*O_#MhGo>A{2AR z$>3S2G3Tp`B9*^5R2ufTSIo3SaKKz5SsjRzpN##4MfERcypF^31KxGg*G5fgb&|Fdyb-vw6wGiB*u3Ri{}vwW92lQ#qSZ7@8R{R>3e8ORW9qY=2NHUi_;Lo0^!ilUkp^NDUT z5|&r=lHu=peLO7Y+toOBdQ6%tSI=jfeb1gM?XwzsinW|{$`7f8k@ZKeRxonjO6nFcCN zj6Mc+{6zGT%(VS(dY)n`@2t8)FX@p`25IEa-luGTUNcK+Rywnz_Yw7&Gg-7P&2z5TddAo zT=L6niAPTeE zIcM*^)@S|v1q9EIjb=e&cu?gczzkEe3Gn>(vk4w>Y{bEVrO?{gFhf#WUo*&(TC!2%J~0wDKAf0P*=PaUz(#ye20Tq*dRt^ky}$g zQPZ_hqJfaZm%yZBPD^cWdY6b0h8p!QnD-R}wTy$Ri8@mj8{xob)8*13`#uH)k(|B^ zMtfP<`$B`@C~7JZh4KY+LDiT1V4(2;XA?+<1ywK2D5y(UUSp%cfW>-=#cd1#Sn7;n_<&6aXc)3HLb!jJ`|F=tNyMLCKaM^qJgMk) zJ2Zock}Xs)ZR}ZpesX`fetr^TQ99Zs9PRI+)b1)@z_hrykGer9N*kv4gI1%B-1tX_ zpH5LKoaYzYlT|?N&=qjCx?Y)~TDTPRpf=4>Bfy()HKVs-(#DAw*na74_vV{NRFS_u zaC1BijFK2SAj!Wn6IVM^9}&QWA<)o| z^);ZK$Cw02S-LxA2|w0rGKU#9*@F69nc&ERFvI{Sf>T}- z-|Qamuc`7ZQ3~20Mw%l^OL_e~OHkl>Vf^0NvLhP2iy0^+aRNxEa!<++0}R>rm!l&I z8x^lC4Di62C8?QsC7Ath;dJ;qj3pzFyT2me-BCNB=dxKY5CQF3i8XaTZs-TH(kt@4 zZs|P3w|z{N-xRpJbDv|4q*8>4tezHi&inHT(YNj}9&+$vz!A zm>45rC3ROJibz>H;Nb;k6ElDo1`Nwto94uGpPsMlJm>Mm{?`vJvwD7g^w4!XfIhbU z2&zmlcp!Zl2tVH$`40NtJ3V8J?#v_!?i{IyGdZXusI)*C@8tCeRX&z(?U*MLJk^!g zP#+tl5FFUZ{9PQ=YlXAaJA0ko9ph*rzW5yg+YqL}w`9p2OS!bqjDXChN^~*_ekV#GuHFm|J0<^c*Xa(;0t}jh zi#r5o>^66Y?%kD&b!WJO>^hNDdcXJmsJetg!& z{V@Bxd!F_$Sr$C}HQ^fOriGAN-*&vE4nZw%--UJ~d3feKZ zQQU{Jw>K6l-7&>dJE<h!ftg?u>+ z2_Ck4#32>}EKSOj%4eIsa&5lXFfT_Ag;D3%e$yXZ_tgg}ZgtBAVdORQKAEVxtXP5U z>v$@OF?<**b}qt2{W|Xv;z_N|M77+gVDMnD))uTw*2an*(%NjJp#+$c)_^nqbz4}j zEF*8#Rvq56)nRt@yv?GK=19`?9jx5UEbB+s2%~VmlE~E5kAx#Sl#$O%XARo<5Apzt&~|3_gnnELo(*7e^nx4iy0PPQS0v&@?7=r z9bbiyJ7Y{4uFrq#r(Yw4J0~@l@4AAnhB|oszF%%2$reB{>{-KxLHM)_(_0%hoBqFC14H#04?P{3sK0E;OZ06R#e?QB8 zl414IgfAI1ERt;aKcV;NP%VmVnjjiv&Mt8#Sau8BS*2(Gq z8`bq_e8MEP3>nTo&ybsV_%ZPX*;z5@NCow6fp=LQa>IJ%m^XbK-yT%Jo(THHS~`-? zVk<@c?&dv$c(D1TyOfTXWq`CGVK1^yMNSh{MMTJo@$x%7j6b?#7roK-yY{I4eie>w z_iv;ijF=-XQ>h&>!3OXHYJXdi^b}|%!OTi(N*YyIXLC|7=FPBmbZS~Jc4HDhkrr~0 z@MKB#Pd(-IA|z?9^j{G_yeQ$E)6d4s?KjdC6vyHd5fMU{SkLHbYgYF)<`C_KMME0b6$d4GU< zT7z#d2Q4eI$+z+U+G&T@|ANw%rMq7^xddGH;j#7O<!o1$sx_}}c1Gkda0zNY&%h}q_}JCJ3*8JCM?xXE;-tr!Rq98mnkDIolkg*S5Li+&_=u`BR$f$-ndpM} zr|!Bum=z}zBc3xeR60cw%(X?TB3Gl2&aQpiKn;N<6%Q@fM+CBD8NIjnN%|>)irw@N z7m=_nfWG7RmH31(7|R`rq|0sbk~F=BX&NFTGd)ey~JL=@n-#23;J1g z8p=nb)S>R1OE3Rc&_$doYf|@B`qWOB7|U=e&=C#*`z_`buwA-__V&_| zQ<8u95<-aZT)(U#CpyVfw+y^ANvT1WYWX&n_x+sPc19;nE^MO-yHIZGQQSH`mX=k* zk(xf|#omdDMGnzz?LvKyWFUd+E?`me{`x7b%BbT-JHmKe?vJ=LsO~0i5g2$IZ|Dt2 zYZs36iAsTRZFyC_sdur=yjDmvX{)vU)8$0npXv7 zODErjhy1)d$rdWC-rDW;5gbv7FNZ?3*cxCNW8IwZ;A;c~p9RLNOAh{yW zYb4OCb+9etUHtK#^Vcg?{XA9Yq{6*?jv;=8mEdQpd=@5u9#MI-d0A?^@j_3c&B-Jl z=%F#Q%p8ANDX*Mj#)^2#o&>WLsz6>~rifl_%$)=af?puF=z~n2a71Lr{h0ml`Qg)X z_NN`Yx0GMtuj#j@Jv!ix&3^r?zp!+8T*0n!(_WN9>O2eV?(s{cs=;O*lFnYW0TNYH zVN#R-<*a}`N2yeM(U2_LjDW^U%lkuGr_{}QP&UFqO_8u)jX|aJ=g$#UMw6g5=M7Z( zDB9WpBA!&75RsW&AEelHPGz(5wB&;Z(LhnXt8`9{F;+k4l?+lGn5 zj3PTDW1gP*TR>7!K~0t0W(r{i;hR*&7g5SfJ|9Syy15~i17kjc#a?q^NwBe-UOaz! zD{3cA6Z_hre{BSV^=3EfOrBN|LbT4_7|&PXB&b~W^GdXUB$j~2IDq&XAUIuI0@Cd6 z(#H211|I)w=DxifN!fWEXg-?LC_z*ZxDdOBX*y9QilOzWm%A_!N~`h_ZH(fJ4j9PB zU9Lz$Zoo%wtxB`!ufVLt82>`jPQ1xJQ7kZA_e<3>|QL-3zjswORlyjI7GUrTY`&MIra7bA^%ts z^W$Um`Jt6Pz7}b|@fxfXc<;;MEGX-tXZTj|kG3@TGT2J1J(nAn%6*ldlUXe!aoX!mD;cuyI(a{jA` zH%ZUXq(fx%-BKOWCP0x%$A8z?YVb^{*HP44+KYqLbHNA^@2$NGkcYkz4EF3)F2gqN z{LVsFDiB)aAIyzqNd}Z|038aFr1mdchHA=H$?#-n^jJx1AEPkRDASz06MCRhylro? z+NgtM% z{OUES>Q~>!l>%C^3VywcvGOPDhe}-jN+{iBz2zpS(yHGwdzG&vG*Oie+>!d?c{-hb zQJEV$*6R~X#Cm*Z^obQ+1$QiHU64dJiT*IJ)07~$+hO)4j@v#MtLLqBA;1`;YCA!r;6 zzR6gozBy?k>*l}KMM+S@9(Y9yZx2(<`wk+p#FzNW!K<*HaQ2M>oH%1$%f7Qf-`_P3 zH}~rom(6O%M_^7cb+zVjbB|$T+w$^2%424vq4m%n9AAWe?9)0b1 zD!b=jM3kzZubo{)aY!LZ^{DEfaXL&1&OS)PPK7ct!tj~MRk1y^8xztfzQ*KfMy~C6 zJ5-k}AEw+GFzpctA}$mGHtEuPsKydaeCWKXI?hrIv;~wv$5vKoFK9 z1N#mRNC40-6ttDB5i}A@#+}fXt534}YPk!&&oNv|De(N1*ry)sSBCXsxP!d@z|;4H z8OWOuIpydo>iaUvS(KH+a#Bh^X@#%vyXt|cDq_sw7hW?^?w&clL~a~AJqH7IyQ>gj z)s~(n7nV%-QG7aUMne-Ye2wWm%IMuNgMsVJ^p}|9>9_)oE(;r(1vo-}d)nT)Knn0j z;_d*tI2`zAB4y27y+J;kqaB8UfnhcqniqEhA_6k(a``fI>J*vVC3)@rLxL-Xll|H$*3a!7Z0|Om{x;3)At1n% zu5+sCIft5JTl2}D&uVkNEdl|FIj<``=7Dm+OY zD4k@fQ#s_|0%j1M&wJ}Mc;P77a(=MF^-+TI=$ohB1+uXAgGbBhJ<05UQY_y`Uj5a7 z8Hhl7sott&pRQvS-OkzxH0u|9Y;u_81vu4u4p89w8#Y?8LB0wlG~j7}skxYZ6ma-W z)6|NmYrIsUlm>)gFAA=4^w|UQmw+08=>$_K&|kD(~j@!$c__8{s4e7Lo9D^@5Pn% z{--q#5Y}>bcWDRE%Apw)rQ2P*|FvofEs7r|N$hkG_!IHLQYWB{{O1U3caDWzY!!?M z+ucV5lB%NqPYzy~Y0iztwe(uvkDMO$QWivBTYRK}@aJrDMJs?9EVGcG=+**&q7)#| zxodH4Et7@2K4NwCv~`0lW4qFl~HSX6&^v;zGJk zbo0ez6KQ#QX^Q99S-fgvv8*s+Eac{f#h3jt!2l(TXG?ki?aPE_&3 zDX1Ohf)q-sr-=lTZVqP-K;;W}5rbr-ZxuFhZLc~vQ1wbz7#XBD0&HJDcmXFStbb(9 z&bk1}uxe6U&37)=#b)jT0=Y9dXpB4_3VqLP~4--Sp!al2dI0j3+rcrSYW3IT^awkju=^gc+a} zlP_Thn8>@Q|4Qx8iC_!$D2*m}*=@tgug43Q6Cw+H1cN=QmzKx6R}`Nc8pnR%AAa>% z;M@i^fET(01yxGI&5F;P5wU>vxd2v*AA_^HNe2aQkXcmsGHQ;V(>ea z=gIQcWwetPW;w)%ChP~~wQBYkYadURe7{bkm2HF>QIC9nb|-gbk?SYx&{qq6)!Q(^ z5d6yVVBUrm_LsBV$sNo4-aAE+kr;FMl9yaC%ePak`UEKe24GTJGJbIsWYD7|C{G>! z-oIJ~5wFCUh&uEM$2s+qB?@w=Zz_Wjg#Ubouj~3}qwWTs?@n_6tky>;YzF&*ic9SM z-5U%ImHS4eGbSc4B_-oI5AH%@=rH`M<3ftw61*~NolU`*RQNc4A=BCIJ=WhZT;GA& z5>rF)r-2nXij|!{fu6kB(+%{+TClZ6K6?NRelfur(2-$A7L?T*ZboAS8oc~4T|I4j z%cy@NdPopj7)RP`CYuRHE?AiWttvc6HPiJU6(p0|6O7t2cC}@tb$D#Y$;&BG?d|`I zzWiT0d73t`|B->m7Nn8o%+icUY>^C!=@SwJYRFrdX3jX1Xqi~5vt%H^@paGV*Xyr0 zX=;0p$b8_|>xzZIRJwFNHt^V>hGs^EO;w)2$5^o-_EOde%XEtMc?kD_5O2^He>pWl zKNV1Cfkf)l*RR2IUdL?gKAi~cD2fu+%dGl;0^7VXhE=BIXd^Llct!uh=uL&RDtKDj zh+ROQ%09^;s|T!Kshn2k(4F`sq+jVHHTwu`U`dk(L9*@nxJFZJS(NUa>%p9U%+o8GCXfN*Fmps>KDluIiXI(-!20@tb&UIl= zS_7yts0s)P{;$?y62pf0Ma{jpln9cTxYPE^38Z)6h2yz9u+m_In3hrXG-GT`x8Xn- z12DE-T`^ZZUmoGTL{&UIfv-!>P7^D4Q$9@^W};Te2Vy_QMA<}CWEnyTzgIBqTHX4` z>0Zac1%@lp`9WE6qWe;N7tU8>He-Y-;e!KTyTwj$2}LD6VwWK9gqrxh!stz7E?X}@#IcGgJ7n# z7ry=2BocUsaB)ixvcN#sXLJrV?+h6X{sJ#3EmV!$4fBc>4&iICaSt||T-jl`;ocV_ z$=XhTjXo`60F)x#^T?e^@5;Y>eZTP}5^g>QcAsCiDa35g3C+0VuYA!=hgivMJ-nX>GGBKh2>FE!+2+Cc~6*q@< zkz_;Y_WlE5!svGi-+e$`QCSM?Ej2Q(2{IpU5HOPE4ga7P9-q_(w+T*z{{8vwX z1qM2mIW$K3rBY?huHO8=gt!msNS?#~c(K|2Yuh6Kyv(b}szaI*z90mqSPa$Nh?WxI z|4`e%A6@+nWUYB_c z8E^5o>(-+<8SM|gS8P>9#0Q~6O$9pUa}Kkn3#&dvntiNB2_l$ z?=G&ZDf|V8{Lnsf%hjG|dDb%t&~HqK5?>{c0>~pvjc}RL`2afaLDprRUC-=9VStzjH$|P-0O_4LrK^yWSE{o{dVBnhscqe;O2)ZXsGa zWH0k>hp|t}5!>Is`As~TWwZejXm2({7R;}o&oDrc%d}&C^OlivT!LBzaqmt*l;36g zXqRl!)D+BS4`nHRC7dAA@`iBJ8FQOQFdg~(bu_v{G&_{W15c-bgg!Qz3)n8Qq}(8D zx<#*c0d8(*hff1t%7fj@W?)GJ0^r&_&R|23LZH`x|K#z!DH8rUM?B@HS&Y9N;)H&_ zT`c2d4eHR+z6oVxW+1xkrC6eqEH@LZg}>K2v9jmrQJ41w(ud^#(ymkZG4iRi-7=AE zNPHevs}aTR@up7-#FI#LUUD%8*4D7Ctcq^H@qNnqe9rl@%?3y8O-_L(Kh;0*KL~gc z2&aGV7o+sH@C#Ms>zo^v%*LtX%j!XhF?U8b&mlIXm|ceIDN&{kL2JH)kd)~$=CjJA zVjX^*2QgZP`!(<}XwF-lR9Xk2Yz(;(9k+`kSqFEN%JRC%UB>R~KfAt^oficijrevL`paw8wxnwYK)o%`t$x z0mP0SG|g@6x2#dREFm$mgwiF2mx~se74hg8$YYATs&7gdb+gDkjT$53HI#GQYYO91 zmmr$;OK8iG_0z|~d~#mWL-)8O_7L>zmohSb7O&0c78hI^>7K3{efyu!d>=Yb|15b9 zS;g+f9%v^DXvDczov`f2=(N4A3~7@gg@S_q#h!R|g_)roqNc_4=m_DzV-g{}R3>Mw zm^;WxEaXh7cSGJ_aX^Alt{&o^m=Wn^bSsohF|**DGp+^3nPp%|DtVN)b^tFe)KPBa zaUSQ>tcYFLc6Y~X#Sgta(d1UrmyCZ*NDHy2hjd<5a*Ab)aby@ioHjzNvaHQ_Bl7U&ggS)Ia5#~A{Uo)&pBM^hPT%+F_F~B@-csK zz}L`>ueXN?0Av_O_0=QlWu^Z7!3QNKjaoo(u6)|!B^BRjedUD!!{EK8M54p7i}-qB zgB+T5uBj6@PyTh(H6c^`oe=*37jpJC@STA3^d;j~$L>Xy@!~=N#E`~DcZmF>(f}l+ zPJrz)jpH1`lrW~23z0z{-(}q zBhB2u@wpY9fEmbJotU<{p8ETn82#R3VzZFbP) zawtK(8xH;L3iV7TwZ|fMjw^_000Wsn#~ zzfZ8~N9Qdfg-tsih~Z3V3bO#bjgdyEG)TB$WWDWz?eWZ2j#6t3hmoUZec-fH@lkxj zEx^p4wJ~$@#gYSgTOx0}%aS?IzQc5Z+_h>@Bs{HQvw!=^#hcERHJwDh;-;?D9t)r; z+W$@hIw>m5qj z90+eiws?n*ZOBbK&?kb}@B>K+rNneS@wXvdqaUu+CleG8LUQ%IO4W`E}v8)L(t z#n~2dvQ*#lhkXMCE-nONy<^H?Z#Z;ttq>IL!2-|Jc(m9Cs9n0iayk1b@-YIr29IG! zrTYCZQXQwb4(lvpx{HGOir3ksMD3Jv%t}8RUqE~oNEQeWI;<=@p|no>Ta>Vt|I$hN z@F!R<-k^B=%7ISWyoF3H{*Av9(;mkkj>aR%IUDNYH%fz9>(WI#K~E1Og&I~7K_Deh zF6K*&=TVEPZ)&fHWeYgdypp(m|1v=EOnXHVman>TWDgjNkMKXam&1TyQ!sC%#pt5Y z4ALh;Fwp$5O;&IhM0B74hxVxlZC8mA`o6NEH(eFLhU(uVK#=>to~ zpG?B`kE|hwo#KDC>TADuv51secPQMl1)<_HPk{Xh83K$YgSWRl|L#SA+!V}D!l0?{ z)io34Y#wI=+GFJMr~}en64&UPDf;JA5vkwA4Sr#~#$bP&f*3P&gpOk^8x`V8eJ3Z) zE*{S>lVmgY4(3Faq~_lHWO{@L9eh@Db7<>$|hzW0De@AD0)!5tfWOFc8_!yMVw zzTiG?s8>CE;WF&5=1AWHVHV+qAovwa4O4XyS4IDj>HZraEy*I-OGo4Aw$$hki1$S~ zMP9KQyy;W`mkzo5NsGHXpGU2&#GvYw;;$NfJN<+Tq)-ubL^#*hk5>W1cUQL(Sp_z87 zSHQqpE0I{WVAtK!_>I@!FG9#8xgTTeGE>nJ1??@F+fEXo%raSRn7)&l8Ap^%X0wyr zHYzhVa^vhDRrR$kp?zFn8PKeDRB(zzSHbu zZfWb0Rl~ulIK73OdcT27D$N_Tt+w$d!d-+QpSAvm6Ni9l5cT96D*b$-I#tr^-ucIz zezn+dM@%AHrh&xOpGu>%2Z*_0Vi1jc4t%S)G9!N&a0vXIWTK^c_c#)f&LVXhXB8}= z)~P)hkt{|E0II%S-I}4g8B@XiuaI|V2YC0?hg)S>riARvd{Qg2%)kLh&{2}&~+J4zFImPH#jg3H(Ngvt}s zOLgP|nq(k8vDPx6l6})h)eIjY)?7n277Yix%VQZ`Vz{h*mvvGp(g6+-s%i7Q;Y{p-ompw#c|B!_ zbbT{YfN0qXjQ|FdhZ_5WsFb zYZ6${(DXslB~CL#HqlSZ&lfTxBgI@fxS#WHLMb7WB2=b4D6A2wF#RZ8Z%Fix373yC z-#s%8f&zl%>%%6-(;cTT=p6wEg~;y;LW7-RL!WVz+UB|YOTLuL#-^l>wJ+k-kD0k| zx^53(#-mWGWn!2Xa-@X!WQ{9`paH!i=~?QuX>+zwSfG10Tcn~>v5+!p2`|u&B`h+{ zP0fMiKq($jTZ)fqYx%%Po>zr$Nt2TBoBU}NPR&p#@SFsyNI2d7(;I91o%~R_Op;Yh zkEoh^?kZzRtJwagu`d;;VMiC1SVwo~(~;3xFP4+Dt7|AgJ%FM4Q0VMPEWb4_o{NqT z>?;b_eC^>-l7EY1w393Qm-1$_NAA-HZc~5jY&e|{EVY}9d)^?6PZ4vsd9skOVJoMq zSv$zDg)~*VMLu7wU_u@y*yZ6BNZ;{7OvV2FhQsEANP+#Q?k&Y{U9?Y9r=J_SZQ0|* zrg-vVKR*g7ozPuq=S6UvDv40od;EdpB8>(Jn6T0Ju#gA@8;F0)$5n<>%~WDyRqbVG zCmM8RoN431xq&CYR`qbO<2xTe54b9J1rcW!CtNAb=#^~j3RKF;yllG3*1uNz!Q$sKG+WEbggV7TVHD z+gjfzPA)N4n~($Yk=GL6lkGo|QY!2J`&3RZH44b4 zod{Ew+eZi?ev4W~AO_vA$r7&l4)F`LKil8(&4n3qiRgBQ8me$7#tqbzzDii@y^rfb z7Q91QL&8WUP|6gbMq@@-FR4>eu!|LK{pkI24y?DL`0v6}_WZO~S2Og~{imChvw8rM z)Qo+z=tm73M;W1m&X*x6owUZAJU`v9Vf>VKIjizf#D7(_d9o&jM&$OH{^)Y$?ms?e z|NGbRb;bk$lfN;ok`cmxr+V^?p_$`E4TG`$<`OHH^b&KPg!;n7buG}a%R5A?EC}Ff z^7242lCiaQi!d>n^>W1p&CP0lso|jTx_>q8U*7LNdpCsC!}^h2q(|UCV8<}NtMVsDbFlAW%0l-`=zULkER1{e zv=(5@ls_3O!Gt&1yw|l-*Y~B|lHUUlt24k6SrJVF#OQlaUcgQo8NBqPt%{h^;Ultt zU>ZerpmV@3$wfB!H+zsfOH^W1F4i4ZL2;r@;WSZ3GB>N>(!Z-S#>)Vf(0&;*;F4wpq zH6RKxp<_y`t&H?(;?xT_`9<~hUG%K*7cv+{lFVYXvhbhD4AB1n!j|m)P?(-cqM5+o zfO#}-0wsVu$8PJjqDyix0^g@E<92HPT4X8Lhtx^pH%-~Lzs1a;U}v43HG4?_t}^T# zh!$(>J~(*YvdWQ#cf6_rk@Hq|n$Sf3+#z${9%g#?P#;KfPIc-oA=O)?L$m+DwpF4R zZw&I0+*)&sArKIF(&+B~l9X;P)NQMo&CFW9z%UY?%f}3pV8Ciil0OPlN*wlqYnXJ! z$Hyz3^tgq~d2%5jY>ssE3nY>EsSYzBaL*zYMHEl7zsJwqrw);Qwg(*K;H=#SF)(OP zAk|}Iu8c@PLH?j+X@)tc|s2BZA`Piz+2jsp&>m~@ZtNJV078|UTA%DD++Qf ziUMv(!%jI-WC9yLSK2Hu^^co^k~g$H@9+>{>W$ow4?ujg9%0Gf&~|!6D^xuTta+Wn%_jXsGO_#YL}}=#nQ*rux+OIn*Kmt zdLGCFmNyiOCstI_==#mLWqKS5s3vvxyx1h$uN1v1wP9EFOID^L_}J)Rd8t)HA||Fm z1DRtdkIOrRZz?KNNA3Y(y?Wear#rT`3I}HmRvOPfSyvlBj(;rN=ml!zH`Za{@~Cvm zOufG_-!>o_u%PTNl8*jgk>P|juhzUgZ5?@ZWalF!(!Y94QCKUu8^I9n|7T}lF-HgU>!O_eVXK3j}cD?)^uR$KjuVUOgQrj zbWl|ah=~nii9Ln--ZwsDfDa5(PBsLdn@;GBpqrwL*xV5tIVU@*9)tDVTL$}mnl=A@ zZjf9~PVk-9Q8@bLSU+{(R(xpI_5QdZw zZi{dCQm{m}V1Z%s`Hj=s#`@DS2)7Uchb~xb!vykxK-xIVc6X(*=SlIFj-UqdvUI+) z>+E78g$L738WYF38x~J{2?a)sF6We-1v0QBn(~qYyANs+mDme#fD2J`hpQ2-rhu$# z1$k&+s6}i{r)R4EUsh>s-JL;WS9Le3$cew`@b{E9CWWr=x3igz^MRy9x|SOTK{J9H zmC~AOQ4wsDCgl>D7Q~n#0={nFIv;lA9+IEWjiyfAr~m%mcN+>RDD`nB|F=d57h^@j zgi~9(ZY+BwCFDSDEBOy@%FjcV$B1qxlSQF$)&@Y-0B{GOO>$o;lxrQ4rfhg_!7VBNR-LnU~5Yn4ie19Qc`AB^I6_8|1hF;zH2E-)Dwa$JcXdu05 z#$KpaJz?Fzl%_tHc^tHqgS})Nu3|<7;+2L4Wk6m7;InGI$`460@(*};zRV@>i(#4B zIB9-`MRDyT#G6@1)=Ru;3ELgU{8SzF_y?11~`D?GlT!^0#SQUAM~K0lJ@- zQGM>Ra8uN`PNL7n_1Kq+>2EP`+42b`D9KaqA4TWO`1B&uuNx(5UfV)MT;;xuo6gF1 z@+%RvUL}B*e5dyc?c*)6E#2Xi@Z@4d>ngw>c#e6_YTjhKvPRAO;<1g){N&)Ka9eiI;bg*IO%`l2t*@#q|ZyxNz0 z-ySCi0FG_IuxZ-?%#Y+%l2sI3hHX!~?l1TaP0Y}rPgHaSGoDi$4D!9f5lXjgf8_Rt z?Xqf&!=0cQ%3j75ezlq8YOI5H2Cpb#2T1>UiI8VeZi9rtJrJ^THuy#WjAOFbEe_uL ze>#3(Ht9{Rf8`WR8NDVqechFfNodZWofLSZ#vE!aKZ|`1`|nO!ZeAjznN|*Y23y zBdCWS&^+;?|tePeIdO7g6Vd= z89C*LhFZr)yh>e70%IHW`LfnDrSf@n_|E(}hSEs~J>jZO&*h+AU4Jf5Wz%9Ka}HBF zET-_7;rGKX4=s4{FcHoCJp0$J@Q|cQskoBXRva4?MRUdk%lRPher&NWlr3>{pRhxt z*TZ+r7Fi@(T%jSYP^S|SrY6`nSek_Vp|f;skAJ7+ZX*z=QosQ5xfs|3!Sw6wZrrZz zb`!9SR9-J0(T%e6IKl}3WU^B{E%{*}`?f)zHFwf~4B20hHHA#|A_E1Yujt5)g{YgE zv-%fcsm-|d%|x@nNL{truB?mbT8ftsTj~7=Y572ZNUug&QXsmo)|aG{R#0%A%pVE~ z-YQhNEmy0NGf(F7D1l_^q7}bujzzrf&t&4_;{X2ZWTei1JNvVJ97FS&aXdNmN@h)v zlN8GG_wQe4Z}$+u-q^8~$??Ok2m!ieu0Q0v?E!fGor$E!t|}(T!6=q7uVAT4R*<%Z zd#MK~YVVB1L(smGV9pbRcY&OOa_eD9Ff$_y=uiO-SR>>0q~M=P&jf-3>Iq@v7>SV? zCY}6Ggq?L?*msROlbrNOvp&m&c`Y>W#G4D&U#6yHvYLKKbaB>{nXpgS93l+>7md7a zI*%BP$G?t|K)sMVz5~wyE?35-p3cf_Xa5DFL{6c(oEvJ6*Vcb6J@SJ@<0SfqwnyVu zSl>#jrF}3Duap@xf-`Q>s#@qDK6`E0`Bet9xGFOq(RfiR^P*j*l@=CSk1G5|{$FMp zGh&qAm?|oSw)jJ#!dYg5=cOuMoCS%{F+Tvo-aW#!tg`4G^V=uUD8jxm>OEL<;@IBT zRu7mqvWfay1+F|Do-2~EkGuNIRT}y)N_Tz1bFFV$nE+q=yV=i03nz9LZGi-s+dg|+ z@BL)xWPCTp+p)0jwFeYYDV&4=0p8R~W-?LWD}vn?k0;x;m6bvk&})9P{o{^0t;=KQ z9@4iF93_j-NEkAgzYc%8-7dv}pao=!Tm5M1V&1_(0Svlhzda`=(IDg-ZL}IaVsslf zZC8s{|JX_2_wRP>`92@$l@g-xSlo?8_PBieB|h&jj2BB<;j;~6<_Bs7iB6;w;nJGE z=`)xstgETLvDDx2OmpNGjG|+5w*_qyx?@@5WXy~Gh z87a_v1mRtwUO%{CP*TSQB5bf84WrdlY7x6q#+%XcD6*tniGNySFM1%>$Ts4&d|a7v zgKW6ld}iUzb$YCQ9jC?vG8aI;4l2LzIA{r4_x+u{_X)=LSZDDvvr9|&A=JzuZ})VF z{`_MA1e19P4_Lc~EcpcI>cE|*y;?F??1}8hWYfH@Q%}Mg9P(rlh&zcS55xAwq(ZJK z7P=$`%YAiy{o-g{s4@h{DB7PBs_fNorM_n9+;({+J)vW)q`@JtSRBpDRg9n3)$U2i zR}MjE(;habYQw5efEWs-GnLs(gI*uA znSs#JuhhRKt09ffRYN@APz9@g_q3~t6Ym*-H3kmZ3m!g8+(jL9v4;}Y>5%{t2Uwq> z&9fWiq9P_R8QqKNjEtcE!5PKvaG<@)bV3a2a(Rrs@rlGZ`RRi776~>zPM%Cu^4kQr zLh9;)8YN&Wj9ZNH(05^-=d-YUsJ@=Hm*{~_+kdYi>Nu%TV|U$10lBfP2y?z+{*XGp z9r(}w9vwVc?gMGQvNXH@qB);&b|CWz ze<`vRNGfI7@GW5AY2E%0F%DR^Pb2=YV-=lyZ1XhYazeSzR+jap$L!s)ca zS4--u9sGhfK-6qk-A?c>`^4zYk(nk0t6*J8WJ2=&Rt^J-c+Rh*$9(sV0_kIraHDm- zwSD_ntowVQvp?Sc_XpX^im4ku(SqcaTw!;l5H1=g1KpXeeE!|_G+n%lRbO$M5{9^I zFfU^;7vl29K`F$B15BG*{&fC4a0LThQ&D(Im&H-Ch#q(ox+nT4u@932P8^dTyvMo5?v;b&%S3%EGFwx|hTp^B1_qHu{^5}bF+80?F;L4w+exbe(co*94Y_1R}2 z>6ySP2?7k>n#|QeAk`=SNXAP{-640ovDdyzhyPN?I@c&GxcLq}zRckL#N@+(b~u{% zK!2Yv1ZK^*?eIZ9)RYe)+b89XmRi{P1q6tIzwoEWNYOUXa^8v4u6FZ+;|#|*sI9Co zf$?YJD{$Djtu0@IT46)DhA{A8ptOD5as&vlGr`QM!XVh39vQWyOoo->XZJXYEdlF}ky zyQLfHF6ol)E3b_eV}1> zH>N}U(3j#q-27k-6OV}w`143dE}oxs{rAW(xgyn#o?C zGk6lzvUsWMjoYb@a<7R-d9SH4h4#TD-o;~T&@3749^61z-mbeV+Yo8RY}{nwlSMBV zOj0b#k^j@F;6UTkyUIr_>lM3_iay9;bNPhw+*pKz)Cx7h}O zN|9Y{Op}o%2}mCO^ZM;Z9t;fjlvCS&S_shn!An|AX>C4)e|Wxo=cQ?d88k*@XJ>Ex z{mW9Ek*oL(NcGdksOZk%=&buDM))Mb$5>jR+%#1IrKAzQyOtDVBtqj~3+2*@=*Z!d zL^0t?R__6)vS%AWsZ}Z1m=Bn+iB&G3=&>KNnnKKmCbj#nk{^}^+8T#?2~m&bD)EPq zaE2&fAR_Pnj68*&BCKlHy4%sIn|3UKwkc$wcR8jv>XMBT^<82;u}&`$ft8tf{m;WU z``(N44u~)v9>)Lg(lgF5cs{f6Cs~Z>SF1`fPU#!y(AXkmAs4L?4jlALr~GVxM$4Bo zTN8+&u%ilX$FSzpE&w~kG%ycCp=yy4*8-yL3@@p6iaHY(Or?MB+lp zX!SAag^>KSrR@|o%syJY?o7WbO(Pj>Pyhg92!oiRQF?h+^)Ia(B5~IBEVq9RNFf5P5T9K9!e)NIIzKHN z2`{v2ZbB$*E7N&zhcSKKML1}G93lP)AZ4d9df3I;tF3$vzqXXsyPf<|0K+iL1BrO{ z{d`Vn2MiMh1&|{mKJp3U0n;*-3uZAU5;OD_cQQ*DDNq78Dt>&-63cD$nAqo|>m_~7n`3vRJ!Z3CgHknBr_5OTGddx`&G5>)}p%AfDy!xv+ShN)(KWugo|a>q z=zy_i?;JrMw>VuX#TLx}%<&)w5ArbWFoB9?x*|vj?s}iQuV%L$9xNA<%O$Qnk)NT3 zc%_^kNRqb}x23UvAGwj?6t&qF4ca%xP)2==#p2QdwB6^^YhOM-q2Awz$X~yLIe}=? zj?Gwg(X(9M#`R&OIJ)6~;7lExoun|2ajK2d;mPe_;{(N+>?J=41yea6gkq_hC0KZ~ zLEM_2*OPcuZAAzwO&Zy&Y$%__iiXs?w|#^2to~RC?m=Ih@IYDfA-FsyU2{4_F-B0b zgacSxu%oH+s&QYIe$qW}u{;8~)%RxgZg!|KGf2QEx@hdt4zOd6tPBI;X$1ZO@1|5wK7N zHDxmTnFFyQ)#ImzEkr_p13|MwiJyHe@ivrw>+c_Y*s3o!QM0=K`iQlD7hln|c+~}I z5PF}h{sIL_SXC)~mh{>Qp5;`@48s!qf6nxM0$g|grm^D~Z^P&IeRhQdMzHF{>OoB` z!*}__BD!yS>LoZ3kLa`FEFnxooPBQ0P6`TZK}&`iUAs*~_q*HM_of`*C!nWZ(-GKMvB~l1(P1@Y!Rl?aH0QY~*sN%2(~4$7;mEBBDLzCI4v? zMuE1B5o6P7+g2E`Uw`{+y~s8;IZXN-T1F)wQL1b3N05fi#p$SLu!xz@Y2J7A;zycN^*z_GS^kFku!$J@LqTwxmN=6;tqG9F4vQ!JmJr)1s0T{f71J@{>EPfHA0A63`8r8=tmQKp}0vQaX zEaGgGtezOYLAqELiX+<4_h4RV{hRx^S_;BH?lGHV8>X?>zcc9oLrIx+0SDO+10BL+ zw9VJs;rH)rvwtF$d94TQ{dOXHhQSW@>nE zTGJ%gXaDn1>YdYX(1oA(eb+Gg1tF-R9#1N`;p3wyz<@KpsW9||qn%PNw<|c7h7Gwi z&b6`H>8VzWJgF`G9a45PW>HZ+0+=oEqYpqtjI4qj)W2;+hFj-%zG|>1l1-}h(|ydL zf+C7-&3z37;NSu(ilMP~+-wXKrp0|^)g((Fq)lhQs3wRW4 z1YvLwz6G*3WQJu%IKC)%47yU3gpg*usiH!|StkgVxsH^0e!H(nT0_9cr&1H9!_(4k zDa{Ol0d|mnIpUCx7h~tkUMfz({IbXnGPjk7_5W6 zFt$8|6Tz*jR6J(Od0)1gT86mHy=gO#L{%CV2uGDYA z)rvB9`k9VOmRw(WIKc>~@{g{`B5jHJVc8+yTevkoqtFPPAN~sPuCDA-LVEv&!7dVI zNX!PfGg|ER8t+uO!kkHERh-q!pQFZjQ0|E3iOd_T*Bm577v?iql&+SAY5xn7d62f zNqpmj5id{WRAv+;c>|8p!C+X}59IsI2j6#lg%k}3k9ZfStwxWDHBO?me@o+uYI~m@ zTwc9Q8{=%NWcd2b^t$cpG$TkAE+pWOGJ6ZkB4s+Y#_tmPbiEO9Fw2!rJf|9rHN%&; zV;Z1hRrX# zB`%xaCSw~ZBaiHOf2H8m%8=U+JWevgZjIOwyR66|PFugY*cy0HX8_&Y|HfS^GAM5p zoiH!hsPZd3%uDJO*Z-zN(^^jP0a;%!$O551DV)&mm^)K0k2zq+WW`lPvi=*P;+A+~ zQekWYuBq(tOGN=fu{*~0uD_Rm6{(=?vxvYjG7#ik#xN`bz=N~-#bbBuv3>CX7#2+J zL<8&&`}o;HfoWf$CYr(IUnJI88I4xpZkr3aVG#<0Mj}eo*_#Lu=}c+{-2k3N293rI z9G4$cEG1N;GygogNoV2cLAMy$p;I<_s03uy`X3S-t!)b(7_sE$!g%PBC{AFyEI~m# zp^)CewII8zIonK`6=Hqb3g-HoMH*y^C|~97bvM~$nhi1ezhlctB8)_EUZE&t-wNx5 zQCVQuYzuybyMg7a9F$*fdiL%9D6X05@6y4c=R$74R)x~K64xx6^1Ufpg2d-f)gRYK zzoV-nzF9-1s~3+SZG#T&Wq%q$AVOx#KcwOF(cs^i`^8!+^8?##pN?aj)xBLGt^AGz%DO z*cr2jzxdO8afimlaAWqSAn1>0g26j1ZnY1%@mNiWKm8XHv4u*xuZsoZC28O7s*y$T zfGKNb3`-}`2)$v)a3O1a{2V9l!sDmYFN*JGy593N-#Sw!91ai5;!8&K_^*yTi&>`A z6M?!mC7uHz8Oz2EAajEgr0eu~A~7*%V2Ngf7KxWl-$64PM&b*)j-bR| z4|s|lYeS9FDh`@|zol!$pCq~)14YpjAzWq?_Q$T{_S^{-*Caz3PB4%|S1jQOz`xuQ zX~b2WgLRg6)*6X?4r&al7hA6oP+yGy^gZ)QtmuXRu3%3NA@Dwf56ed9d4 z<}tgmiR^v0lUpelwq3eS_+s7X=UI}$SS9}5UOtvNBM*`^L9!p3=R&v1y%%AnbPE|6 zzspbVZqY(E6`)PUj$A58aC1xj#P2A6P2ge5oNE4UQ7IHDEjOuY zbVx0Af96*o5q2`W)>Q(gKM%r?=4w#XcPo{H+Ok zsS3C5ENgGNcxsY|pGPXw7(5L;;tAH2i)WbuD9Jh6PC42>nI_Vwwvu ze|=&b05RgFtJ1mbz=nwKc^sC$NUg>1IA{5EkunL%Z#P2NBTiSRN4}V4NKp!kf3&k+ z;1f|PEsrMb@h|ZCSTrPAxZ0J2XVQ(a%qo2pPTxJ3Lnf9JI&MBaIzoE!9h?m&^p|F~ z@qayYaV`sm(oJYg6I9qrXUW(B?@Xgl=a!T@nc#Tj;UtLBntuAfM_W7SN&VF&i=fm` z!0E9sR54rNf(Hw{eS$rDOJib+J#ZRMFJ^R@D{>|MHEw1{sgokYWiZBuD%-=6XC3>pk?3$COetmDf7nQOnKU;o7m4 z?#HTC<+pZ?(z+>4tR(lDs@_C-6(v7>imwkB$V&8x-z`YXhJ`!rH4`?yMd=XDNSHW)JljVGogW5&Fd7 z;!)@U7b=*v$~BJ(gNmUmVP6lTF!RLav99U`hoJl?jGC#?sfBJgdIxjtO@ElDlmWVt z->NJQQLO99nJv%Uw}s-4?X9O67l_mYl4_aMC<%$e-VABf3edz&rgYNza=AFWf!UGn zbtbq(Oq-&ENX)_5%?C*Wr?FtM76tj?KenFJj!Ce(mM25kt<4T$HyAd7D0|TB73qe2 zW;=nMMI=nKAg3~QIlwSkY>UfFw_+Kdfs{>hut55v@F@Z$n%GE8TAQnAu zMr7DG5kYWa>D8nv&&0MD{-OL9l}_-5#km5{4r}_bWFMgV+tX=x&{3K()A9m_r zkc8O!5t;*^@4`Cnt=})@_ASO6fO>=IBxo|;@T!wXQE>h6l*COT*UeSw7Q_OAxeyY} zT}5qr5^9B4%EuNiYEk>AvrE=+c72LcXx+AXK~I8MU$> zm8ph@CjB(Vz9i(<741yL{ZaT%>s*M!Ht%Y!;Q7a_D;qJ_6)RoeR^}gEd|Sjo&Tqc} zs_&wk%@ASQ#|Bde>2GAa8r6{1W>%+lJizPd0{ZYVqlSRMbIZaBX6%i7rzZ31_tpjJ zm)V~tTX@Io3+~%Q9|%HK;U^fsvA~V!nT6p(Rp^5%8ww`^m(ix#`ud9aNGNXO22$ER z*T3UY?tGPTFZrX5R2c+R_pveYM<-ih+1FjmL6U7b)*ekM^1ou_WvvC4+*t3`#y#%S zHo=tr2Uwef(Te7oa$n(pg^;KFDvD+aJkDfUV)pKa9~RCq?TPyu5hEYXy26NUuQ`zr zUrG~6R|&O|!udJcwVS|}Az`7P3ps4BPqTRO4-UH~<`!98yx`j}Q^`|nCaJ0RY}Og-$2?^8OxD{a@2RlN;UToza4Px0yZ8-o zgDr;YE@|XkGh9k-&&+s`FFZdT?Rq}zqcnJ&jIVg20Uxf{^Ga;0d({b__nNnUoBr@p z#TVM2PrZl@>~4|b<*~6=f*XAw-O9HwF#y4Pua@7xvANnAn;_a^Q~nOXgXnvyA~!e} z$h1Cm+QS+=_@>6ZG)cZpO_Q>}J6IqRI4`))7OgA3wZS%?jmLBhABtkfre` zJoa8mGc1h;ZPhAK_YDxrFa8E;I5YmVKZq*u$f^SQ&tIJi4(MD9o2QKoH@Yp&dl)xN zKhjRCl4zB~A9;9i&zw!C;3l(qd1*aZz389Xd(o>(j0$%qr>5Ak5|_9o33|8vQFb7oTI{_ zsjLD|M|a*i2tt3pr-~g+jlG1(z0X*(U9oxJReNhUOF+#}E6?MegEWM- zdg;C5*=)@7=3LxYeDsUPjl=`LEX`46?VlsN?J4=K9t&TT%RsMOA=sJdA8M^NKOU$< zqDI1Ed>(qyWjmzkmgE9Iz>4{FrUPaut6W$*ooihk7b+_}zWT5#^b_)aG+cPH8Qs7a zQfx2V#{fvL|Kncc&Aju=ssV*Tpb#X$quNzCD5c!FG6JO+9ZZuxfDxuX&jFU(QWSqjyn4(yH1oj&R3h7ENX23T6s$Jtw znaE}5mz%;W`R^5jZw$yN|D!aP=M2l7g}JRojt$B=mc;H@1 z;|$~nMPhCtKJUhN|8sCvVJbzns5)Ena_ zJMHqcD@DE@nd!6ERTnKkKjILUYYLTQ`Sh?=QRsuf?n62Ia`u*GaXvD#_y_^h#mP5}rq|vZI%xNz zkF#3J0rj0{t@w})fKPah?A**F?Qv7R4;E4D2a*O^xqnC3s@JZOZ<;e%x>mB*Ww0Wh`O_podj>|{h?iS|FplyAu@m)St&M|C}c6g6mLu<+4R6qE}ChTwcO!=_3iQX z4i09#E#ASwCkK&9%ZMZVCKnI530oq-@=^NXe&4R}_tn?lw7C$p_?wk^qlICH6$57M!(Lqdt3M3^pvL8)|L%|XQfB;Ah7An!uU@!GK@E$+*)4_yf*)Nx zjs!`JQbf1SNJ*AAF-P5mKOZY9B1EMwJM+dsGO!d8uusB}wpaoP2}SnF&sMez9)|Lh zCLUIL_wYgh^1Mnb*EmUF#~am~Q6^Q2RS6S`;4>)7iFD#a5XWMmsV89Nt!s{?G+oucYrb9`Va;|;$78JeR zT=%tj63cg6$5Z`YDuqDeP%LniuT#sOH^mNmWjsrPAXh3#I^UiT>#+zzVfgn}?V)%g z&lMdz^0J|j_32H4RQ$B@SBSt~BcYbBunSSrw*OqVlN-{?(tx%-^xli7b&5P0Rk;ZP zOz^(WbWuyTsFK4=^s{D_9|I=X`#VRXOv6FmodYx{(8r}4> zRq47^iKOpjTE4-LcyooEN?}a^B9z?U&RIXOO1e>KYxT$0IvpAMzAD9QTfUH~oXz<` zvr*<%>-C&<^lF!M-WJ2hcRt^8<_L((W%yj4OH7jhu;%}Lv{vlG;bF!BBPF>;+1Gq= z2Z_Ky#v-f?09*p6R&r-^yp^P=tIoS-7t*?B7T0A@q71=F#Mq{|6o1IhcGUrYG82Bf z^0)NKQa%H8u;{iy*YNDr?m7?ReHj&ge){&$Q%1r;8UB}y5= zvtzE^CIX}cQrK-vYh2cL`Ws#>;w0m94H8~dxNO`MbFB`VKxhZY z;Dt*%guDEWHol7@rRzB1vSq%x;8*7T@Gu}ZC4mm-*Zi$^-3==0bea2JbsR-=x89(p zUeEPO)T+2C^?GC|Lb$kDd9&lJRz*x~K6%t|d%vBK#Yjc&B;ay+o-|ZwdDorqF^hQ&0HOr6C^$St$iqmjf*Bm-?80%Z+LO9 zLXLM%>#%M^uyH!mQY)Jyu3{u5K>?_SE2+yLeSmK`KJr+do$$Ejsx3_KdS9i5PrVB_ zmL73CAtsgh_TxX2ECBxt0Nxb3bvp zK!C6_z>_7hE;O*dn~jaN8|~H@p*V|LouGbAm5}@2W&aa6@(qmqmCW&LUN$!}+r@%8kvJ1ihe>Sb8teE^HyXXi>@7^QN0eL!+R>3l9?om4eC zJvIudTR#Ocz6GH96~RP3(TM}~!r{ua+0qu#|9MY%!w($juvTj^a`e!N(|A-vkk4-2 zaZrtg?Sx8LkZ<#ViTgSjUuECjYt7a7kz){iWSDh>v7?3h2y(eU$siH~$g|rq z3v*RxP!QBsa~Xz(c+$hB{4%Ep|AQ!K0lV=K&GSSs=HC#g&)2D0%*NM=Zs7up@AvF7 zeuk)Lc$tdP?JK7kFx~Ly&z;T$?`Rqj=FeX{G(jeO`8$tk{E-iKP)8jF1hd2mkGHgCdv)CdX z70i2VwsJ=Lna@z|GYW`}*k!&4#fIyO=Nq6~tCF>Mn0Z7g5`PxJ&3>P!VU^M#9MH9g zC)2#vPrQ~cKTAok{h+<#Mzh*$p=QPs73g@%MwamGS6__Vi#x8!JsE3}AmfMVC!kM{ z1VBLj9=TxOZsiq`#c z@J${voS#DBWu?L_P=`!m(C%SmB1k-uN9{3XGAVO|E8)HHfTGQ< zlqqteI8ss{D$-BUNVr@LT8V2oAWaDtZ5L`@TTS*3>!~(cl-b@&-zMHgm~a%RZEx@r zZ{%9iWNNx0S^E3`73y0Ot<3*i6e$4+R5NycTFpa+ z)NEL2^Iu~E8^mqqyo)8LgNtjxs!t`4<$T5I)Y;uX_V8o$bvqK-xSZ|+oQVCTVc|Ju z)hyS>P$T#fFZ`6Ul)f1|g?d^}ZuY(fRlb`eOSxWWqTV1{MH=s-QUAZ2?Fcfefohg# z(I%~<6G}@RXpSg!Z7isEnQYEmIIfe25P0>JgEX;+Og{jutp!~!>Xj4^jAq)0Hn1}M z_#`wQ{KAwG@czD-A(%Y&6eC7Jz!Eqb=DuOIUOvCntNz)teFIMRYPMG}ZqPOuv|xa2o6qY3kLcQI;B$e7F53Mh zRsKO`9*Q0Yr}RNX;M+#N4lJ5ksS^!jJvj%0O(GIf#E<~!no0+suFPsxOLSu>{3IN9 z8`a|TO7@k?H@MP)#vL+GcJs4OYLXXrvA5lqN6S3d3e&*ECr{5v(Crs9Y|ivnfdj$@ zMG`46;&jGY9187PefQhx;bsIK%Lxq;19IJ@#;iC3BWoH2Vmsjgrbl32dim^ud;jim z`RytlI-7+FQ`_AKy%55mAEJq5Fo`zQgH4vYUjbCp-5)QxfwPOK)R|1l$D~2R@>w zrKO<(DDJG5?PHCtYy0TP;yq3zv*wzC?#PC-^$e;Z>4^Ci^0>#KGVc}ZuQjeYJx1{H zQ(NREiOeINquLUTpJPYdG<1eyqr2Y!XSe-|{(N+y33_ie41lU(fqcI6_S;s{!fpD? z{iiB+dxQ)29TnI`+-zy)O@BO$>NA1)dCGnf8`H0+fFuH+fH1iZ)8I{c0|40nbT}J{4{u%6v~bTW`Gh z7wk;2uuGX4wNd<=@6s~F1fP>Ml5^ne=B&s+=On%HH8D)qnb-$C5}Vt*v|!i~*%I$k z`lKZ&(~VFnpt-iYK29RoexIb8_I2K$r}&LODXJHO2_9DZy&u_&Knh-Sf;7? z{kA)0Yzi|>OuLKx(tzs0D&9$89?UPi@4hjL( zX(o$$LkM*ZC;!}C6@1o|h0oev70E%VOdtQlru&_3xC?qe*djXjml!LIm7}Ccx9vO0jH!4X^xzB%=h$Fqmq=fi(o8z4QnmStbIV-%6YXY_WO$ElfA=^t66zD=H>iAWW+5z^13a zl?n#GUnQ^R?3lGDpKkG$u$KA?q?~ilmKhBo(m)%%u*-f_lBsu_P_cvZQw&-bX8X{s z?L8f;%l7kRyQDT*B#q*3pMxK`qM7u+&`bEmL83{uHpAbp#7x6n^k5uTGr}4E&``gS zO+9&vN-scW#<0CRy7Yd=syr<_5H%QbxEppMY>~7qIA{lI#5_!+5x>h_II%r zHeKzM@~;{X7QZ^~Dt_`{)}P2W$m10Qwq5<858hq`qfIOur&NZnB!3oRap)+!2p0{r zAS&;3>;`>JEGdDPEqL5Six15xBk(zyOwh7xRdENrKt`MvW!O+r#pgqQ#E-h9!ZMf? zFL0@-YhK1_@^h~KcOV*>dXi)-&k!%Y_}5EiMZZhifJ@tJU2)AFZeVC+xcTr9u?5wJ z??h}6vOqx9vn`*RM2?xwjZ14iXAE$aQ*?0obXer?<-s=*B)*h!?Lmj4c7mFuq+fX)#8(~eJ+A-YSO6Ku86poDQPdlrPZh$ zo_Ind++idYg%SHD`p?Q2iLG*_c)Ekc2nbN_*oBi4IowXimZ5ldK1c2IOm^|7aOreo zX9rW_lfj^kWhF}6lqs4-^lw>Oe=iZAE3T#~iyi0_MScxDrt~xyZPT~pl@iKrO-;*wSAqR&YNFfm5&65o)&QQhToOW% z#)}4|E>@Wq1`uAmvRUHGk-K&Ibr`JcY7s|tcAx!CT+A<`|0#-4LGEPNMI4V?n{m?i zDDilJ>!2#*+}6ofU9HPoq@ii^G${C&zzkl9MWd1}g7I-6D>U_5J*ySa~e0&i?M9 z;F}a2HN=d@@A5ZPNf}K{{>UOY!Q2;TWYLTZq1BwLTHppSVr%bx1X_LL(I7krN%)qA z$pk8|sm_vMTYcTmCnV5oRv+YwSH=Bltv)G<&vhA)wUxjQ+_!VHRj=^(tDIVWp9jrm zc7Le|{Q50dRZ%)98#dZK>fY}a_;p-KJodhTLk(eQvW+fZTuiAjh8Z43y7H?;o;teN z(`4knN@+&#Uzpkb(@P;RgXooP35xG)SHN%x>9sKvPS|zK+{dO;LhY^XY?3?|ivT<& z~n21+GbULY=QyViY@Ek<(r!vSfDE> zKvW_F_X4w{D3R%70_4Xg-HO|8<6vY5zP`#bQal=~6-Y21u8@ zkC<5G+$>RdP~{uRMt&;g?DMT2(+!dhhPZpP zs1@pq6wtlX#Vsc(uCb#3nmpU{^1vdUUxk{1f8d`_s&=Zd-{Bz4O>q#7>?q6xs>4b}p z=GM3-?l1E0J5~VNK$zZ_!4TA7W?k6$wg2Epvh>h#3|+AQXdO@2_**_sLd3eBjlM+9 zYnyPl=nSh%P?rZYR(*k~+$E}9AnH!()4M=xnGE}h*~?a`LOk=y@G+a@ly1R6v%n3P zHOGRm{JG7q!b;Mp4JGOIvKwxZL%}@Vms99h3@~Ei;RgTM4BgH(^_Ogpw$#k6^JBBo z+4c-t)!~*R8jezkdWFIyxixN^HqY?2Pwa0ulv9SPG=*1p>fQtV&^9vu7d#{iEZM?) zj;eyqwiHO2eT4g^lDtMnI2S=b>}6UHk=K>@hp&ldy+S?2C|-a4TOLbBo({YdW%1^p zM90QV+4VQ}_V7>F0()6|o}%N(S=ou?-k@;HG{{Yu`?lGruW{+lf5muOqT1)vhCChd zkvgX8z7~qgU*kZRcUWJ3oN`BXu~~`2vJP*RgX*Kg{J|&?8(>4`y*eB+lnTQ^dL(P7 z9~=t@cxm8Sa2kU1vPPVI9thXZ${~*VxgbYVG1Jxyg>;;ZTT;xss#gnqM>H3;p8$d| z#}_2PU{^Jnn!12qws2JcFQHx6GSzvsvtl&TXkHux_x5&|6s+o}XGhA^MN9iB1UZ9T zBdbP!NKjrte;||`inEPHt*={)>hkTj7eCI8+$#$v8~3k`)r>-0dGYqHGINVkLnvLCE7PY@MyQkC2B zE=uD)3Frz8lIckK(OR-^L>b^LIcsgeY4m%Lrc)Be6h`(_?)WOlZNp-rPMS`QmQFPa z81V<5Q7m$ifPqKP)AN^AL8iCI@nbr)vI$=hjJwxk@wnB)#VSIX!;q}3tKMMJY?6dq z`0UpiFn#j$1sfE710$o&xl(Y{9^7XVq{a9i)S^?Ep(TH~gtfhm61IJ3mT=`jtjWm{ z&7%&7d=h7iEG}bcru8PT(=qGskz0WmdQ|(T$yZ|wM+SHx@X1B}UjwQ?fthZ0(P9>#RM&Wn|FLLK|FTA5rvxF-PLnA7LC}a z@w-G$V#rTelm8&qSa9>)dYZbXU3zLSF~4Je0H{2W`S4&ba3F>$mLNu0ph|4DXyp02 z4T1rb6nnanu3?s?>+)QBzUj||o4I7S+b)IoG~^f4fS~%KbAnZ6#6_UXF_P7mxFta@ zLs@C|gTN;GXmVl#=65!V<`!=JHQ$N1sNiM6x_opChu(`qzpe-PMuP9W#Yz|pwO;Qi zGgVlgio^_F8)GiYUo_8^{9F&%SvbB^Z6em9VZwUmo1D~LBlEqzdWnU-Q+@n01qlHD zICTw6?b+p&C^#P48vKGPW&)FPF@1d!b3?!AD5<_+2TJWQ$Q2Dq_n# zi~56toAnt>Nv244_+Q%haNP^12XPq^(ismFU#TA24TkuoKB!`eQizT6-4gh(hub0OvwIOUG|Y* z^8Yn#d{#!TRUng3lxlgB$~1AwSy5+D!Ao*7fPz7Z=!~&?nS@hePI9DD>%i*N@dzQmt`3 zC&!>wL<&=ZfjlN1-%2Dxr;DA-9}b-UUa&}IoNv8uzFhXBO6&~o*;MHvZkY00*bPp` zk$t&`(Oz2edc0wd;iik`ZkX6Q`$Ycoxu3e(c9vjqfEI2gtlk$@ORg0irjpL`!wWfwsSnTi#J&PJ=U5#1!-n*I==z%bmD5}a zqq+NzQ>}L8n|V?sS5SkGI;sojfnkE+lkC@}+%+6K)^9=47J<(umT6OT`#wcwFm z08a4<6_nLsoe%)XQlDnd9&s|)5|mn7xZzOA?Pt{?*E;#uLgl`#vOj>fnAqAL6cqN2gQBuNLmD$45I~2+?N7Rm=32tsIGr2t zj&YIIuzgCL>!yciEuyqnl${6I${5D5!4YrLIb3UNYj)TX3OJkqL17ZR1KAXIQ^l>e z*=b6dpL%F|hu{7frqx!Pp#SS&x_o8J9G>#;^mO~`3JwH(|5_mi%aL9JJzlQ7TlVh) z(}8~)7jIs^?a0gFPcD1E8(cw9`g(_qCl$4jSizxOs)5&}<4V&@^Yi16c0^eBJ{2V~ zEzz_b?2W?{K{!B+6Ul4r6r)M0BG-uMv*Zu;@4A`2kuP+8$Z`U&GwD%WkX|XiT(h#; zkFNOK@pI*JmG`iky^f8p68p;n3g}D&A+SA97sSt9&+txx0btyvAh?lbN@e`c5RpST zply2dPIp}Tg-1!W5k0kHh}JkWc3ZuCp6&cR`DH=vU3wuMdye8}myB*IpMcN+n0~_o zw(Nc`F&>pa5^%7EP5YahugpH|47rw2SL&(5HP+nEY*$7m2M}ID8zS*E+eQmZ84;t{ zpR>tiGGQ*56I_1pTnGBTHw}EvC40O*ZTowQ`(+n9D7d*gKU|*M$bDnxPi~8HYY?n8 zKmUul1H!4$@(I3A7g;z2%n92tA@6#11$S6#I~n0Xrcpg?O-6ja4)X37;MFs8afmQR zWpDaBj<6On9R^*u_xdysxis(C7bs{h8o=eB?3F)0e&>G+Q{{02UR`*gw-pwG!77>X z01JG|JeScpCb8hl`nd-J#M6~A>I~wU9XLsA*kJO?I?5f!r~&@R!^bG$-$445?5>K( zg2L0&rT$ZG4-|2&$2V?fmEg-giIn*DLZcRH;=q4Yb6EVtf^T5`1h4{p>@t04TqVoj z7muc=#OyH-Xe7jwkS-Ds;L-Q#npGexHPN&>(aKfzYDncl*@}&`JQNNA zpiW6OtXBB58hiHd8S!abG@u`Z0!!|e);iW1RmbZp3+DWZt0Ctyigm?VvNl`}%=B;b zV44cGVoTHw6>2;inF04Lr`K!*SL~qK^7K{$LF@z;lSLCMKd;@_^!8Nl`P+;A2!eDZ zsgsiuO%N4ait#3LIv#w6p=EZEO*0ilVhfrRM?YJ*Q!AivZl7*`CSQ?)zqPay!YqWWS|AO3* zOoId2E=#4$ouqcjKX-2_!-#ESMR!OtMXW>IYR6K)K z;iim-7@vuP?z8;>kQR3K4PE>4NQKfZ!B*}eUNDkO)zOVL1FBPEgqu#|{>h@0#HYF2To@c7A+!p95;A#E`)J zGtj^q_LD9|0zCC4KZ5H~b9g_)DcPa6;ItC(Z)N{yd3-{H<^SPPAV2NF!A^)q{udd= z*(k+CKTL+1Pi~Q^aIhQ%7~dt@o*@7qMFW9P!{F-(dJFw}?}9aEtv(rjaL;N$^Uxvm z=8tIZJ+DHZ`&&dO;D;JdwHB(k<@AaB%rUrW;}Z*H13I_u6W?Jiowl~IVpm7C)2k=* z;rI1nd>TKY0Z3<&?wC+L@zBAg*w%n3D!3c2T1tjV=MoP>_LfR^lJ@tr{p>!4@@ziB zo@kBeaYFz4Z@_3_r7Xl~rb~z**E|3%H?326;@>~&b!@+R91*d4ZFG6d4= zFmXav$uZBBT=ByAy(WZ&GbtTJzrwN4^_qlV%e-^!@iwixSCj-d~;~}GhtRMB38MY3DspHTeZK6-j4B0q7YPR z`hw!_-Ms$1mt;OcfI3ZQtkFq2)C!zeeQ)K)>ST3L`$@epaUhaP%@3zfp!rANTp*`l z4x|X?n$>@HZ%(Ql8@d2uaEsIwsbtgL{NVGqI4VE^#Q#V-%do1tEsP#QxF&CV?>>+E6FFz^z4lsjzH`~|k3qrraIlsW zcTeS<)Y@V2pEcp&+g%$AemAn(IwB48h38Z-fWD5ljfw_!-7xtWeFw53Sf1JbB}DyP z+6^>io%m}Av59*XmJLTfj|XLxwIC$oHbI1pjBek+QOfFrEzggR-48N-QK?IU0ZN}K z&hwB>Fi$9_sY_!nGS$cj_zd~e*B*hC_wwh;$$oX)+DpOWc)7JJFq6&eUr@>ItrGi3 z>!i~+y?2(oZQPr@eeNT`vH&4S0JB1?fHx5$72^7Izz1}8;P+}=?(OoDEnIqAtVDBo zGjqS3O&*$Ig7=bv!?YO8Fm58>5kl26F{a5PDcn8FEkTbO34yVU>82~*R^JgIKe+(4 zF5eJtcP}ok^_k+2iv^b4o8!SH89JYLsF8dqR)6KyKbcIj=*P3$qlzw(-ihOqHh#mpI1(~e zvERVfTy3*S0?Z+UN&KuGuq;6fjsrH%k+>usTBw`(8x_v<9*6qx3D+gmp)#a`e#o&a zuP!x=kI5#@mwuv?cA@;_@QJ$#OZ|mUYQ=8Q_4@FF%zbgr3--RmwhG2#=xxsKl=282WK6FFP2cXSfz{chXvN|PZy&HsAwxj3 zcTPyq(CCj-zxwN}bQic~NqBm4Sw1$(Zjo^AFpeZ*_!%lU^cx16__O!$80RE{c4%z{ zyXiN9M#^{kV0&na^A;>z zD2aJsjPhc%%5e#VKv3K>S`5|pvwguV;pkREZN@_Y0g zqC>5EQ~}V*>MWYIcSTekz< zH=#!b3n#EKbok2CyJQ);;_Ve;u>9l?-q-6uc>)??kisj}O94~k$mP-ScK3GpUwi{5 zl5WPu(vNbO_is{tZo)*9Ri#==9OdIH@gbw^X5kRuz!dF*p4#LhCfURuLQpc{B;1%P zX1utE(H_Oo=9Aj?&lB#wg%X`NG$WiS8&j|lp2@nAj(x@YN3B@bh9D(Xs_gkVv+=F< z;4{+&eAXkWEpfsI&Sdo2kvp;!K6fKCgj*Pw&OIq`|a<7%N znp)rAK%rACr7A+9}(Pq)qX42~a zz*mmuB+If+QYJeLk0zl5AZfCD-?d*P=sZ8az66904@tmdcXR~0V1P^A+|j3$&D#7+ zY)i0?DWH6=E7{EG#{w3zdQp(DYX?ypfezMD!tmm8{{uyQmJFTMAg>pd=gDSlD%6a? zn%?I33RsPJbG~oB*BI!|R?dcQVU+0^~AuM(HN zCPI3~kJmMNG5>&y<_qZQw&&H$8ujl4qg9`*cTV}}2fYB*pC^z((7H-4(NUW9AYS}& zArLp;7vD-4NQleL&b~I6^0OlV(hibFphg1@PKb~S&CPoZ2BPqo_&jXuil~+h5!Y5L z1HY1YbL=%A7Z<)j$gNOKjm6lX&px;+v5n8TtCPQG4N5KaNXF-+JfFSne^ZxvLe6nxf_ENBv?=^{(#E#_aYxn#O+P6{~ z$rEHE+$9Vo@@7&t@q%I9B3^jxH3aSkF@Dcw*v+R+Ywz=pcqXVY-yJqI6GGT2`J?6O zlEZk}aAc@+E4#f_JCkw#+7+5uF+z?oIDQXJ{ga zsUH*@d|F_#c01EV4p_qo>FaB3Yy?`pS3pJzPUB`#6)x8z3SXBbeU#pk32-yt&K{GI zsJj^is9JYfVYicjw}@D`>Q?`Sk;@vpBtPee>kGMj9w;T2)nADlJd(@2>F9UxBU|0L}^OQrI+5!IKwz`T!6&dRc7zXZ2 z_2|R&@QD`%H$d4gErT_ZyTn{#k|(%~kCcoIPNV#<+5=D*u9k&AKv$qU(GfxhYVDaZ zH>eWcu*a zzjx27>!o?*Q(7wICcR4D_hiCQo0)3h7VEUUCg1$|(2SOtR_OD)hTQSC;4Qu{ z73#RKaE8b)&}ZcBo|vG>dgl>*Zn4u}A@Zw-P_RkT7&;WC>1ZWk^e4K|ry^E2!q#vC z_N@g3kCd5=pcH=BdOD;*ZGOTiP&E%Y1p^k=1fE)=c-nFlVOf*K6ZKb~j`AdqJv%?d zptGlbt3giw^r)^lC;>dWnGMy$+g9^Gkb*KRhy!sQ5BM2wE@1{fkEjW!2$^=sOZxDK zUq<;Fkt=4vFax;fkcTZen39nJA$RTYZ5wDn`T&z+wu6^bRF5s@0`^K}_$C$YilT6g9V3ASdZqz4hC0+DOjfQ`Qj5}*8)}x;C{rij% z8~Pq~#gRRG+&Xj&zpSPaM3VKZUm&J{tMrRx&5pC|S%i7LiT)8VNTfnP>HDwq*(6(P&Cg9oJI7s3)y?0{&5%?Tu3 z#T#J<<6y6#bW*V_j}MB_DyAaoB<6zd3A=Rg%ii>+As%H?B8@C(6lCkkfT^F}i9I#g zvJl9nZcJbyS(vhEtF%?;<8IW>;`L@fCNb2$Ud=T~H5_9=Y{gPz_>PA}d;=lDX@>*r zF?}@PS7Q2!ML0tU^!Q ze*JnQ(?Dm5qZ15VD}ro9aeEzm_Tt+HKN&sfxZ2_$rfw$+`kpB~G(va0YDX~tk^95+ zv0YUmjX@iGSRBgrem-F9t`7W4`h7l<;ZG9v_BmhC6kEBBJ&>*ODJm)59;1NDov%gp z4HK5a@oG8mH{+FA?hh)r*!W=!aJq5^MGfxOQ&W4wge+KuR?~Q962r!VJ*Mnkmm{bk zI~VxUFP-XBx;a^UBapwGXACE5vTi73{;=%EyMfQCW6BNBx5D<%GuDacxhmDRMf_8x zt8nK-UgF;L7%70r0Dc&t;B|CR04b6mpbQ-x9_B7tQG7?j9ZExa44+-#M}u1IwY@>L zL?D&K=KLBX5y}ovpAT1XqcotHF7MQMaqP_BoI^XF&OBxW z^hgz67Lc6Z%A|KFf5$F_U|^~CS~mk!x9Y;>kJbiI?VI$#!tv5qkTPcm0j|1XRfM=g z==W2*u%I;r(!t+*GI+K!&Yh}j^q6b4tH5+zfgQ{Iygads;OZ+#sO)?wuCV$%Hx@%c zEN>tF*elVNGbmP(C-gMmUe2Kv2W%qcU?CUAx|P{z83dv0m7xsvl8nG3Hu5@k47wqv zI>7)gI1P;)rgs@{5{8}GB~)XlEW4|HfEBr9ua68>`&9Y~_1I(+?`K4^lVy`y?R7iO&S`4u z%)8ywt0I1|AD3w-(t)=hrD3?gV;ESjp45%(HP4u*VkDj=ndcq zGDRA2)ys%_BRjJ9hnsD#8;%hmo@nRaj@w8P5M+Eg3AM_e91>OH8ICii)8rv!Lf+zA z4D}y2#gonIcWDz4X%aBOMvZ1l281kkH~`DYm_?BYpF=oMmg-|){A)}?GF!mEbp$5h9Z zFJ!a2W=Jw3p}{}A?F4?j>+wWuQ-?EqZsLzwoKf>n(cWz_2?^Ns0-sE0@{%mTzOwx> z5tbi!b4zK|N6$IzUPGgbyA^llz5Hg|$MZp>_yR`@hPlD`fXx(2rDdNRFw6Di-}e0W zOyTz0=UJd=ia=KFn z7+{1=OkidSwY3j{%ksrB{PB^3pX6)_O3In{=zDF+bR{GWDntKQc+lNV*^LVX>GHk3 z0hvlfUQFE2Do47(cu$^W8#u_O6KDEJ|JQ+pt3HI-ZSOu$YI=gi9o%EG1_-D)PWETw zzND^UAbB{=kD;F;{w>BSlcoI9q#&gJ41=~FXOQ$71R%>L8P6!;1^l5>#w=1s%u~j8 z?HlyfB&s?;yW(hPD&zZLmoDs9pZP65MP4fB#I-yN}vZiG%t}KS7`Kh$H|Q}sYqrwk8`6P8nAlDL!6H`(bXf5!ZQ*C$iJY zIfAD{JBUJW z$1flZb$@m)aloLvP*aX?n>%`aA4Q0iV4fu(6q|_I=W%|{JhU1sNRTk|`F9SIubGM? zqC*YcieQJa=AWOaD49E=|6(t!&b607ncn)|4gyhzhf+ZCamsz4Lv>$~UEqeRMWtc+ z_avTVM9dl%g0s<*@%D2Frsh6Z5WYx@ql_3>!b*{5t%C+NPDo9OwvhDvc9BmLy^ttN zRID+6m8;%<_Zkfv7N+?fj-AX--7Y7|&o^kR`hCnwsj-{Jij{~A#!n@U;IMA4j3ah3*#=D|PuKU0u%G4ujq7^;R&c#T~us?@yp2>NKTvp{X)y z4cjOBtG5DTk#Ms-IBaa`BT!27xP^)K2vLds_~ZOhnp87;t`2T%?MsBgO^9FiqzdJE zNZbT}!7WUCn4lc`4d+cAf({ir04lql-@W#BJx2-&fd|4rz{HD74eB%P#In7zi&-l> zYv8fUWhO9IdqNEp4(CaB80h;{5}P~bkth+gWgN7t{`y&m3A_U`vIu--IFzmDDAS~K z5!uY@yx}J1U7XOq9NN?$BS9cB6R`cCSITbF_~q3Ln|g&8zV(il06JjuF#fsM^@n2g3>O_&vewG!H%(8@Rr*&K zPo$$`N~hZ+j1M*R#Ox1JV`!4eCeG1Y>fjilCN_1c;$5VZ< zNEhpu!;Y&ZC`gXZvfIM^oy+%I51;8_gJZeg-j;Zm$mW7lm#X!ahv-y?#4;=StU^0V zG@WFMp;G=#HXY>{XhZrWYPWA?7yElYCcqTy{zHnRo7UJGH4;ze+q*w}$#X%vmz^kN zztn8@`r(z%uRY6nVEzbhU{h|%)P>`*J@_n9$O`>JfIu8>jY?Yy3%m1j?vQ-@z|Ljl z@FN|(r>A2e_-=P?qqnT1>d2MntQ#iCP)ZM&vF}o3c~BrH&Qcao<%OLKErQ#Ke_#~? zF9|(0#pTxjc-&Po$-SI;CEr9!sd?TxhZ%U^zV>-Gcy0&9$**9S>6fWCU{1W>BWKpm z^!I}&7w0*c+(hzzrXCPEx9F;suv$z}495m23p3t2JQs484?*;!3t7*^C4}Xr7 zGy!J`V?k1rUP9lpGXHJmyZR=wppFlh^T;}g`-0nB($}+9UYS=Pp&`P_9}*(})I4}? z{LJ>TpoeG5=BtJKQcyhQ5YmN&%Iuot<* zlbH)~tn&OWj!N3@zokra+{Ew^ma`*lyjy|92m|^v8mz{r8&<_#gN&q2Jr_!>0V!-z zpcEF8f=k8tXk_VP6~98;da`@DdyOI)5_b9*Hp+J{nnW1W8!e&->FZbQ`o-+kcm01k z2tf{ouA3-m7DdM&SN5KfOJ;A-^zrt%CgtJz3QjZHm8B&bTg2&{n<`dCB#ji6 zH#w9vpHTRqnrq(CPsE~*(XzqA#>y)xG>uGKkV_93^$q-GqeYHyS}Sug?)(n$Y`|12 z^%9&tk3fDAf`OE!Wy8YBK}h~=hTS5){9!2oaw*OC^2RfkxX`T-%wH=oQpzqvu_8Ee z2Y-)<2JM`ept$|b#VHm@S4gOIE)d5l#?jEWR;?YIW%jK2KVw^uISi>EEPQJ?#)05V zjyD`^i6Z+0l88XJ+m!qBcXTopcG3c#3bWwW^ol z%$*0BNR+r(zw1_vO&sl&^lmJrG4y`D32u@BZs6-a+z|oFIJb%jr2L7%lxO3xUyERH zRxodvGVT@!?V&ID=^Ipbv+9uiD>b)yFW9`FdfG z=jEBoK>MQJmRQ0;wN^1x-(1!yCRuCeTKr&7t?_&Z$qvX^4FtjHTrdG(#AL?9Kbt;oM|YlB>#i}db1xsFtIM|rid zg}~eN94sp1)r8UBvCmB6)p=A|mMDq*->bHgnl6SPHjW8p#W7wBZZw^%o*y((sSH?t3-Li|*oO(5L~ z;s$nee)eD(5#$!YzwD{7%BLOZ$G9nc~^gBfP6$I^(!ID{U#IBVDZT`ara}3GI`LPOAJ;P z16FFB$6lmU!~FoAzd6jFb$GM*Rn%&s-hMuBlLRC%@izsvd5jA?N}1ktxvPR%9%7&k zhYdk1k|E^`Sz#7k(yM2e4$rmP!A(B(!oC*mc>Z}U;GxZW0gl!Q_JNLbK!DgwZWFW_ zX@M_<4|HNvjrl*kq=N1}JmyxiP3(Eaix)+Tr$LHFqCAOln|e$Ydlo<9=v|N7H&8hQ zfEu)+EkV|cd0WXIx2kH;gbFGzC;Iz2NCq0l1nK?*3*E+MGeW$<`rkr%na=SjIH-qM ztkpE>C)sW4MEv$?8rS6GOZP-4Do3$Y+fM9PLOj*mO1{K=`r9-Chssgl?CJ$XWowuF zvo}LALa;T-JC!9G)_P8z2!4Nr%o4Z@ZE6t52bwEvVAr2sIZMh1o>Ga6D_Sh9F}8mg zmy>kkQhYFAr8g9KjP zGiUK9l-sYOl7=?MaawYRn{&D>!M{#h5Y~T=Cht)b;zY&~jS9?l_u@GAM|$fI$E-si zd?cdUqGEy&s2h;b-yWna5sPUc7J;W`DqgjpgDB@{7eKH5V0FYBju9o{jDS%Q~g6X<>Ivp6`THrnay4hT+Zp z2VYSRRR6K?>w+sDIEszJ3ZEhC=fXN9|%UMfNc_p}L8LSXRammTU{vaD`=VuG*es&e>k0&fNC}b{? zDYBw7rAl$M6BVYApDw>SaUCmZZdjAVRHP8+f<4|dtW*9xIIbv18vAfhaqTTBv*)UZ zC8}*daqwmX%ia#=m>DGi(%6r{C^c-=s@nyjLzeAif8lyWP5h#vnX>vyH(W6#0>nd% z_7wJ8mSz1LuS!v z?FA!c)~%8n$r(tRBfrdH2md6pLcek9CJgW;<0({IIeGuO=GBFxY2-Pl-e3~(FeMrs z+-pIp8Sxydg&!q|zWLpni5SS1m%rx7j*OlS(LvLr(bd~;YDx{vI2bqbhyyE1v78E& zPEn_|+hjvEBGfRid8_s2)91(8?eDn~QN(<*+(|Na(AR0={azVgweenoWGpT+!=2cB za?|x(SViKXxRQtcjqzcj;j+7otnUdsZJ)rU5h|JPda`aS^3=s_%=sOF<(*&h$ANm} zLBe|Sx(F-V<>GCb^8gFpea{4pJZz@Z$!L%ur27w3H%ykWsQh|en6;-Nobk>q!LT?| z0K9heSZn+;?=Qx_6$6Jr!moQP0XFihgBkLH#!$S!P{{x&^8ootJP zCwx@8SkP2bt!!JkyV`3!Nxym%yCdbr6C3V9i5ig2Mom-)Cm6z6-3wWsNa?re2x`Xg zl}fh2KSFzr6A zpD$yLN&`>&f*h*$(5FtbhMmu`{iy^g(nL4jd7{z2QcaJq{0Z*W$U+cj5wq1cA^8@( ze^(%?LBou}`X1ag55-D~g#;3Hwey>{==go=gas3r7P2&}ot6=uVq>Bi!|f{nnp4qW z>iY>Gn>xKZRLo3r-O;H(pBT#-O1ycd)z@;T-=EVO-K6cNGw%$P8@sza{fs&dQL%~5 zc*^sLY0-zZ2HtnR{|3wO-s7Gq8du$O5g-RUt3zF#Y8|y}gbl232u{sbVK_cjb70 z<56)G=EW=OnZGa&#L*)%9}UIU35O!d?uf`1)O&7ciDY3hif z4#&9l&daxW99@jY#^H1JHH1G@9{`(R<^p`^?@QQt) zwNE>>8I>}fdqYLdu0oE4l6siIGS{4n8J4;1&jE3l*bEZxQO}uC^>$7N7SOV9b<~%j zDVS1u&0>mE5D|Pdg?!D2+d<*-|wP`;$c|iAfX|-_H6T3mY2A{vv6_to8Z+FX*n(} zs40x}KWpiJfQDt=cC9e(#Ms8@$fLPHh4?(}uo;2!RM|xbbR<>)Tq6#qwwWxdVL~p!02PbPeVK(r1I`vo~ll zXtixZ;y-5#2cv2kz8s{+R9_(Ax5ygsSa9AFKSE9nk@3F<+!oVHu@Y6=OtVSoI9&9- zv~7IL%NZkSO~C!j4dd=Q1l`L65A{OsLxXtoC`kLom!QTeQoK(dDu)YRZ;U7Ttgc8S zCNEg#bo*Kt>6J5XvoZ_MQ0z|P&eE%Uw)xm76WaRl>P-^j#jJf*^qXW-(HcI~T2`|S zL;B4$1Z+x-cx9fND?izN=-_+WNx}KkkNy7mDd^vzX$0JZ2l;X;l-=w6qXHs%5$jaa>4;AB5iN z=Mh7XTk{&MaLg3NY%ICDDlkukr!1fsttp$qP1vf{lMC+5uo@20Z4_-ZY%|CX*j9wS z#&Of9pXMIHmpQ}y^Wy`vGykUpRCwO^{Jo%;?tQa{`gHf!N1}QbY*zuhg<{C;BR7iL z48r#Fxs>Te6_B=c!rL1oUH&NOJ2X0=A?ReC*&HY^%uZ-N~Qg?kIu_gr(}ZH${b27;Y_lUXUi`RV483 z_4m0Cz43Q*hckbVb?dTh`Iox8>^5K-B^&1qSn2I1a~j@A zqvRw?)5?>M)|kzj?i*FPYHXR#bFQnzg?oG->>-8%Cx#L%R=@dEP%3%HvOyOX>x>w{ z*vu6id`m=Bp0T~F;I~Qp^T^)G9fVD3XnFD9M4|*Nn;}or}Zme0Yn$vq9T>Q zb&R12vOTY3X@uLj%_iiQ_bJ5>$nl)A_iNsvC+O5K^0|Y!d+W=k2jgOq8~G;0*~Ix% zhj~lZ`AfSwQ+8@IO^V+(CKmf01BSKdB1v-R-rF~xsOW%$qnHkZ5abm^vs^=(OdwS` zeEyX8cXS_!gS;{1m(al}k&TI;Zq^^m=V9f}=PVM*WQo?HI&B(!B||%D{U(F2q_c8R z?Wi^2Jja+D>xveuaA5r}$1Hc+f>f%G>#LS0dREeQq8SJEJx9lic;hasNrPXgf_LU+ zWjhiwx0AAAk{ z|0Xe8G6%t_qn-O)0}{@_sq=i-_J|6?I`_}rPaPPUUyzsV;0f;WVSPe#UWetpVfTDh zgOP1>9-4?tQDO7+XMAT|UK`VJ7&ludMQFr7AGez?rp!^h8ZlCdZ)0=%HI(nIZS>&n zsxl!m-zOV%ONN@q`D5YspEiIkOl#LUPR0)Zx7uAz99GZkEtTLJ(q|g6EVF`{o%p&( z;2OGz2L<%Lrw*u>$x81h6wqpMy*eO%e1a_5g$N5*6HSg`v05gWWEw%ng-gn1p8~@q zm9FXvB28Wn+|x(_M(C~jM2}FfyXQe6er*j{sINieK5*VPyvNgT-ZQ+%7_)Asa+aEJ zyL8ZI^O!siS(@XOBU>tY(7^S>j)Ljn#JNG19Mu77PNcU8ScE5w;|0a|F_+VY)AxN@ z1iKvc9h3U;$x)Q6W6~r`mO}z>BVa2Ecjn06ws+5tYGqH(j{KTNg)@lgx4&UN5OFVIK146Lu<}ZZ&@etr_}ufr<(NzX~pHgYXM&Vg|40+g01XqMLycr6u1(~B~!s(SJBIVpgNfmleOZ2reF8) zsS`Vh{%q`<=jg2xDvH5ZzIM#w>!wKH8lV=xM_OHfX4=GVEy zY@3*D`yH|^udqPbT#QZ_*8QV;Wnb=$81bL)jQFRxy$uA)oVjJvKD{4^U4+m>ZH^Wj zP!@hD2oQ=+(iosenM%NZiDq70;QQP!t~h)?Me*M_dR%__wo zw?*;w?c1WME1p_^A)*ZyiTT_0nSv^_q=X3>%qV~{=9jtJm)QYO9|bWZv!DyB<`dQh z%XBwK59vwtv4ihbzikv=Ktk9&A0uhXN=Q~2$zkd;)E|IzotBo?_4MqOev7N$x3^gv zCnpF29gu)ENLPoi&G6TWL)GP5&1C@87$5w;ijcB{LzbyS<3JHPW2AuI`< zT=Aefy0AGjw-fDecN^R=i$`kaIMYSOJ5uV$4BKzlK94FB>LHRPR)l+MoVS%y3)P**6oOF&Rjqezt63wtiPI2um#Qmie-|=Q7F3)2M-b z?%J$@PuKl5*$drZ-czxNeTRNNu>c93z4u$~PBXOG;_d$C262M1`rYTU_s@G)&YDVB##P z`43QLuWwqH_RTb^Gz6($55yUw32(Q!H|J8VS&SQVZj$lS z5N&M?dLJ%#JdSeln_UfI&4-I8brUV4F{ncAeg=Se&BI9-(ZjdrGke#|GqC22*7~rD zmz%|z(WrOJSJAg3|224|ne>7AGJZ~4HI=hqIfFnrBlu}l5_*n&%4j`>;x(O29qrgN zBA9R{fVA|Dj4;nX4TWcal5~5EVk4;cPjsPp%o5!S=F8#!3t$hszQ`S-^v>tp>ES#J z{|oTM`~nk318l?>4$hu|8yg@bO|o_ij<~09h1S8X;ClMN2M%L^hB~qTWn%XWvl;1*58RhuK$;ZM)g=ZV zPta%r|AN7C*&r%KBaK%kdazKPRoDXtgFR(6&4va$ZL8ATFixCY^i_V*jAYQK*V2V_ z6oaJ_A=KQ|A8DQ5OwwQ=F-Lg3s{4^cIykc+P5LSNt z4NfS;)PQdL8!7gVO8BbuDXEQbR~Jl1R@UnLcMI3k^;e_WnQN;KWJ`A6Y@qw+wlvZM zZ+>KTm@AK@JM-oRTV@cTlhYVjt3Q_Sq=>#(6QCJQ(p>pq6X2knD6FD+aOo2=G{o5A zag7G>D#q-jCJp*ijV&VD-VZrytEV1AdjgFod@pNEw?g^$oP>z_%#2cu_gd&l3cDzyJ^LX6h@!psjh-f!FEc@};3~L^u`+ zvMGOuxeHgB;56ZKG5e!?84d!?$(jS?m~qoB*seIBD*#5Y&9zs5DEo!r^fDH?_)B1v zqNdxiR#Yd6+}?qRYN2FQzR*ymr>`Cyd)e6M+aJVEH3Ij;KJiAXamiZZ5O_Npr|7nw zlBP>hQlVg2DSZ@MOyQZ;Dr2{EWKYwJ>;L^&tIy4%~?d@N+x95thSQ?4>{i;}^BHct8C69ETziO%#1$g;kxbI#a+m~@Xx8ovPiCVoz3>4nfQ?yj&ZKYt zOG+Ys@19PY4$SoI}(!YGXV;SN&K)nXBT#kn=u&VTtt*qXjTf7wB!R8Ur3*f>uRr6GX z-w(n`YFIOK{8Z2=n0=OQ6ND_*u)(ZNPP$||gw|(9%c(DRZdP!Cu}g55opeE2uqMEY zkpd@No$)CBe2?a}Ko`tTlj5)CsNUV?t%;MXKIIJMG$# z=9U>n?XzR`amyEpSAzIL-BMYG2pGqauGUWiEL>N|3kXUC#=+guKOw;c_%O9M=(ouv zD~$&5$is5>yq4ZK^Pl6*;CYD-t*f$FqZ7> z24GLtHl)CMc?QF-{wKoI#PV+)X(e03{w(}cgU&_tt&bIGDfrY~an@iIqL?u8FR_4% zIry)`&%bj3J>Z(5PqxXm03rvLKc=wXTzqO8Yz7M!63#the2_G`3euarjV2|eEqiHR zE<^^DkhgS!St|s|KhBu~hWx%Tj?RL3Um^3y>Iu6Y{3BEVL5|zull%&L9n_i`?kVBu zJ@@}){kSo{z_`L|xxvXLbCHzg8)Lj)tS;Fq{1K=5N;q?YmDcD8mbbJ?BIT6=tinRl z&768fw~#eYT8hG>U^cOm3K9!tWV;yzzudzyChy9RYw6;9>23(;72;4ft*+cuZ+l%J znzXD1UW#Rw4#+xGoqhEKhd@w=iR^Ij)fAA*`e3=bM^&B^_9~!+&()qn1J-7+Vw>#@ zYtox2X6`6zWQY{guoigyC|(@n9`Yc^6rH!n2pHER}z^CH9PhQyXlot{@x=tc(zReSyW$N{0(*ehnKCG*+ddp?AIZENiVtS|rceVbu(r>flX&<}ACC|h`@ z#raFGe#>(n?B8wGhwU9EFW>m+q0^sdgM0!LFwP33+W8XBm^+ z3-|MGQCu)+(aheaSg|W&nysvgW@Ko6mcc({2zc0pFgla|!8|ffVkKt#4Ht*m7A_4s zxYoq9ByF7hgV6T%{k!&1uWnbp>KZC%RM!Z)^@Q_Hl{8E%cghicJA!sY;&PyfF)|t5 z;umpRC5jGHb$R?&QgNC-EHhdK6}=r`J^S~`Ns!XA)Kfd*j~1I0-864dASB0e&@I`> z$6g7__hoVeXpzN|C){UqOOo@AY*}4gr5j{|ro`**Nvk|bCRzH(E}Rx-_RbmGd7-VW zSa5Dnv%t7iq^u&IncoB;AcOMPPPR{PHNdI?J`I?BVNaO0t1nd%?QTq8cUP;nfnSD) zCSCjz`CR(4Imx+dEMQ@B5jvamu!CzS|4G1q$X$GJm_KFpX3P#FDO|8?K3s>E0P{b=D9px(2AWH95h)jQ2ibmPTt^0m5D4s`ib#OSFOZekO92iySw!It)5{K5y-@(M?rVEXDC#( zuiPf4ZoUr(_U-d#3n%_o&_@0Me>I-vDvrHzp=fa zZkfFww`NgMg?V$E7Dt2x)13cn*nDUJd+N*&FVwTIF?_|l_kASxs|+E#k?bC=UiTbl ze^>vVK>GTmEPp!H>+;Pz8pWAWGWQDYIw5#XSJMv=d`NF-0I26#HdG-|wXY#`~bAku)$`xe56 zgUOoI+vOq4SAlQQ|7}?shex7x`!AYwq0g&O!fZd8)B&&a>^q;EsN;m88I6;UA^AWn zvE{XE<>%`18`WkCs8Gn_-@!iW7Y!`A0INQK2Ud{lVfi=L@Mej%yH-l;zDUocJJ8a*e3v{RH zHiJcOZZ1Je*#JSf4F`ab<(FyKS*RXiob;;s;#EUvwK0U#KP?4z0cZ;>15gSa|L%Q- z6Ly{xs!)?rDn&HnqK=n-ef@X6s9mk)`nX3O2?E#sh{|$ll!deRvsCYvXTROuk2qPY zPPFc=CwkaP%j)A)OL}QyQDk$0b>xeCxaM-FVSD$}KLiU4i$s-^E-BBO6DSD~AJOfC zDbYjJQdd{Yq2z$tg4BdVhB%z`nEF5C1O|%U@T!yfDX<3yuEO6YQS;vY>Av6@mhnUE z`SAYYZ<;8K%P%Do<^?zcWoV(c+|&q9@=jVWwJo0x!!VP_o;I=3hE$Pc6~cXeB_u^u z2ZzR(N3Ve}iZphky!YzO@>j58XeRG$2rtB`-r3*PadRKt!n808;#JFX&t?Y{o4{7M z9@pyOro6?~W}3Nib_)Xs4*=3#JKIA7yr`F<-)iKE+Y|mGFMI_JcT}HUr)9R&6=3jb zC;PJ}UK=L;Uh}a!({uf+EF)?T6A54-0B#Vx)@$u*01*f9sywFbE?uI|l)Q_!c|4nj zuGyk)?c=EpE)rS$c}__1o(yGAOCb?4Ta(Q9#qbBcd{BopPVVTC?S-rYtc_!dneTJ z3^&0+b?k^j<$Zz}s$H(sZ%^*v6GRMuGA;8|-e~>t@!UywaC4nXau(XCk70&P_~Nd3 zC&7%_)Gc~gx6GFBzyxGBq@?b_M}Y1n-%+1~Sb5>AQb#HWi)+VZ+#PkuPo{dHg&KKJ zUbxHD+pxpDr{n9~g89Qb5dabyiPsMfPyjA% z{}SL)VP}8N`a+e(|0y)nDeV4}GG1)yWIo-dwEnm!7RNy-z$ly(hQ9JPHyu{I|GSdk z-|dFIS)clC`c%7Wc`VZS-&gOiuBJbX>JCbSu$qy=L~@EFhbkNV8_QdT^79;TC^x1&-J<;Bn%sLjRgN2 zs0R-T=C^DX5L*MV1Oc%0$aWmQoAXUR+JU|K=h!X)douP19J-Xoe}Cch{W0$`hC(8& zk_a?4CnXM`@>9NVqvze+4eCIK5HLpK{ENg{;3Ii4zTLt>o)N-3&1-0kP?NkHFWus{ znsDuQ3fWDphNjwX1$rk#sviZIIpU%o9x&QAwYrrx+QUYbJzzlxX*HSz0?<1L<_chP z3eRiTJ%e*KW(gL1Lw`2(>VG7iWmJ^k+l7Z==|0Rg2uq`Udi-O?Z+DJ2ck z-KaE3NT+~wNXL8lziZ9nqqrWPIp>bOuRX&hrK_!y8C;q;F(TF9ly{`nXG87~QcU%V zf;7!TcG|f2`iNd2iXI%wW_69$QT_UBg-A=G0;o{>s~=P z8aI_v$gAl{I?w+AfQiQvSP}(%{Bb(f{}0Dn25>BJS3f&Efj=57uN3|w$*Ridv;N-P zk|}kS?6*mE*5Grme_Y07x+i%)4Wky#I-ZWx{V#L+gY1=i$2#(*ySYlI-iv^wrmrrWyr}+Y_rO|`OLmsO*K6#x@czm39+7>zc zOb+Mz@tmUC^7OX6$7#6>67EJDB03rt^FiI!q3#~SED00 zmyb_xy?0hT|0rFJ)Jy;VprxM1lmuCch>Md@BRFtbobLV0!O8_k71rSat8a`!3A=nt zP0b^G#6Sn}efuEPu)Ee%F4)lKB;Z*}#CO_Z(v*mDS#(t)+Bif}uSeeNr+pm}T=jC^ zzgf1OPXud}gNMYQ@KK^np~cT=SwT0nizv-LExnmRl0p8C68(9n-3zQYw-0cyyoXvQ zMVc_a)5b&a1U;1vbvPMxosHtqlJKpX8V9nYQ3M};2g}TN2IM=BMU-4S&yg|>18feA z60o}*yB_cySK0?aTL3(yu?05a<|tb(bGMw0kSy5>X7Gz5C9lG=z9%FAg!9;zlmrcz%=u&&t2rO(QuC= z_#AwMx#wq2IQK3tKlFABrX`EOq0TRCN6SODrQe;Vt3>#tf|2vkP{cJbHG+5kWW5i; zu!{kMdDz8V2eA?ZsG@B&CeM0L=-yb6m?O$(NTVw9*OQnSudne3RM*|upv+=+$3R=C z8k=+HGwpPE9SZGa1!?i$PyPsItxpuP)tJa<7%!Zbd99Wd?$&_)`7%DR5OuyFQgT-2 zi>$xVx~7Jk&u`p*jz8TvSe?J^u~L@@HJI!lyAp!gAkrKO?vXvl5`=->BOC0HLBbrH zkCiJ5A{(a-<*GLDTJ+c|vW~IP)ptJ2RF9!oCh9-Bgg0{nNbxYW)>c`YYRmA8lS#$^ zf=~SMlojxCA_|ylbSu?Z2|nF1OZO5;O#OPBp*HzN2^l#f=pC&$xd$!E(=kdEISd41-qmshlVNc64H7r?IUQhSZaFz>C>8@m+~t& zy=%b+e+GZOy^5YZVKfPI($~HLOLM&_I&&wD5|QlF2)z-tlSKZ64>T{?sG`WGJ(%fR z8wCb|G9D8Pi<|foSo<~vY_A^&r3o|sNvo3!^4Wd!>$|VMsR)04bRF04PPR7kq>Q>A z)+K4c`kp|it9bflm7~MS=_X^l&mHFRYF7mIRJF}+eZZc6RLv>-0x^wh|1T(gH*P{H zy&k*b^~VI{Zm}Ni9@D_OGVa__;EfiE8`Rgc>;@H&Eglaq~syg2CDPpw8 zuxY`A9^0K&*7uG^GGqh1LtF#e`dPH@!Qy#@B)4f&V+fvYa_3P9zk;+h@gAaF^RDlU z*PGgM#;TDU_=Wgs5OgOmx5&OeTJ&4^vKZ=3l~8}bjTb7JcZ4g9n=o-G^s`#;75>S% z;`vfLTTPAb%j=hb5;n7AnLQYClZY>2z^cT+3qGJVZTIu}=y|-d32f8b#j%5Z8cm@! zRfwFw!V(f1b&KKHOeQA3?Vs=?`}Zc(-~{@??I17yke%VTI47xl6Xo(PXc7^~-AhU= zK~08^2yfn~*^JM_YV7PGec+EH6s8Mym?K-CBpa?h` zR$oq^M}^m$#BO6lnX6o29X?Q zRWZvcioUA|6F!~O_+~i zarMURAuh^6Eg9CW{We?%k*90tWlK)Is_IY8y(ag|xLGb;HOy8RjHj#xf*I*{OsafY zIMytN()D(tcmlen4+-h4jQvg3I(pvb{M&ySA(wQ|TK==)ur0=fp^n^@PITb4jHklH zJTd%oqmKlQNW+qgs2vIvo;9H*TLchP`on@pwZ;AV@EyoDU;)j) za_o8a1`O_k`Q+i?K>DQ}2IMF7rHDxPZs0e_nf9j$acX!QJqaeAH}M_|LE7?&m&j%X z;%`|(Jzv{pP#ABL!oR`7ldR0E4~KB&asvKGOiWC?*ZCg|@GL_>k_+etS~ZPnXy|l4 zKnRakSk_CCSg)%p1!g}jv!5=5^D_~-JEuG2s4$4D|KxY2qp>PJxakcxuU1zNfB;g}@{~j-Vp& zI4iD?ep?_Wip$v{0jNzm+K`>`@w^79>bagDKdAk$O=1_bo{Kz?%28`r$t-8AVf+rC zX&=>HXOS2VvutYOCyX@R=yl*6Hg*uUVB;rcCK0LigVBNf1WcuEjG(^_dnb#HR}4bI zVP3m;aU_AZNaB{7LO81v6RmOv)`Fe7w$TWXuT@is`6NA?-I}m~)$U*(RJj=bb?im! z0%&%7h+%cax|KPK0h0I8n8&X-7lj{+qUGdA?nrGnQLLOIdN%ZyN-? z#IN>w)OeS+D<=CMtN5caWd9KjAZls7KiHYxrUz;WZU8gUuT<4&P`(gGo@R+%Cl(<^5je)%r@L*eX!N;sY zU#g~D)|#o3Cb^um&CXYi`nt#}TSs7m;U%YDU9JeO6gym?p2hyBLVHWrZiN?|3nPs% zuHMww9{)BfQ-i`jag*v=*CA<}{H6W<@J&(nGxmWQJMBHy0+S$|!g$fe40d*VW2W~n z%0JTTG{;-JSO0(^o;NFk@7e*l#xftC-0gBH|%U5 z4Bv$LY(jga9Ynw$<=^ZGNUd*^AtTR6UL2O$ zjR_u$P5a+7$_yP^qB}X&44&d-7`}bG1IFFE|8^aP$N>y=ZFd&}00H$&HfnkE`12KL z@7)g>)GG;MPwjD~q?lPn>My+?2Vv92&QPJ~V!>DHzjVIP^Hy24Fd9aO{p?7;V;iYp zLWAW2%1PFvGL;050Vo@KiCLdI_Pe$6(CV#N@3Z%EtA})(!ilD;lua0+`$5YrAHqe( z^(C32y;1nx*YzpJqWhH{Oo%OZBL0ZZyU5EtiNe-T6&JTZrb?>eKi}2ut%J{ZwU6h= zcrbYYQv_hpH#JB|4LN@(dQy-{BXqFWb0u~0^kGU;eOv*rZ7yl=9_bhz$!_0gU3%Ug z>s?h{IC=yOKal70bUSD3=HmxM7OY^~3l^gIU>KV)Wvd5iAM9>a@L`4%R|vm*nvAT3 z6n>EHwHY#}hwaFr1@hDFq|X=zl8*J2F#QFB!*x}_zd%=4jFp22e1Yd!&hwW#$Z4#K zBwIp3|1yKoVuB5sRr2ICn18v)wN~ZZnbcoW4y3KJ&`AMx^2re5d4uU+GRxeALBXkbq#~{)+<#XHH0@_+xk_^ z*|Na;87JS( z$^U+?dN2AZZf$%8U0Mzm3)D=Ttm#dqya}Tkf&sHtC08~FP{g4uAU}h`>p-HEkgza9 zXw?1F$fH4}69yGL54;iXl`zz{9N&Crbkx48f8sxCYJDXLIub&wXwT!mLrDzV^zLOV zxHRdS(TmJyQX5G-2LCevgk@8KEHkl^fsCrEDrY}s|Ajp_8(YkgQpJ2;pJeRm;Cep{ zo(G%IQ@91|999Z#IP7twhWZU8Sq6*59s_j%Sr)%~ynUYEIUS7uoE9b25}~p31NjbrlRpMHsb2llKlQD+Ao%XNAAzxXN==8xlf&#ul$$y!6-nB$>Eq9KjxY z3dldzg#Q;a1J=%!NtQMeT`J8AhCG7n=5Q~%od0E~FGSpL<57Xsle z$L@Hycv&y=jg)rM*N{kW23kW)k2zlCV*@tWj~V0pYoSZLPI}w;`B&8etG=Dmk|g52 z>p5@=CQ=sID7t14lS&41vV6O&-v~>{?`rv|KopibIug6Xbh#sBi*BgUdO`#9iQ+LZP}7EFL@|figN8 zKmg9g)qL+3bp`Cx#Bm8b6d(*H`pYKmXL8E*6Q?acaZ603%1gPT<#sB9>tLM?UWLGE z?cT5AxCg#2LOsV{a>MwQ1>s@VY&yxVc!If$Gl?bkiZ>`ZPld47W%52XN*zRV^*n&( zK5=^f1J>%h=yyLSWqr^oXoLk;AYUlz!0>5R%Ta$0qB+IE%`VmRr+&ls{CBnfja>eG zwvah3wL>$kuCS|&#eOijLCWgymx(Do2vz&q$It?T6J0Omr9m|&GvoR|4_2lm);<(- z;VKTVNij5i?!+k}-fG-MpT9(O3#WD`qD>BnqgSh+u9^HVbt#v*g+4`{NdRCoUtnUsu9sJ301ex<#XSmp5wttqHiePX3-a_2q|$ zDoHi_SIYuJXq@*g6X?bvxAWx7LiyZ?y3CB|2#}1O#DQ)!{%W;v70D#ZRhPIPrJ`!a z;yeU-Kd5d+w_5u1RtKEs0j>*qT(E>Gx))WS)t43b@q>;Ys{|mxs-+bz#v@D0_O=xl zMW_DhWcsD3_KM<3=e%{pCM-HMN*y(;PNtTlRClYm8#q{OItFd5M{nhRt9Q(*o9_4U zo^o^SaB`stuTTCu(P{2OVVD)jx=I}yi+=4Xwlh|1Yv>iUi0+S339ZsGoM<(bIh^Z6 zt8A<+&CEIN!m;31qT;tg`LK_R!VXbd2*s9Qph}P=3(;jx(FhwY?)3#o%o+BT?Eg)H z(VlLvx}1GnV*r8G)Ih=tK2hyw`3TRxe-I;M8dY!HS42+QvCXxk7?h)s`LWM8WyaG* zZGurSUlh=46V*_m`p%CPqN=}krOxKmM9y1E(YDRpJ=ZT-rIh%IX`9aV15wR{mg1!M{!fQz@FZB&JZ}*>h&F{WV=ZT;YWJ z8>`$+j7rmm5=u{Gw=9utFFhRO>DFSNi@+BiH0vnuQKMBGd8<(Tf9W4-LaJ0_)bR~E zbcsL!@~(Ru1ienFU~Y7h>>|PQ)$Y~Z_jAnFY|J_~M$00-G5%kT?c3{PxnG+-<7XX? zme|^rOuFtb#SD$9Kdni5!sW8g217ntS{ii;rr%z^+>bc1FrKw&T9}^7Z57~nLt78A z6DZ=HNoLKH(U?(bp8JXB!vJ4q!3yb^b&s1ujGWO3GyT?M(=hm7N3Tr->J-uQ!%uU2 zKaXETPp-f6sS2@BMwpx(JR)RjtG{@U@R?bZE>$u>=9&t6oQk7$s+8jaM zNY#4==7-&jEg0}13%wK@bea#s-kRQ4aG7)DuY(C8RQ~w0csG6P?rMl!kN>!$ao2ZQ zNME#Cd|2Dg=F*%4Nradnh;2tspNeqT1$=J9yYr)!CDcPLnbyxTnu4(}%CNv-O+!5HvP42UUim~x=I1;r z@HPK5-lGYjhNVm~`P1=kItg`43P8bSO~s#ul-*^*y2hOJJMk;$Ezin|W=Usl`JI)9 zinZ_7IY$?LG6Hod7Idi&(vLHqZ+xYj7}dzFW@TM~rzwJRcWhSBZLOmONh$d2-thwI zs+Q}jS~0oN5&JT1tRyb}zpD(`#m}BV-Q=j;VS;bP;<=pMSz}z;qu{5LkMQ6#rp9rI z`0d~)i>YlUa>d6m>P4xIP{F*G}qpJ4zUr@ zrz!nS{n?gnq!>H%M=fLH{b!$;dBLgg$F-022XZtJANbi&=7>z+DM)PNyN~%Avv!p? zY{~DfWRcCi<=|e&(8o4TZfBlVHVC{I#U|}A#2Oz{k(_3GOQnogF?W;o-{a9VsIKf48d_UErN(>yA z0)GP-5C8Pkx7iG0x;rgp|6y52!KQxIhSlPtFi)K+_ZwG8y={%(D$l_rR=> zC*KFH-7y{oVNbgKzxXd0+`8z0;}pbLKH;?=iI|0G_b)DVIqFYz#R)bCeEIIuC)#+CwqpQXt9!~O8;`7If7c&?DKzp&g+FqBhZ#Er8&Tp98-7-BG z62;IDZUV&dONXkV>%+O#lcU3z9Mx5zzhD-fUJ1z18Kv|`G3j)ak|!pqU%=kGfKAQs z$m{juC5gY~={tS02o_(DdG0X-r=L>JxpZ^O|3#q+-qff|;3d^Qs%cTlBsHpP@c-fE zIcL#m8+Z>4vGv^lPn3rHh>69nfXtTLv$Ycft<(&y{JId_^{-RySl)ioaY$%hYs@Hq zaL8sV|If?Q-+?`sZkKM&pHz_jZFVWUF zaIKoz;A9T|i`26)BlYUVVuRE`C8R((Z|cSs94Ns!?#x9Blrs3|H&`IByB`10Ejlaf z*;;4Kyim?Mvg67qUse{aP)fnYi5%C~sNUI(bI6d*-%r9T@&*-&+l%tzy{dqeMw)h| z*8jk*7T`9LFlfLFyu-G+-GUlF0nBVo>X5)FC9R!;d0Qr}p0iDuBD20x{ORT4%a!b| zp_?|W=Ul~=cE6ZCH$`1hA&W3J&q3W-CcwGSabd$2)yla*$*AQV-_u@nw_H=d_t(3#yb-KycE%R}j6I_sCS}q7S+Gq_| zM&YE*wpi|T**VcjoO`1vHD@?|-nvUvg8a<8J&Wotx6QUe=?Wp1gIUP;yYCxK^d@e4D zY*REIe#0Qhe{qGhv_s~(C0+ZomSIquFUGGwZgAR$xx8Dy^avJoT-gYCeDu7(jSm3H zjHqBR48$248h9_$duNZwnczc~)s*7s;ywi$h)#H&?QdcS<4(=0TlHF!h}8(t^^prE zpX7#sUI>A_**vBTg8W{wyb^W29_v;Aov%(H8nT4qd-t%ih3*ml;%Rb#Ob?Ny1mUyU zBH-+)UK}``OpKTRGdzItZO$~#+vhJPX^d6c&N$S|N(8bWtkaH z4dVQWYse~(vLtmEE??e|hKn~5JaJ57{ZzwjgF)FjEAX^ZiEDM$u#>>^gS3Pt`eoi*FNfe7l zZy|i5w2hJ+1uJ6ZxQ4F$vxVI)`wY#N4A<#{GG8hbD>PYITG~IEb#SQ8ZCm0&Oq3-n zoRNw-J^hB@0(ZsKYziqXDw*NUIzV30;Py8EO1pPV*V9A5>FU$*(|~D9peE@D4M}DU zlprPb+2|CK*pr?l^6B1TLt)`0%;}(YGaPh?CgHWPV0hzS(+Nwl*V8;{Cw{cxrG}15 z&$1Ew{bq1_xLU_rh=TPEr>Rq|xio;NPs?Z z4!{NAsW0Aw8pOjq@Ex`vzQ2g!<5!FvR2(rfwrd3a0U?K%dat3qhzVpBOLjH-9NgI| zT-iCs9lH5TPAz6W+D^=*WP4}pVFaH>XIpX5a^Fas&+)R@y4RMYlw~ft3=)r6qUjr~ zu3cKhmY3_@pXWY-2()7-clWq$mmuX@XBnE<4Z%T;wacw#vN9=JwO^Vq7@y`<r2BaLNc&^EoG0pt9yfu77Dx0(%=6m%*xfxqPS1r4vL=Pe+$PVy<3L z#sXZG#AbGUF4n9wGoWPyyxW1jJ(N1O*DF63f5^o^mT))q=pc_V26R-o{XLH=TvI** zFCzl%34`r%yLVdON~}tHkcLwKCF$Oi1`d^T1+<%CdXJz^2b zdRs~=s}WJ;Z8PHXZka)*IF`~OCs&^skbtm$2*x1~e<)UR!4Z6S^-0-M;02A+K7J!= zs-J~MO>}`t7B=30omv-jzE#(cE34)hXsA{*J(xbL5gs_7MA!EXy8yq-7&n0d6ZBod<8%!^N$w-ezlW59jeL-N;U< zo2=dc0nuB91g5@H!1iWj2gs0gcxb2%CK=2si4LkzyO@xafKx z@O18ZKmK%2F*hd&#&VxjN=j|E^~pq321U_67w_(i)NGX)8y?G_jR$OA!M$=!Q{$#0 zhQ%oCt-Ydtd9T|m=#`dS{8T6pGmf7aaV1iR)4h%gxk6J4tzbi`vFJ`F+)i@sch=Bg znzenujp&^t%l}WoNfG^4`g2aUsnA6X(hnx7g|{*cpZct0qDo4Ib$T%OW{Hvhix<8s zYP?Q8Yd&h33pVW;UaJT^xf{==qLiXKTHs`$S>g11YRHtbFGth#3JX@3pc?1xmd}F+ zwU==U_c#Os!tX%Qts~a}72{;CpG*&FD1!h)#0V9d9kE<3QuRNsg8+&(b}J9 zvbo@MTXI~zx;DDl)Lcu*UxY7vD}DXJ4-4~6pAm1lUdXV2SxT3(zXf-p zBKYy@bi4V@_v3|}(8u)WLMnt82^#p1`s_V*D6+qb{}fkHCPC)sxJJCtHDgm0YF7zl zyPv4v_Mlu)ciEPmEaf=R@`E`ZrC~?^uomaUF>vsEz^78Kx$8GG2)F3>C6%4hb>oTK zCKZzWHeA~C#FC?@fm=*A-L?I&FB>~pNT!3(nHX*w%DW?y<5Q1aiS#)lLe^ICRpGzw z%5Ww=9(mxr1p#Y7ru4?GEk!hGr%^Y%AUf%M0M}IDwa#xV-zMz1`W;4GG8|d|`fO&Z zdM{$9R@QlF0X;el2hw)&FA01ND{FNLh~pGlyq6Ko8ANn25kvR+RDI@yWe;67i3}-U z38|3LycnJ#-Jc=V(h~H#JE9m{&ECAf7ZvmUUo;X3hhWh=Rh5G9=x6mNUruyP_=Z#T zVC8u4Fk7>U$gLZKuWt|UcvKNSifB3!w~6Chp$PI)Vxdn z{tewbr!7j~_BI(3TMws?-BWmU!SNOf7))0wv-ppTcyHFkFbCm+{&4DEt&$3I5nLno zu5Re*3si8uPTh_jeTBm{0sfIMli0ujD+TZ?MEmY0@UPzxd8*^vDp~Tr*N9V6A3-V1 zdv&Qt&ZTs0i;}h#u>6G zZqy;^Ik%xYh>kGd(5*z%eYgIHaBi+iNIW2Z&T(bU=;@yE@wBVUR3P^yZ#y^QRFbWs zaw05XZ31lxn_cuOS1)Hyv!hLRt!aH9HNcZVWCSoY;lJB|2`R8R|0#sXK#kX z3hZ!Qc>R)CxWTeq@*s!2my)?d5OLm)Wn^GVll_bA(%e43gCpbh-VWvIFlX0VdTxMJ z$#kEUOjvN?)_dtJ*HH^i2TC-K2DX+>b#2d0@A^Dv_8mi- zASoYEsDUvj2;^Dpmy{Mi+=gGwrSb0~R2 z>DRKX<;aRrAFy85+TzN8JAq3&2^FgjpF)*;mq>>ixG8e!7*awDhb8>R4$dlb*DUzc z&huMjCPcacPD5gJ#8O+Hp|=dYR+Zd1619K}ggOZEo^^&LvXYU~exJcceX1nehW{sX zv6al;Ri+5~>ZO$>U>kyVyg*>i$;oN%mAFT?j}Jx8dHon3{DJu|r8dXh^Ok#aC>~%T zhK{?s27!+De-j#H=Wa8gm5bF@Qp-pa!X_De^FDY!$XNNVg%}YERcc?c$2G4E$ zlA!*s)TPhi3<`b2u6X~hh|Nvf3N236xt{~2R9-8YgtV)uA$1kZqt%+9#{Yobao80Q zQ-C`U1RF;@4+;(!o1Sk%!t)kaZ%Jfx&2vuwq?xYNnO1wLpPxydmn4m~D-PiwgK z5w%f0EAVs{ZW14LJ(PIg7a1nr zi*KL%9rN_4qWqT!^+EPgS`%9{C>sga!m|U*R~YD3$uCatZ+J(D(D3(h)u&RJc|X3* zcRhvbLiBy5G??3TSBW=&pwDT|JtG>$4E5LgBEaI_Eea%;M>}(c^F6SYSTYPBG|Ll9 zH=r_!rfDtOCfPk+=4%FphJr`biArk9P*IC@%voJds>=*~rGRVlLRM|<_AjdV;{~n% z&G7j3c6x4peeaJS+?be%pu4Wxn?LB3!)#tVZzQ(RYbQ>O>E@4ae8rwTd*v?Dla7VB zaSazhK}e?~M*#n!Mjoy4`;R#(E7R4%2h;WZ;3tQD96b}I>0g*nG!q7udu z)dtC~)ri1un}%mI0ocmDuRM4}ZJ1gir%b7Lo7;*p zklq%$lT@nhlj^dv(CSw>d*7e{Y^nIu8J@-Y6Ot|~I-;L>hD;jJ{`=t;t2g6T>hCY< z%ImQ~*rBVP@f!#SWDNMPs35(SKAp>NW-p1`Kl)9ho_#n!HH(z4AQ3Y}v-_4O^2hbM z;glhOr}A2}l}hrqqEdF1I7Fo+0syKn?%+iN9?5kBEuVlW16X*+j z2RW3u3)K7(lI8XkiUc-Oq1o3{3jLofmiZiWegQHzERgYA9SisF)nrqEhK8vDo0=B= zdxQMN{RQNckt9?G;-$aMD?yI3Z+emlr^Rs4lr=Ez^}@K}5&_mJlWvXrC^%x>l?_q( zuxsz5_pbZV?NO)4p;MpGA>TEo^8g||=>I^LE0D5VnQ^^{KLw77cCQ=q@f>mKv=2c0 z5bX|Eq-g5TbI$!D$Ry&M=I3SViL?ieJz^BNB7(BkaCQFrGH0-x0+}V?l>o~)yt2Ap zbmk_9g?U*l&@ zr3w=Ad@;sfe`iG9yd+R0A%z?ni|UH9@4_V~g>0YOQ}b6ss`mlBLiq$&$~0FhJS+e9 zLzz+v>dTFhsCmt$n!j7XJ#XnjI>LovSCBeERTanS1_0Fnf#GHSPQoIS8-F)2=~%gn~&k> zMbSyio?k2UfQNk}5H8DNtBw}>S&2hx0JU3wjlXa^}h;V6^{Ba$+HXXoVZ9rm1n3id76 zH#@l}XESI(XZZ#_Li85_LW6?QF=7hn9L^hQS`ktIK209T~6ok%h{cC8z#UwEk z=2)GW4+@K|O~UVPcH9+Eddc0p;l>u>A)CqWIeOFcBBxu3PJF z6ebK-H*)K?EaG@yt~?M-k1S$xbQFB0--HvSdN3dxRNyW6AL)KU_sl;6hG}jPX%E;_{@3dS&q+xy58(u@C4Vu>zhrzFf%hKZ&{^{c_`)c~O8mQ)5OT7i)c zJK$y*>pKrzC4uHJfy2-{Fr@4S#h>H-xzSU9_0u&5xC2VdQGq6`E(SvlBcLs%5g!aD ztF!VX!wVOXE)hPr+-%};k1VzmLPZ@{J6*ud4A6TJ31MR7Fj>RFsSJ~>)`DQ4&!8Lo^RB*Ods z4AVuSr|3wb(K=?qDq7r`gWknbo-9@orqJqz_=N1JrL832j0L`dLQC{(hfCKPmi)6@ z8Pw_nlGZA{w^3g=bdG}I8#E^rd#vJ2pLL}{2t&p5#ymNIxOWBQs-OZw0dZS^H2>nTEOvri$P^4*0JW3rSs94imxrCFhEYCD42dI1(`)jCa4Uds@n17IG`&$ z<$0Z~T{-*Qih%$AGx{n$oj87|bQqOqSakS9Q}P9z|K-;Rd>PGoj66uUDzh=QeGMxG zxz&A*1G;$U(MLLlgcAwyy>sgN{S>oVCy)C8E46XM2+@r1Z)B z07K2JulYyHN@nMNdP=LJ^6~WV_M2y=`u)jY@Ll{b$`WMADUkuc`FA*5-%kF{OmZ~G zb?Df=LX=Y=HerXD zsFp(JQ@d3?T8xYEfkK$Ns2@~kz{-C>qxHNW1lw0oRj`oOGcSMJ_|Gg%D7f?NTxwr? z5_f1e&+&((+&?%T8an@LS-#>|=nl&K5Zo7$pyWL2e>YQ=UoWcSTo9$jj8Ms+A?KR8 z`zo8`#sXtbRjV{5Tvy$pF#+1tWcEK?0L2B8AJCU`UROWek`bcm+I=UE1DUPU(`H-h zax-hC;jiZQ=Vn)xl3A;fsKN{kuzNGjt(E_)Y+!^a*?8eL4fPl>_nU=1nZ?J;-F`F^ z*E(5se=yi_tnWi%QPfFRPoma?(jYs=z{B>PSBLBgqTX_m6Ny7|1Wwvymw777s9V|q^^;R(V1%JlW zAdhKwcKq$$m!D#D>o?xA;!i`GkZ$DY#EDE^(nu-==ENW9I&$y$2ROK*iXIy3NccYE z#@uQ4DaljOK~ETm%zW?dhs3rAgl=;xkIAiOVo@z~88Ym7i7WGB8yDxvvm_ORj_N{G zR%98EA>lvKh!Bd*D`N<9#C^g6T2iGcMx`l4x5^zBQTS=thj~j1TaR{! zK+KdMUXLBN#yRTO=Z|eiOVaxIxjqkRYJ5RV9*K5wq8QLL5t4PG z1|-E_AEI<;a&{Mt#ajhDN1J4=^;Ew*IhohU1^>Hx(*qYW8~uT7b8xz)Mu?-ktr8da zMKBnIKysy_Vm_q5ool)3UZ_wZlM&c6$`%Db?22Y>V!&n|EsZ#;;~Z!k9331@y4{x? zLFIw~mh>UtK$J@^xt@u0C|GKvhl4mEc=T}iE$_|`{s3Re@F>ldIMjI#h-V&1*91qqFmXmbl>IVQ0(wEUc$HOLa$J>9;JJlMBfL=c6Xj#d%Svy7()Fq zKHk|wF6K3J5t!XVQ15)rdw5!Bo4`3{vCd4F`m}68=iftNQzgRyR(D-oSZlCGa}3el ztgG|$m+K!V7bL7?b-N^;I68}Eaum|pjj8l+X|zh|rp&5S70f-pH|I!6^-4d>G}gH3 z)`s08jioTocPjVh{Lg$AA#AwKs8Xd&VG8mJ{0nhGiLWwiyuE*IyP(+b{`>O>ky4yu z16X{JkdTCgU-pz!8m-Tir(dr}P@dcmxPr$5KLqXYUq#B+=@~^ER;8k8dWoNJ*p{OH zO1fxNFR!rkoR9L}qMNJyP;Xij| z>PpB~qtkGuMC#Tnu-FEN`mYAs+6LG|&NsMkEAs5elFi_UdaNYhg|Wp7w_GFqje+y_=f@K ziJM=pjOGk2YsMm2}YHLj`pzO$|<>Tz3;0=bHm-p^v*zXwc!Uz-v_w5SoF zqL!_@FIigI+5%W7UB}h5SU~V;ke{~fe1pNfWQFH*xRD^Y-yN z;xkWsl_@^~_e7drZUfop|xy`74BqD%^NoSWxJ{lYg*%mf!O z^cK#~$3;%DDheQXKe-*7$&fojx`eX?7XM{KxxZgnLHQOCN|=^Hub|NVPw)qvuA&D- zr;I*OOu1gzASvE=U|gP#tVbIdX^JxA1XMSf+K;FnGbZ|TY{TJg8zCve;P2Kl*2cUX zjD+4t6>(+fEnAkFsH-61FA1PT?|?&U;46W&F0z7Zz}W$mic@nXkq{}KE5CZzttV|P zoNw)26%Nv%jG|dy8nJkbELFid3fI}2Huvz?huo4syKq`8JT8Dqb zw)AjV+`Yu?dhCel?qh|yQ4Il$EDE?vt|Dw_wqfh~7_IEiAXiS8b67-oW1!6=N^*}k zM1e}kl|CCZ*xS!GH#sRtT!eH12MHUYBxh*l9vk{W9xBoWjxHANt;YyEhHmg_>=Nhxg&$Tv) z!1Jp>Ox3)$Es3lAPMz;#Jva2U#-l*FoQeqq~IyY$GO1 zGh3Zqrwitc$9QOLIb${9%v>^9)3zJfk8x3`Z=?>7G=``FBwd~Sy9tox5CzaXE~(-` z=0Z#iJrFN?63)SoT`R@`X&7t8B{P4Mt+;7WsI<;b~&U&Qk zHmuoNAbN*llemFcdTOxS%JwT1!toIx1s%A{FEW6YmY|=!>U>+Rop!eTG?qo=>s<5{ z?e#xMv#LK-s<1q$rQ%c$Z_jyX6%Be!N?rZ#=v|~bM>?-XuV`X6d4a!>beKe-wT>nu z!__#TTA;CY{1ML6J7Ec2W~3uzq3i;yLv8a?zdo6zVZNY1PQNRNvPN3+O=FJ}+$aug zICS^ETJ~NLWfk@1fzi`*@B9pwoC1Fu6ao?|vkAMnwA>oq}bh`HbQH5UBi#K|x* zz6QWR%&Au1Wx5w3CGvd_XaqipP};h+?2qM>6FjNtRW|NDXuTyA0@uXKSLdNr$Dg&4 zcBD7P%o-~1%JK{YmkV$J!sDXy9_71YS~QR__O72Yd3-rjiwUnAghZ*4a(t}gzrPeq z7o}b~epH&uH0I%c{4hX_4bJer;w7d`WRuQ$o<;HPRRCE0{hl@GJP`u3?Ffne9F|Zl za(@y?HC7Y-eUk6+tq;~s#hBlo=S@W%&QuNB_#1=1sNO0PeQmq*v1^1FIlt02T7Q=J zBF@XXbZ&QPR{oTgUv_EyOyYUcnHT_QX6AldCfuC1vj*o|-NLE;a=K~9WV--a*a@Ej zb%QIEnNx1CpEkZmLDYJ9NOB!liCpQV`1~!YbQK=zQ3ww*@z2+!B)0{2*cTZ_3Pce3 zr5nOhK7~@I;PUmNgVGW5(~LKg+!XtcIhjAo+>?B3;DF=v#K&k~g_4*AA=4aTtp-~C zoB(%OkTV+?5Ysx23;s7?C;NjH;Jv)M=X4o703hzk$;nRv#p75Hd7eSCc*cP5zG2{P zF2DWc7pw#IoWa6?lNf+3Kgw@4p%V?Kk7D5*pgiwg`cbV()=waA#xrm)&qM7UW=eX7 zzkS33^ek;?4{lAi zFxT<0@FA}J@|Ba|n@H+$yy%WaV(MYgsm8kC4m3IxD4A`#b6L+;QlVj5J=}7iT<=J* zyHxPTO=nWa(s%iCW6rxN^zYtya%2grI9? z2qXr;-Q9r)Wbw;2Z4DnTfKLAB<7!|V;D5*lCuK{HWBWq}sE00v^+zGAhbIqF6}YHX zpEa}25Al}vEl%Cmda^#fZE8F`>S7vF24lXwn)b41x#po>8T;q@?LP?0uT@3ZV?It8 z`O}yjEE;gQ%l`*J))2r85D+c`-OSI^ot~6T8*--a<+9$(O}vKyGo$R$4{V)Ajj10&+}JAITK`S#i-->hr)(>4d3F~==Ct_2E)By z5fp1*DEWfek2QPh>FKo68ZOW}!2Z#pE<(u7imC~7;5}vL|2xr4O2eRfZWa?{9Lhai zT+n(g2hP`dEG0}`)x?(Lk*5b{GOM*vqPgA6G)Ze3Nx0qDx->R(0;U9z)W zV}T?afCL9nm)3yz8299#kK=%Mj>7`Ocr#b8>8jBT9Vr44F)2k3-RFYLrerYtGeyw<(8OEc9> zCZ%G%A=U3(Su2`;0tS($Y=%M#cNDt4Wj_c+ooj#MCH(|AWAcW~!av+2jg*(?8BQI1 zvp=VJW#4N0Ra7ENO4>tc0(n;WZyp4=OX5qD^A%lgNi|67$@XYWrdiWF9ZZ_}z` z_N=A%fnJQoWW~$qcfs7TA0AG*JG&2!kZwGVX|%>B$ezHK>u)(8z*r7|=vElZFbwc4 z1MV^a8~;$ubFUObfQOQm2u7$JUFqDxpUylx61yk^dJ<7U>uOhDxKwdv%zA5IU3nt& zjj*hOC{?Fas+jx)CfF-HDa45;m9!6`>WZ)z`}8O~TAs zJW02Gi!jJ?s754GS8>gR>bYBmaFkd6JRxYiiEvbwyKCJ6V1bWu?{u+Z4lx8*SgEVK zJV&O3%LTy1W;fC52?v&gK4AKhIc1*yFbfFO;Hn4IIF(dP9Nb%m^W&}SAGNDoMcRT4 z-Q8Mmh>JftKd()}FJ&HQBqw$g?BdHfZPO5%Xe`@#sP& ztWr%Q#G@d&&Z36zJ?Ig5^IMM`kCv`We3OOswrsFi>Wu{Pa&eS_uue0sG4c{r+T(8FiR>O83HkL^R(6IaNwW5@jdEPaJOljn$ z%t=mF;&`4#JHJ<*gTVwlW|bDuz=Fd@aPU<9Upd)}rh~B2CN#SIvUHv&>vXoEfn6M~ zVE>Fn&$DgqS8H6{+A;*k_rZFCRqCJ;GMZGgsA#IH1zFA$uExap7%8FJd*d7yxH-2y zO%S5?$iu&9%sajZH3L%~z>=~bhOP#Lj)tUKK2BR{)z{dZI{Kp|`Rb1mq?=vE<>*b0 zPL0E(w16(&q&&1rWLFOmaQwIxsL%i;+=u=gFuSN11z4AdXQU?9Bs?n0u^-ap`kW-Z zuby-5QJJpyAD_?X9;@6@=EdUxVt&UbZcf4x+nrV)L3Z1?0*8h9xNx}%6A;lAmX=gl zGS!U4vo^6HRM~BZsI>`x3*ZE%=xc%j174+GDZn1j6(SjwO+(0)@!F8aH|WD!r<(u@4RZCCrYoVM&%h;KY7 zC~V09#3U>-NQ7eQmXo}}!j~D0myBtx^9IX;luz4YE=amCdGyaE5%DVD; zraqBewdy|_w#upvls!FSgY_kv=_TXL1Ebl{>&zSzS_?jyr%FK|?s~KHZfzLD@`Pqx z1?w#QGug+)ZK<1SYaDg1laQ1!J3tg}s;j#5J;obvQj6b82S1JC_A*aj&YJRvCT_53 z`cvVD_hfhanghrL@CCbPmni-YBkk)zZf}SI8B@|uF1p+7eoQ@dcf}~>mpGUw2nGRj zXjn+RVfCVqo}ms%gM=D*9Bjy;<&BIC2%lGeM{Dn0dyb1?pz_}~{zXcU)7h&?6@xAy z5w>9i)ee%eHQr9KA4t=LS^m%!ZO2`CAE_4|m*Q<-hDl5-AnYr@`B99tx@|&x~jNXo6XPDgJ?lnZOE<(%~GL4&DjY^2kM#Z zUC@#tL=bL&F?VJs=Q}uk%c@6!9PC2fAWJu`K-Eq?Kr+dKN_rVD`q~;Znau35Ph>-h zgLrjch~MGkqZ!5U)P_r{a1`i4wnNP`4hwd-{w-Z!;k;*G%C3uV+^8ArRw@t1o{pB; zn=uxgJK#w{G75~WSqblEdwL=B&@(2QsI}3ZvlYKEqZcT-g~F6N6J_jhi~&PhYE;(T z<22-W@M>EMX)h!H)vI`bs4Co&SNlX)%7MXPH?&TNo>{ZFayQU1ypCCKLGo)lyZrgQ z&HRmYf(>+2v!#yqF$bi%hw<)3(9CucRnV8#d`jH|!=O9B!&@biase%qJ$^(?pXoXphF?3i`f7ja#ekXw=Rf(vC@$8P&<&x|V59gaAvqdR^{QyH@ln4nOnyY+SyszxcQckz`xcAKhq zMw|3W7X$;NxMbeUNE`4P;}elm0n!J3!i0=+|WdB13dg){6Hnkv{aIM4S4NTNNU zADh;q4hyzs4iGdRR>NLyt{u~C-x5hYY`cZJh8!vy z+NOc&CX*-XMo-xa3DE3?{83(<^QBz6rTU#jIU&ADv6#tudX$|mfAnPblP^0-!HVQV z{+G3%i$6F_kck+{un+aM5T_uEQk0tpQt?%Ch8LU>*C~RA}TW6HV+yq25H?Hfujctje3AZP~!d3?oY&p4~Gypm9&y9r1Nb_#i>< zcj$YaRU0IJb9!Tx?LW)uCT$iP#&%6<_C6pGCaSN4l5A1=&Y$3uoQW#Q;}L5z=z%MDjFTktLyPpH~ zPWY}WL0Srwcqm5MH~FU;jm;Yqx9GjuOD}JbD{C`MYfmv#N51pZK7XX-Wt9$4v&eJwG`kF zV6Hv9D%bw@i+m+yn~Ure_Cy`Mg3Q*ir(Pu>Hz65OPtiTd;YH*iLSZ{iFWUGbF)j{8 zK1vTqT0PO#T>5>FsK!0Z2HU3BLA9&&77$h7w%1M>gAqXrz|CiQXX4B8mN=NU;9FD8 zRxcVQAwlI<4G)`$5?UJ_PyM7?cqVTJDJuIi0fP(pRbl%~s$Sfil~D57Pc3WmjGnc= zm_j03l7#50Wq8C#D1*Vrpf>u`#$MARt~t6EI3}bW$r^*olpnwbh-}C0uKcIA*NfIo zyue=D&6Q^O_Q+&IR(PPGyjF^X-jQy~8M%Gx#JTe;7yn8QWp+#zo8-%3e(*sM zBw&P-0Qps@JOkLe_u=jEDu@Ai4j#g&IHz;}JZiqHRl+^nmTh3-`%RFEGE|~#gJnAE zg?&8Q6T8qdaNm5mFjgSGI|d_ko)yt|CVf9Lh%2g!i0Rk%geuH;m1s%We@iAdoYi>H zARx+k9MFJ5?=T$cCmb2P<~3_|+fHtn>Q3MyWW=UU0F0SmNz$q5<3FbL2?h#-rgH6e zO`hY2&9Ld~QM8~3KA}JFexut*F=zC>(CaA+sk3jd^iFE}DesJbC*IVEv!0QNL|S%B zlZq1>g?VSVEf%wPbfg|$FY959!t(ASe{%o*og#mH;NSHmR2{RT;TICAZ#Y%JKBBv& zC)~QcwBMVOLU<#(SJyeaAmVh$a$Te=7{)5Oa0gOE%0UHRq@mGE&@;2QZQ}XcCdjhD z`_!#y_vk5w4WX^+3JHJv0&Uy8U!|iI7DnUH-YLp+^*k9aq-jS_vwjKr|c*}7R<*-i2-F{YYkytQmGu(akyw@ z4zJ#)lg@5a&9X<&CWF_v<}uM}y9J3r;n2ykY18pCX(G@oQpAcbr*Y<4&RZss*KTLn z6?4k=TXSX&2g;i*Tzm>Oe(FY~Yq51rBP%apW)Hcf)^)^tk0mo0)bFyDB5K)()V=fM zP?8sIc|Yh(7JQZJ&R9CPqZl!EIL@!}I5FzNt>^I=HfJ4BG;5O$$d3x`{ep7rXif>g z$P~RtOTu6BJle8lC%Dy(XiSH$;XOSjYC;&UCIdh5YLuj$ZiIe@qwjv(*ZT`FHZQ7~ z@xaH5K^{a({gFA@FF3`~O>xRV>l>XmoC3~?QQvaV;;c&)tRyn{91;-gPH^f*(bhhW zlww;ql$+X8lTQ9I@wP!miWfr)exjh6tfexhMwqUvtWe+a0<8iEf%_+ijK;{b zS@5>y0{?tSOt+PLaS2Kl8+U%p0`4V#oVjqc7Tti2?>u4W9K~b>RNwD{j7S|V2f%`EzejJb00GM<*$Q1 zLDOjBDDf7Kfg%4Y@B;)FDK7BDNbZ)lA07-t3G%gB@kU6f`amm*RYa~zy78B*5A3QX zEP0q5@yda3N{cv~t2kGQRgI0V-gom*Ze1I;g;x%SVxPbY>-JVWnyS{e$-g#tT$rk8 zsBteJG|$8=|0U#DVpJO7gpy7kP;CM+LO6vNZ0mXmv@b#cYS(1Y0Q9gxMY@hW1?_0; z_JoGZ8>)&}yEpcQFQ@crpiRA9mXmLoxlXs%yv}P7F5oXBH!A}FW_dlteS1<{(OB_` zsv`bK{JZYQD34H7z2IV{>S(1@XI0M`xdfv=etsgWB516s=NHvp zl|6)kp%ebEr>jSjzpZ#=k6o6HSe=kWffpKP%%`*{E6+mO-dPoDHOou62m6X^ctl#3 zcx_+Bre=*(Fysr|!j*7N>C;;q%Nl<7*_W>gC#lMcT3BU)(>H$DYyIKxvtr(~MdkJK z>16ZStnRn`>Bx)J`6=VY_h%0n4yl2ehbcdUhDl`n=Gu}9a1C8N{y7xcBA?;szFt%9 zg8$Im-bL>%w^ibsyFlnW;!9WirZxA>@w7I{&bVosJC)8XWS)z)Vb+=oa6;z7NUgh! zo6!Y2x4GbGmURYE6UtH&IiW$^(MuIrEWfPlwnaDVsVl7KBg`=RtKc`=;gyT;0z!7& zmrk<_ZyPdeYJ$;_?YDFpl;!+4q1HUEvGW->m74T7^}G%;yh`V01FtKOQcif2yl`AG zSOEVTZ?-9$eNgA$lSiDNHP8O*-Cxm24>r^a;F|nk9ZwMJf7TgS7C9NreSj9A0e0-m z6EP;^mR8s8{F#BDeAuR!kH_}6MX>=@Uq5-?ghJkHuUa^}&f`3Y7K9HKr^M+-hpuD8 z;D$7GB<3i3>p3??q)*u3uY-OAt9e)DiZr!}RcC={# zka;;USAn)H`rl9jYEg=RBlMCt_5UCE|8w|%XxUt-@XH|tAN6Eh0Rcl-%RsYQ9U1*U D_%~NZ literal 155476 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%0AV-F+tmxUlDq*L#hoA9ieT1)`U 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 diff --git a/static/images/icon-16@4x.png b/static/images/icon-16@4x.png index f2ba1ea4127f4efa1848277407b79cb1f12af1bd..f6c25697e50b38e729438206232804cc568e384b 100644 GIT binary patch literal 4544 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 b_{RSSk$~jjq4a~+00000NkvXXu0mjfPX=sK diff --git a/static/images/icon-19.png b/static/images/icon-19.png index 2b9e3ab5e10039a3a958d9de41d5f019a690d56c..ae6fd7369009dc93ded7d2bd8682fcf6126948c4 100644 GIT binary patch delta 792 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 LNkvXXu0mjfU1{>h delta 2647 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 FV1ncn0)zkn diff --git a/static/json/languages/en_US.json b/static/json/languages/en_US.json index 822c63f291..97379167c3 100644 --- a/static/json/languages/en_US.json +++ b/static/json/languages/en_US.json @@ -6,7 +6,7 @@ "toggle": "Toggle" }, "permissions": { - "firefox_permission_denied": "Rainbow needs permission to access all websites to work properly. Please grant permission in order to continue." + "firefox_permission_denied": "OrbyPlayground needs permission to access all websites to work properly. Please grant permission in order to continue." }, "wallet": { "rename": "Rename Wallet", @@ -71,9 +71,9 @@ }, "settings": { "title": "Settings", - "use_rainbow_as_default_wallet": "Use Rainbow as default wallet", + "use_rainbow_as_default_wallet": "Use OrbyPlayground as default wallet", "sounds": "Sounds", - "default_wallet_description": "Allow apps to prioritize Rainbow when looking for an extension to connect with.", + "default_wallet_description": "Allow apps to prioritize OrbyPlayground when looking for an extension to connect with.", "contacts": "Contacts", "guides_and_support": "Guides & Support", "learn_about_ethereum": "Learn About Ethereum", @@ -98,7 +98,7 @@ "disabled": "Disabled", "enable": "Enable", "enabled": "Network Enabled", - "description": "Choose which networks and tokens to display. You can connect to any networks supported by Rainbow, even if they’re unchecked.", + "description": "Choose which networks and tokens to display. You can connect to any networks supported by OrbyPlayground, even if they’re unchecked.", "developer_tools": { "title": "Enable Developer Tools", "toggle_explainer": "Enables the Testnet Mode shortcut (T) and adds a toggle to the home screen’s top-right menu." @@ -127,8 +127,8 @@ "tokens": "Tokens", "cant_connect": "Can't connect to this RPC endpoint", "rpc_not_responding": "The provided RPC endpoint is not responding", - "rainbow_default_rpc": "Rainbow", - "rainbow_default": "Rainbow" + "rainbow_default_rpc": "OrbyPlayground", + "rainbow_default": "OrbyPlayground" }, "rpc_endpoints": "RPC Endpoints", "watch_asset": { @@ -138,9 +138,9 @@ "privacy_and_security": { "title": "Privacy & Security", "analytics": "Analytics", - "analytics_description": "Help Rainbow improve its products and services by allowing analytics of usage data. Collected data is not associated with you or your account.", + "analytics_description": "Help OrbyPlayground improve its products and services by allowing analytics of usage data. Collected data is not associated with you or your account.", "hide_asset_balances": "Hide asset balances", - "hide_asset_balances_description": "With this turned on Rainbow will obfuscate token balances accross the app.", + "hide_asset_balances_description": "With this turned on OrbyPlayground will obfuscate token balances across the app.", "auto_hide_balances_under_1": "Auto-hide balances under $1", "view_private_key": "View Private Key", "view_secret_recovery_phrase": "View Secret Recovery Phrase", @@ -208,7 +208,7 @@ "warning_1": "Never share your Private Key or enter it into any apps.", "warning_2": "Make sure nobody can view your screen when viewing your Private Key.", "warning_3": "Anyone with your Private Key can access your entire wallet.", - "warning_4": "Rainbow Support will never ask you for your Private Key.", + "warning_4": "OrbyPlayground Support will never ask you for your Private Key.", "show": "Show Private Key", "copied": "Private Key Copied" }, @@ -219,34 +219,34 @@ "warning_1": "Never share your Secret Recovery Phrase or enter it into any apps.", "warning_2": "Make sure nobody can view your screen when viewing your Secret Recovery Phrase.", "warning_3": "Anyone with your Secret Recovery Phrase can access your entire wallet.", - "warning_4": "Rainbow Support will never ask you for your Secret Recovery Phrase.", + "warning_4": "OrbyPlayground Support will never ask you for your Secret Recovery Phrase.", "show": "Show Recovery Phrase", "copied": "Recovery Phrase Copied" }, "wipe_wallets": { - "delete": "Reset Rainbow", + "delete": "Reset OrbyPlayground", "acknowlegement": "I have read and acknowledge the above info.", - "warning_one": "Resetting Rainbow will reset your password, & remove all wallets.", - "warning_two": "This will also erase all data & settings in Rainbow.", + "warning_one": "Resetting OrbyPlayground will reset your password, & remove all wallets.", + "warning_two": "This will also erase all data & settings in OrbyPlayground.", "warning_three": "Ensure your Secret Recovery Phrase is secure before proceeding. ", "warning_four": "Your assets & funds are accessible with the correct recovery information.", - "button_wipe": "Reset Rainbow", + "button_wipe": "Reset OrbyPlayground", "button_complete": "Complete Check Above", - "wipe_confirmation": "Reset Rainbow", - "wipe_confirmation_desc": "Are you sure you want to reset the Rainbow browser extension?", + "wipe_confirmation": "Reset OrbyPlayground", + "wipe_confirmation_desc": "Are you sure you want to reset the OrbyPlayground browser extension?", "wipe_confirmation_button": "Reset", "wipe_confirmation_cancel": "Cancel" }, "wipe_wallet_group": { "delete": "Remove Wallet Group", "acknowlegement": "I have backed up my Secret Recovery Phrase.", - "warning_one": "Removing your Wallet Group will delete the Recovery Phrase from Rainbow.", + "warning_one": "Removing your Wallet Group will delete the Recovery Phrase from OrbyPlayground.", "warning_two": "Your assets & funds are recoverable with the correct wallet recovery information.", "warning_three": "Ensure your Secret Recovery Phrase is backed up before proceeding. ", - "button_wipe": "Reset Rainbow", + "button_wipe": "Reset OrbyPlayground", "button_complete": "Complete Check Above", "wipe_confirmation": "Remove Wallet Group", - "wipe_confirmation_desc": "Are you sure you want to remove your Secret Recovery Phrase from Rainbow?", + "wipe_confirmation_desc": "Are you sure you want to remove your Secret Recovery Phrase from OrbyPlayground?", "wipe_confirmation_button": "Remove", "wipe_confirmation_cancel": "Cancel" }, @@ -450,9 +450,9 @@ "my_nfts": "My NFTs", "my_nfts_watched": "NFTs", "copy_address": "Copy Address", - "view_profile": "View Rainbow Profile", + "view_profile": "View OrbyPlayground Profile", "add_wallet": "Add a Wallet", - "lock": "Lock Rainbow", + "lock": "Lock OrbyPlayground", "connected_apps": "Connected Apps", "developer_tools": "Toggle Developer Tools", "developer_tools_disabled": "Enable Developer Tools", @@ -784,7 +784,7 @@ }, "routing": { "title": "%{Action} Routing", - "description": "By default, Rainbow chooses the cheapest route possible for your %{action}. If you prefer to specifically use either 0x or 1inch, you can do that too.", + "description": "By default, OrbyPlayground chooses the cheapest route possible for your %{action}. If you prefer to specifically use either 0x or 1inch, you can do that too.", "read_more": { "open_text": "Still curious? ", "link_text": "Read more", @@ -815,7 +815,7 @@ "flashbots_on": "On", "minimum_received": "Minimum received", "via": "%{Actioning} via", - "included_fee": "Included Rainbow fee", + "included_fee": "Included OrbyPlayground fee", "use_flashbots": "Use Flashbots", "exchange_rate": "Exchange rate", "asset_contract": "%{symbol} contract", @@ -914,23 +914,23 @@ }, "unlock": { "welcome_back": "Welcome back", - "enter_password": "Enter your password to unlock Rainbow.", + "enter_password": "Enter your password to unlock OrbyPlayground.", "unlock": "Unlock", "having_trouble": "Having trouble?", "contact": "Contact", - "rainbow_support": "Rainbow Support" + "rainbow_support": "OrbyPlayground Support" }, "onboard_before_connect": { "before_connect": "Before you can connect", - "almost_done": "You’re almost done. Quickly finish setting up your wallet with Rainbow.", + "almost_done": "You’re almost done. Quickly finish setting up your wallet with OrbyPlayground.", "setup_wallet": "Set up your wallet" }, "welcome": { - "title": "Rainbow", + "title": "OrbyPlayground", "subtitle": "Explore the new world of Ethereum", "create_wallet": "Create a new wallet", "import_wallet": "Import or connect a wallet", - "disclaimer_tos": "By proceeding, you agree to Rainbow’s", + "disclaimer_tos": "By proceeding, you agree to OrbyPlayground’s", "disclaimer_tos_link": "Terms of Use", "invalid_code": "Invalid code", "join": "Join" @@ -987,9 +987,9 @@ "import_wallet_selection": { "title": "Your wallets", "description": { - "zero": "Rainbow detected 1 wallet. You can add more at any time.", - "one": "Rainbow detected 1 wallet that have been used recently. You can add more at any time.", - "other": "Rainbow detected %{count} wallets that have been used recently. You can edit or add more at any time." + "zero": "OrbyPlayground detected 1 wallet. You can add more at any time.", + "one": "OrbyPlayground detected 1 wallet that have been used recently. You can add more at any time.", + "other": "OrbyPlayground detected %{count} wallets that have been used recently. You can edit or add more at any time." }, "add_wallets": "Add wallets", "edit_wallets": "Edit wallets", @@ -1013,7 +1013,7 @@ }, "import_wallet": { "title": "Import your wallet", - "description": "Enter your Secret Recovery Phrase or a Private Key from Rainbow or any Ethereum wallet.", + "description": "Enter your Secret Recovery Phrase or a Private Key from OrbyPlayground or any Ethereum wallet.", "too_many_words": "Too many words", "too_many_chars": "Too many characters", "add_another": "Add another", @@ -1071,10 +1071,10 @@ "home_header_right": { "settings": "Settings", "qr_code": "My QR Code", - "rainbow_profile": "Rainbow Profile", + "rainbow_profile": "OrbyPlayground Profile", "guides_and_support": "Guides & Support", "share_feedback": "Share Feedback", - "lock_rainbow": "Lock Rainbow", + "lock_rainbow": "Lock OrbyPlayground", "testnet_mode": "Testnet Mode", "testnet_mode_off": "Off", "testnet_mode_on": "On" @@ -1126,8 +1126,8 @@ "allow_to_add_asset": "wants to suggest a token", "wallet_info_title": "wants to connect to your wallet", "wallet_info_description": "Allow %{appName} to view your wallets address, balance, activity and request approval for transactions.", - "add_chain_info_description": "If you approve it, this network could be used within the Rainbow Browser Extension.", - "watch_asset_info_description": "If you approve it, this asset could be used within the Rainbow Browser Extension.", + "add_chain_info_description": "If you approve it, this network could be used within the OrbyPlayground Browser Extension.", + "watch_asset_info_description": "If you approve it, this asset could be used within the OrbyPlayground Browser Extension.", "wallet": "Wallet", "network": "Network", "switch_networks": "Switch Networks", @@ -1617,14 +1617,14 @@ } }, "wallet_ready": { - "title": "Rainbow is ready to use", - "subtitle": "To access your wallet, just click Rainbow from the Extensions drop-down, or use the speedy keyboard shortcut", - "open": "Open Rainbow", - "get_started_with_rainbow": "Get started with Rainbow", - "get_started_with_rainbow_desc": "Welcome to Rainbow! We're so glad you're here. We’ve created this guide to help with the basics of Rainbow.", + "title": "OrbyPlayground is ready to use", + "subtitle": "To access your wallet, just click OrbyPlayground from the Extensions drop-down, or use the speedy keyboard shortcut", + "open": "Open OrbyPlayground", + "get_started_with_rainbow": "Get started with OrbyPlayground", + "get_started_with_rainbow_desc": "Welcome to OrbyPlayground! We're so glad you're here. We’ve created this guide to help with the basics of OrbyPlayground.", "discover_shortcuts": "Discover shortcuts", "discover_shortcuts_desc": "Looking to discover more shortcuts? Check out this helpful guide we’ve put together for you.", - "pin_rainbow_to_your_toolbar": "Pin Rainbow to your toolbar" + "pin_rainbow_to_your_toolbar": "Pin OrbyPlayground to your toolbar" }, "wallet_header": { "buy": "Buy", @@ -1640,9 +1640,9 @@ "verified": "Verified", "on_other_networks": "On other networks", "bridge": "Bridge", - "popular": "Popular in Rainbow" + "popular": "Popular in OrbyPlayground" }, - "verified_by_rainbow": "Verified by Rainbow" + "verified_by_rainbow": "Verified by OrbyPlayground" }, "explainers": { "sidechains": { diff --git a/static/json/languages/es_419.json b/static/json/languages/es_419.json index 27155c11cd..b8c4cf0dec 100644 --- a/static/json/languages/es_419.json +++ b/static/json/languages/es_419.json @@ -903,7 +903,7 @@ "unlock": "Desbloquear", "having_trouble": "¿Tienes problemas?", "contact": "Contacto", - "rainbow_support": "Soporte de Rainbow" + "rainbow_support": "Soporte de OrbyPlayground" }, "onboard_before_connect": { "before_connect": "Antes de que puedas conectar", @@ -911,7 +911,7 @@ "setup_wallet": "Configurar tu billetera" }, "welcome": { - "title": "Rainbow", + "title": "OrbyPlayground", "subtitle": "Explora el nuevo mundo de Ethereum", "create_wallet": "Crear una nueva billetera", "import_wallet": "Importar o conectar una billetera", diff --git a/static/manifest.json b/static/manifest.json index 70503cb762..b87b2cfc42 100644 --- a/static/manifest.json +++ b/static/manifest.json @@ -55,7 +55,7 @@ "notifications" ], "short_name": "OrbyPlayground", - "version": "0.0.1", + "version": "0.0.5", "web_accessible_resources": [ { "matches": [""], diff --git a/yarn.lock b/yarn.lock index 85750d2cf6..fd5449777a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,16 +12,16 @@ resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz#d2a39395c587e092d77cbbc80acf956a54f38bf7" integrity sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q== +"@adraffy/ens-normalize@1.10.1": + version "1.10.1" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz#63430d04bd8c5e74f8d7d049338f1cd9d4f02069" + integrity sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw== + "@adraffy/ens-normalize@^1.10.1": version "1.11.0" resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz#42cc67c5baa407ac25059fcd7d405cc5ecdb0c33" integrity sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg== -"@alloc/quick-lru@^5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" - integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== - "@ampproject/remapping@^2.1.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" @@ -482,6 +482,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.25.0": + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" + integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.18.10", "@babel/template@^7.3.3": version "7.18.10" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" @@ -1980,18 +1987,6 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== -"@isaacs/cliui@^8.0.2": - version "8.0.2" - resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" - integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== - dependencies: - string-width "^5.1.2" - string-width-cjs "npm:string-width@^4.2.0" - strip-ansi "^7.0.1" - strip-ansi-cjs "npm:strip-ansi@^6.0.1" - wrap-ansi "^8.1.0" - wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" - "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -2567,11 +2562,6 @@ resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz#64df34e2f12e68e78ac57e571d25ec07fa460ca9" integrity sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ== -"@lit-labs/ssr-dom-shim@^1.2.0": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz#2f3a8f1d688935c704dbc89132394a41029acbb8" - integrity sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ== - "@lit/reactive-element@^1.3.0", "@lit/reactive-element@^1.6.0": version "1.6.2" resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.6.2.tgz#c256690f82f2d7d0ffb0b1cdf68dcb1ec86cea28" @@ -2579,13 +2569,6 @@ dependencies: "@lit-labs/ssr-dom-shim" "^1.0.0" -"@lit/reactive-element@^2.0.0", "@lit/reactive-element@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.0.4.tgz#8f2ed950a848016383894a26180ff06c56ae001b" - integrity sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ== - dependencies: - "@lit-labs/ssr-dom-shim" "^1.2.0" - "@mdn/browser-compat-data@5.2.42": version "5.2.42" resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-5.2.42.tgz#c6672c6008ca36846c46930d39c8aa342cff85d3" @@ -2925,6 +2908,13 @@ dependencies: "@noble/hashes" "1.4.0" +"@noble/curves@^1.4.2": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.8.1.tgz#19bc3970e205c99e4bdb1c64a4785706bce497ff" + integrity sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ== + dependencies: + "@noble/hashes" "1.7.1" + "@noble/curves@~1.4.0": version "1.4.2" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.2.tgz#40309198c76ed71bc6dbf7ba24e81ceb4d0d1fe9" @@ -2962,6 +2952,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.1.tgz#df6e5943edcea504bac61395926d6fd67869a0d5" integrity sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w== +"@noble/hashes@1.7.1", "@noble/hashes@^1.2.0": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.1.tgz#5738f6d765710921e7a751e00c20ae091ed8db0f" + integrity sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -3040,46 +3035,38 @@ resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca" integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q== -"@orb-labs/orby-core-mini@^0.0.4": - version "0.0.4" - resolved "https://registry.yarnpkg.com/@orb-labs/orby-core-mini/-/orby-core-mini-0.0.4.tgz#4d6938972fe4613dd05b9af05779027014083b9f" - integrity sha512-fJ4mUqsZAr9wO3HtUnJiW1L5JpflbKHPLYQdHVd/waYusnCLo6Vwo44faW9y2akD+BnfUkGpsEotr1w5veBJXQ== +"@orb-labs/orby-core-mini@0.0.9": + version "0.0.9" + resolved "https://registry.yarnpkg.com/@orb-labs/orby-core-mini/-/orby-core-mini-0.0.9.tgz#7e5b92764b78297e83a6f942a71378f0a4fe5710" + integrity sha512-08WNoKBUWjd/PKWL7LE+y9FGF/hQNqFINKcvkFUOBhfb71oKMwPFQpiKP6afK8zsjQHvr2LgDzvJQmDfW5XqMA== dependencies: node-fetch "^3.3.2" -"@orb-labs/orby-core@^0.0.12": - version "0.0.12" - resolved "https://registry.yarnpkg.com/@orb-labs/orby-core/-/orby-core-0.0.12.tgz#3368ea3a36c55a9615d0fefdf59dc1436728c65f" - integrity sha512-gcIlhcTsZYvNUNlbttW6+028ot3bRiQw3mnnB4ExtKISCPPt6ZUUaEJPkefV2JDUbpU6mePQSrN6dr/jjgqvqg== +"@orb-labs/orby-core@0.0.21": + version "0.0.21" + resolved "https://registry.yarnpkg.com/@orb-labs/orby-core/-/orby-core-0.0.21.tgz#6059b5f82c7c9af6b8ec749aa9607c3f47922949" + integrity sha512-kfUdr5RC7I0qL8ZfF5vBJP7nKpKXC67DNEzY/2nAlmIiZX6JLjqqpAtpxnQT561ykcRmkf/naV6nfRrwnNl1pw== dependencies: "@uniswap/sdk-core" "^5.9.0" jsbi "^3.1.4" -"@orb-labs/orby-react@0.0.30": - version "0.0.30" - resolved "https://registry.yarnpkg.com/@orb-labs/orby-react/-/orby-react-0.0.30.tgz#08beb94eb977268988b760b5d6611c1f9314a85b" - integrity sha512-0tr4doZj9mw7FmjdcKh3rUp32DwsJfTR0ZsN41Y5rppaZEIgy2aVk4nlakdLguWZWOOZKZ6r0L768NwOxzrh6A== - dependencies: - "@orb-labs/orby-core-mini" "^0.0.4" - "@orb-labs/orby-viem-extension" "^0.0.11" - "@uidotdev/usehooks" "^2.4.1" - "@web3modal/wagmi" "^5.0.10" - autoprefixer "^10.4.16" - ethers "^5.7.2" - lodash.isequal "^4.5.0" - lodash.uniq "4.5.0" - postcss "^8.4.33" - react-icons "^5.3.0" - swr "^2.2.5" - tailwindcss "^3.4.1" +"@orb-labs/orby-react@0.0.57": + version "0.0.57" + resolved "https://registry.yarnpkg.com/@orb-labs/orby-react/-/orby-react-0.0.57.tgz#a5d9e6ff9630957ffffcaacacacd99e2b693a6f8" + integrity sha512-32jBOHpL3UfwOMQoVFK3maP9KuRgLLwUUFwFCr1Z6voxcJCFmYd+Tr14oJgq5kps+L86UcqBXkXQbDHcYNKwOw== + dependencies: + "@orb-labs/orby-core" "0.0.21" + "@orb-labs/orby-core-mini" "0.0.9" + "@orb-labs/orby-viem-extension" "0.0.20" viem "^2.9.25" -"@orb-labs/orby-viem-extension@^0.0.11": - version "0.0.11" - resolved "https://registry.yarnpkg.com/@orb-labs/orby-viem-extension/-/orby-viem-extension-0.0.11.tgz#f18546a033f9cf0768b4535f5711fc06f03cd284" - integrity sha512-K2Tgn8v3pcuSLpFe9QpbT7RyAOy2osGZXJfcEgFd3fBSGWCwab3nAxY44tvbrBlzkHoGhXGro+xbAAqQ413YFg== +"@orb-labs/orby-viem-extension@0.0.20": + version "0.0.20" + resolved "https://registry.yarnpkg.com/@orb-labs/orby-viem-extension/-/orby-viem-extension-0.0.20.tgz#a903a0bbb8c3287eca2eb8273999ecd0d57f5bd8" + integrity sha512-V3RdNn47+mi5CDyDMIfTfcWyNRDB/6vFvwTW8G55cJrpJFMMUCQh7MbsyjpTRwLxP7DKCc/Ws7ACieXBVAaMSw== dependencies: - "@orb-labs/orby-core" "^0.0.12" + "@orb-labs/orby-core" "0.0.21" + ethers "6" "@parcel/watcher-android-arm64@2.4.1": version "2.4.1" @@ -3220,11 +3207,6 @@ tslib "^2.6.2" webcrypto-core "^1.8.0" -"@pkgjs/parseargs@^0.11.0": - version "0.11.0" - resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" - integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== - "@pkgr/utils@^2.3.1": version "2.4.2" resolved "https://registry.yarnpkg.com/@pkgr/utils/-/utils-2.4.2.tgz#9e638bbe9a6a6f165580dc943f138fd3309a2cbc" @@ -4360,6 +4342,24 @@ dependencies: buffer "~6.0.3" +"@solana/wallet-adapter-base@0.9.23": + version "0.9.23" + resolved "https://registry.yarnpkg.com/@solana/wallet-adapter-base/-/wallet-adapter-base-0.9.23.tgz#3b17c28afd44e173f44f658bf9700fd637e12a11" + integrity sha512-apqMuYwFp1jFi55NxDfvXUX2x1T0Zh07MxhZ/nCCTGys5raSfYUh82zen2BLv8BSDj/JxZ2P/s7jrQZGrX8uAw== + dependencies: + "@solana/wallet-standard-features" "^1.1.0" + "@wallet-standard/base" "^1.0.1" + "@wallet-standard/features" "^1.0.3" + eventemitter3 "^4.0.7" + +"@solana/wallet-standard-features@1.3.0", "@solana/wallet-standard-features@^1.1.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@solana/wallet-standard-features/-/wallet-standard-features-1.3.0.tgz#c489eca9d0c78f97084b4af6ca8ad8c1ca197de5" + integrity sha512-ZhpZtD+4VArf6RPitsVExvgkF+nGghd1rzPjd97GmBximpnt1rsUxMOEyoIEuH3XBxPyNB6Us7ha7RHWQR+abg== + dependencies: + "@wallet-standard/base" "^1.1.0" + "@wallet-standard/features" "^1.1.0" + "@solana/web3.js@1.90.2": version "1.90.2" resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.90.2.tgz#bb4005d4d4bd1021c0ac0c8756091a262e9ec271" @@ -4381,6 +4381,27 @@ rpc-websockets "^7.5.1" superstruct "^0.14.2" +"@solana/web3.js@1.98.0": + version "1.98.0" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.98.0.tgz#21ecfe8198c10831df6f0cfde7f68370d0405917" + integrity sha512-nz3Q5OeyGFpFCR+erX2f6JPt3sKhzhYcSycBCSPkWjzSVDh/Rr1FqTVMRe58FKO16/ivTUcuJjeS5MyBvpkbzA== + dependencies: + "@babel/runtime" "^7.25.0" + "@noble/curves" "^1.4.2" + "@noble/hashes" "^1.4.0" + "@solana/buffer-layout" "^4.0.1" + agentkeepalive "^4.5.0" + bigint-buffer "^1.1.5" + bn.js "^5.2.1" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.3" + fast-stable-stringify "^1.0.0" + jayson "^4.1.1" + node-fetch "^2.7.0" + rpc-websockets "^9.0.2" + superstruct "^2.0.2" + "@stablelib/aead@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@stablelib/aead/-/aead-1.0.1.tgz#c4b1106df9c23d1b867eb9b276d8f42d5fc4c0c3" @@ -4475,7 +4496,7 @@ "@stablelib/constant-time" "^1.0.1" "@stablelib/wipe" "^1.0.1" -"@stablelib/random@1.0.2", "@stablelib/random@^1.0.1", "@stablelib/random@^1.0.2": +"@stablelib/random@^1.0.1", "@stablelib/random@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@stablelib/random/-/random-1.0.2.tgz#2dece393636489bf7e19c51229dd7900eddf742c" integrity sha512-rIsE83Xpb7clHPVRlBj8qNe5L8ISQOzjghYQm/dZ7VaM2KHYwMW5adjQjrzTZCchFnNCNhkwtnOBa9HTMJCI8w== @@ -4506,7 +4527,7 @@ resolved "https://registry.yarnpkg.com/@stablelib/wipe/-/wipe-1.0.1.tgz#d21401f1d59ade56a62e139462a97f104ed19a36" integrity sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg== -"@stablelib/x25519@1.0.3", "@stablelib/x25519@^1.0.3": +"@stablelib/x25519@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@stablelib/x25519/-/x25519-1.0.3.tgz#13c8174f774ea9f3e5e42213cbf9fc68a3c7b7fd" integrity sha512-KnTbKmUhPhHavzobclVJQG5kuivH+qDLpe84iRqX3CLrKp881cF160JvXJ+hjn1aMyCwYOKeIZefIH/P5cJoRw== @@ -4515,6 +4536,13 @@ "@stablelib/random" "^1.0.2" "@stablelib/wipe" "^1.0.1" +"@swc/helpers@^0.5.11": + version "0.5.15" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7" + integrity sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g== + dependencies: + tslib "^2.8.0" + "@szmarczak/http-timer@^5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-5.0.1.tgz#c7c1bf1141cdd4751b0399c8fc7b8b664cd5be3a" @@ -4839,6 +4867,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708" integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g== +"@types/node@22.7.5": + version "22.7.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.5.tgz#cfde981727a7ab3611a481510b473ae54442b92b" + integrity sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ== + dependencies: + undici-types "~6.19.2" + "@types/node@>=13.7.0": version "18.15.3" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.3.tgz#f0b991c32cfc6a4e7f3399d6cb4b8cf9a0315014" @@ -5009,6 +5044,11 @@ resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.11.4.tgz#62e393f8bc4bd8a06154d110c7d042a93751def3" integrity sha512-uO4CD2ELOjw8tasUrAhvnn2W4A0ZECOvMjCivJr4gA9pGgjv+qxKWY9GLTMVEK8ej85BxQOocUyE7hImmSQYcg== +"@types/uuid@^8.3.4": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + "@types/validator@13.7.12": version "13.7.12" resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.7.12.tgz#a285379b432cc8d103b69d223cbb159a253cf2f7" @@ -5073,6 +5113,13 @@ dependencies: "@types/node" "*" +"@types/ws@^8.2.2": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.0.tgz#8a2ec491d6f0685ceaab9a9b7ff44146236993b5" + integrity sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" @@ -5223,11 +5270,6 @@ "@typescript-eslint/types" "6.4.1" eslint-visitor-keys "^3.4.1" -"@uidotdev/usehooks@^2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@uidotdev/usehooks/-/usehooks-2.4.1.tgz#4b733eaeae09a7be143c6c9ca158b56cc1ea75bf" - integrity sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg== - "@uniswap/sdk-core@^5.9.0": version "5.9.0" resolved "https://registry.yarnpkg.com/@uniswap/sdk-core/-/sdk-core-5.9.0.tgz#8f1edf4d0e94b314f4394fa5abe0bf5fc9c5a79a" @@ -5482,6 +5524,18 @@ mipd "0.0.5" zustand "4.4.1" +"@wallet-standard/base@1.1.0", "@wallet-standard/base@^1.0.1", "@wallet-standard/base@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@wallet-standard/base/-/base-1.1.0.tgz#214093c0597a1e724ee6dbacd84191dfec62bb33" + integrity sha512-DJDQhjKmSNVLKWItoKThJS+CsJQjR9AOBOirBVT1F9YpRyC9oYHE+ZnSf8y8bxUphtKqdQMPVQ2mHohYdRvDVQ== + +"@wallet-standard/features@1.1.0", "@wallet-standard/features@^1.0.3", "@wallet-standard/features@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@wallet-standard/features/-/features-1.1.0.tgz#f256d7b18940c8d134f66164330db358a8f5200e" + integrity sha512-hiEivWNztx73s+7iLxsuD1sOJ28xtRix58W7Xnz4XzzA/pF0+aicnWgjOdA10doVDEDZdUuZCIIqG96SFNlDUg== + dependencies: + "@wallet-standard/base" "^1.1.0" + "@walletconnect/core@2.11.2": version "2.11.2" resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.11.2.tgz#35286be92c645fa461fecc0dfe25de9f076fca8f" @@ -5505,28 +5559,6 @@ lodash.isequal "4.5.0" uint8arrays "^3.1.0" -"@walletconnect/core@2.16.1": - version "2.16.1" - resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.16.1.tgz#019b181387792e0d284e75074b961b48193d9b6a" - integrity sha512-UlsnEMT5wwFvmxEjX8s4oju7R3zadxNbZgsFeHEsjh7uknY2zgmUe1Lfc5XU6zyPb1Jx7Nqpdx1KN485ee8ogw== - dependencies: - "@walletconnect/heartbeat" "1.2.2" - "@walletconnect/jsonrpc-provider" "1.0.14" - "@walletconnect/jsonrpc-types" "1.0.4" - "@walletconnect/jsonrpc-utils" "1.0.8" - "@walletconnect/jsonrpc-ws-connection" "1.0.14" - "@walletconnect/keyvaluestorage" "1.1.1" - "@walletconnect/logger" "2.1.2" - "@walletconnect/relay-api" "1.0.11" - "@walletconnect/relay-auth" "1.0.4" - "@walletconnect/safe-json" "1.0.2" - "@walletconnect/time" "1.0.2" - "@walletconnect/types" "2.16.1" - "@walletconnect/utils" "2.16.1" - events "3.3.0" - lodash.isequal "4.5.0" - uint8arrays "3.1.0" - "@walletconnect/environment@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/environment/-/environment-1.0.1.tgz#1d7f82f0009ab821a2ba5ad5e5a7b8ae3b214cd7" @@ -5550,23 +5582,7 @@ "@walletconnect/utils" "2.11.2" events "^3.3.0" -"@walletconnect/ethereum-provider@2.16.1": - version "2.16.1" - resolved "https://registry.yarnpkg.com/@walletconnect/ethereum-provider/-/ethereum-provider-2.16.1.tgz#4fb8a1df39104ad3fbd02579233e796f432f6d35" - integrity sha512-oD7DNCssUX3plS5gGUZ9JQ63muQB/vxO68X6RzD2wd8gBsYtSPw4BqYFc7KTO6dUizD6gfPirw32yW2pTvy92w== - dependencies: - "@walletconnect/jsonrpc-http-connection" "1.0.8" - "@walletconnect/jsonrpc-provider" "1.0.14" - "@walletconnect/jsonrpc-types" "1.0.4" - "@walletconnect/jsonrpc-utils" "1.0.8" - "@walletconnect/modal" "2.6.2" - "@walletconnect/sign-client" "2.16.1" - "@walletconnect/types" "2.16.1" - "@walletconnect/universal-provider" "2.16.1" - "@walletconnect/utils" "2.16.1" - events "3.3.0" - -"@walletconnect/events@1.0.1", "@walletconnect/events@^1.0.1": +"@walletconnect/events@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/events/-/events-1.0.1.tgz#2b5f9c7202019e229d7ccae1369a9e86bda7816c" integrity sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ== @@ -5583,25 +5599,6 @@ "@walletconnect/time" "^1.0.2" tslib "1.14.1" -"@walletconnect/heartbeat@1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@walletconnect/heartbeat/-/heartbeat-1.2.2.tgz#e8dc5179db7769950c6f9cf59b23516d9b95227d" - integrity sha512-uASiRmC5MwhuRuf05vq4AT48Pq8RMi876zV8rr8cV969uTOzWdB/k+Lj5yI2PBtB1bGQisGen7MM1GcZlQTBXw== - dependencies: - "@walletconnect/events" "^1.0.1" - "@walletconnect/time" "^1.0.2" - events "^3.3.0" - -"@walletconnect/jsonrpc-http-connection@1.0.8": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-http-connection/-/jsonrpc-http-connection-1.0.8.tgz#2f4c3948f074960a3edd07909560f3be13e2c7ae" - integrity sha512-+B7cRuaxijLeFDJUq5hAzNyef3e3tBDIxyaCNmFtjwnod5AGis3RToNqzFU33vpVcxFhofkpE7Cx+5MYejbMGw== - dependencies: - "@walletconnect/jsonrpc-utils" "^1.0.6" - "@walletconnect/safe-json" "^1.0.1" - cross-fetch "^3.1.4" - events "^3.3.0" - "@walletconnect/jsonrpc-http-connection@^1.0.7": version "1.0.7" resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-http-connection/-/jsonrpc-http-connection-1.0.7.tgz#a6973569b8854c22da707a759d241e4f5c2d5a98" @@ -5621,15 +5618,6 @@ "@walletconnect/safe-json" "^1.0.2" tslib "1.14.1" -"@walletconnect/jsonrpc-provider@1.0.14": - version "1.0.14" - resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-provider/-/jsonrpc-provider-1.0.14.tgz#696f3e3b6d728b361f2e8b853cfc6afbdf2e4e3e" - integrity sha512-rtsNY1XqHvWj0EtITNeuf8PHMvlCLiS3EjQL+WOkxEOA4KPxsohFnBDeyPYiNm4ZvkQdLnece36opYidmtbmow== - dependencies: - "@walletconnect/jsonrpc-utils" "^1.0.8" - "@walletconnect/safe-json" "^1.0.2" - events "^3.3.0" - "@walletconnect/jsonrpc-types@1.0.3", "@walletconnect/jsonrpc-types@^1.0.2", "@walletconnect/jsonrpc-types@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.3.tgz#65e3b77046f1a7fa8347ae02bc1b841abe6f290c" @@ -5638,14 +5626,6 @@ keyvaluestorage-interface "^1.0.0" tslib "1.14.1" -"@walletconnect/jsonrpc-types@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.4.tgz#ce1a667d79eadf2a2d9d002c152ceb68739c230c" - integrity sha512-P6679fG/M+wuWg9TY8mh6xFSdYnFyFjwFelxyISxMDrlbXokorEVXYOxiqEbrU3x1BmBoCAJJ+vtEaEoMlpCBQ== - dependencies: - events "^3.3.0" - keyvaluestorage-interface "^1.0.0" - "@walletconnect/jsonrpc-utils@1.0.8", "@walletconnect/jsonrpc-utils@^1.0.6", "@walletconnect/jsonrpc-utils@^1.0.7", "@walletconnect/jsonrpc-utils@^1.0.8": version "1.0.8" resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-utils/-/jsonrpc-utils-1.0.8.tgz#82d0cc6a5d6ff0ecc277cb35f71402c91ad48d72" @@ -5665,7 +5645,7 @@ events "^3.3.0" ws "^7.5.1" -"@walletconnect/keyvaluestorage@1.1.1", "@walletconnect/keyvaluestorage@^1.1.1": +"@walletconnect/keyvaluestorage@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz#dd2caddabfbaf80f6b8993a0704d8b83115a1842" integrity sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA== @@ -5674,14 +5654,6 @@ idb-keyval "^6.2.1" unstorage "^1.9.0" -"@walletconnect/logger@2.1.2": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@walletconnect/logger/-/logger-2.1.2.tgz#813c9af61b96323a99f16c10089bfeb525e2a272" - integrity sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw== - dependencies: - "@walletconnect/safe-json" "^1.0.2" - pino "7.11.0" - "@walletconnect/logger@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/logger/-/logger-2.0.1.tgz#7f489b96e9a1ff6bf3e58f0fbd6d69718bf844a8" @@ -5715,13 +5687,6 @@ "@walletconnect/modal-core" "2.6.2" "@walletconnect/modal-ui" "2.6.2" -"@walletconnect/relay-api@1.0.11": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@walletconnect/relay-api/-/relay-api-1.0.11.tgz#80ab7ef2e83c6c173be1a59756f95e515fb63224" - integrity sha512-tLPErkze/HmC9aCmdZOhtVmYZq1wKfWTJtygQHoWtgg722Jd4homo54Cs4ak2RUFUZIGO2RsOpIcWipaua5D5Q== - dependencies: - "@walletconnect/jsonrpc-types" "^1.0.2" - "@walletconnect/relay-api@^1.0.9": version "1.0.9" resolved "https://registry.yarnpkg.com/@walletconnect/relay-api/-/relay-api-1.0.9.tgz#f8c2c3993dddaa9f33ed42197fc9bfebd790ecaf" @@ -5730,7 +5695,7 @@ "@walletconnect/jsonrpc-types" "^1.0.2" tslib "1.14.1" -"@walletconnect/relay-auth@1.0.4", "@walletconnect/relay-auth@^1.0.4": +"@walletconnect/relay-auth@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@walletconnect/relay-auth/-/relay-auth-1.0.4.tgz#0b5c55c9aa3b0ef61f526ce679f3ff8a5c4c2c7c" integrity sha512-kKJcS6+WxYq5kshpPaxGHdwf5y98ZwbfuS4EE/NkQzqrDFm5Cj+dP8LofzWvjrrLkZq7Afy7WrQMXdLy8Sx7HQ== @@ -5742,7 +5707,7 @@ tslib "1.14.1" uint8arrays "^3.0.0" -"@walletconnect/safe-json@1.0.2", "@walletconnect/safe-json@^1.0.1", "@walletconnect/safe-json@^1.0.2": +"@walletconnect/safe-json@^1.0.1", "@walletconnect/safe-json@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@walletconnect/safe-json/-/safe-json-1.0.2.tgz#7237e5ca48046e4476154e503c6d3c914126fa77" integrity sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA== @@ -5764,22 +5729,7 @@ "@walletconnect/utils" "2.11.2" events "^3.3.0" -"@walletconnect/sign-client@2.16.1": - version "2.16.1" - resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.16.1.tgz#94a2f630ba741bd180f540c53576c5ceaace4857" - integrity sha512-s2Tx2n2duxt+sHtuWXrN9yZVaHaYqcEcjwlTD+55/vs5NUPlISf+fFmZLwSeX1kUlrSBrAuxPUcqQuRTKcjLOA== - dependencies: - "@walletconnect/core" "2.16.1" - "@walletconnect/events" "1.0.1" - "@walletconnect/heartbeat" "1.2.2" - "@walletconnect/jsonrpc-utils" "1.0.8" - "@walletconnect/logger" "2.1.2" - "@walletconnect/time" "1.0.2" - "@walletconnect/types" "2.16.1" - "@walletconnect/utils" "2.16.1" - events "3.3.0" - -"@walletconnect/time@1.0.2", "@walletconnect/time@^1.0.2": +"@walletconnect/time@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@walletconnect/time/-/time-1.0.2.tgz#6c5888b835750ecb4299d28eecc5e72c6d336523" integrity sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g== @@ -5798,18 +5748,6 @@ "@walletconnect/logger" "^2.0.1" events "^3.3.0" -"@walletconnect/types@2.16.1": - version "2.16.1" - resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.16.1.tgz#6583d458d3f7b1919d482ba516ccb7878ec8c91f" - integrity sha512-9P4RG4VoDEF+yBF/n2TF12gsvT/aTaeZTVDb/AOayafqiPnmrQZMKmNCJJjq1sfdsDcHXFcZWMGsuCeSJCmrXA== - dependencies: - "@walletconnect/events" "1.0.1" - "@walletconnect/heartbeat" "1.2.2" - "@walletconnect/jsonrpc-types" "1.0.4" - "@walletconnect/keyvaluestorage" "1.1.1" - "@walletconnect/logger" "2.1.2" - events "3.3.0" - "@walletconnect/universal-provider@2.11.2": version "2.11.2" resolved "https://registry.yarnpkg.com/@walletconnect/universal-provider/-/universal-provider-2.11.2.tgz#bec3038f51445d707bbec75f0cb8af0a1f1e04db" @@ -5825,21 +5763,6 @@ "@walletconnect/utils" "2.11.2" events "^3.3.0" -"@walletconnect/universal-provider@2.16.1": - version "2.16.1" - resolved "https://registry.yarnpkg.com/@walletconnect/universal-provider/-/universal-provider-2.16.1.tgz#6d52c41c7388e01f89007956a1117748ab9a11e4" - integrity sha512-q/tyWUVNenizuClEiaekx9FZj/STU1F3wpDK4PUIh3xh+OmUI5fw2dY3MaNDjyb5AyrS0M8BuQDeuoSuOR/Q7w== - dependencies: - "@walletconnect/jsonrpc-http-connection" "1.0.8" - "@walletconnect/jsonrpc-provider" "1.0.14" - "@walletconnect/jsonrpc-types" "1.0.4" - "@walletconnect/jsonrpc-utils" "1.0.8" - "@walletconnect/logger" "2.1.2" - "@walletconnect/sign-client" "2.16.1" - "@walletconnect/types" "2.16.1" - "@walletconnect/utils" "2.16.1" - events "3.3.0" - "@walletconnect/utils@2.11.2": version "2.11.2" resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.11.2.tgz#dee0f19adf5e38543612cbe9fa4de7ed28eb7e85" @@ -5860,36 +5783,14 @@ query-string "7.1.3" uint8arrays "^3.1.0" -"@walletconnect/utils@2.16.1": - version "2.16.1" - resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.16.1.tgz#2099cc2bd16b0edc32022f64aa2c2c323b45d1d4" - integrity sha512-aoQirVoDoiiEtYeYDtNtQxFzwO/oCrz9zqeEEXYJaAwXlGVTS34KFe7W3/Rxd/pldTYKFOZsku2EzpISfH8Wsw== - dependencies: - "@stablelib/chacha20poly1305" "1.0.1" - "@stablelib/hkdf" "1.0.1" - "@stablelib/random" "1.0.2" - "@stablelib/sha256" "1.0.1" - "@stablelib/x25519" "1.0.3" - "@walletconnect/relay-api" "1.0.11" - "@walletconnect/relay-auth" "1.0.4" - "@walletconnect/safe-json" "1.0.2" - "@walletconnect/time" "1.0.2" - "@walletconnect/types" "2.16.1" - "@walletconnect/window-getters" "1.0.1" - "@walletconnect/window-metadata" "1.0.1" - detect-browser "5.3.0" - elliptic "^6.5.7" - query-string "7.1.3" - uint8arrays "3.1.0" - -"@walletconnect/window-getters@1.0.1", "@walletconnect/window-getters@^1.0.1": +"@walletconnect/window-getters@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/window-getters/-/window-getters-1.0.1.tgz#f36d1c72558a7f6b87ecc4451fc8bd44f63cbbdc" integrity sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q== dependencies: tslib "1.14.1" -"@walletconnect/window-metadata@1.0.1", "@walletconnect/window-metadata@^1.0.1": +"@walletconnect/window-metadata@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/window-metadata/-/window-metadata-1.0.1.tgz#2124f75447b7e989e4e4e1581d55d25bc75f7be5" integrity sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA== @@ -5907,118 +5808,6 @@ loglevel-plugin-prefix "^0.8.4" strip-ansi "^7.1.0" -"@web3modal/base@5.1.11": - version "5.1.11" - resolved "https://registry.yarnpkg.com/@web3modal/base/-/base-5.1.11.tgz#11beaca17fd0193d249174fb20da7bbb605abb2c" - integrity sha512-wJCsqQ1FG0Isiv0Exaz2Sv+FpijVmNPNay+sGdV5HP2SpBAR/1xxHca2/vLBdACX7rYAFAj723DYQE0fmUpIaw== - dependencies: - "@walletconnect/utils" "2.16.1" - "@web3modal/common" "5.1.11" - "@web3modal/core" "5.1.11" - "@web3modal/polyfills" "5.1.11" - "@web3modal/scaffold-ui" "5.1.11" - "@web3modal/scaffold-utils" "5.1.11" - "@web3modal/siwe" "5.1.11" - "@web3modal/ui" "5.1.11" - "@web3modal/wallet" "5.1.11" - optionalDependencies: - borsh "0.7.0" - bs58 "5.0.0" - -"@web3modal/common@5.1.11": - version "5.1.11" - resolved "https://registry.yarnpkg.com/@web3modal/common/-/common-5.1.11.tgz#29f6a0df6d6e1df7c3adb619efab08a6f20d4eab" - integrity sha512-YfSklKjjiM1RGxFTQm3ycYZ2Ktb6vswt9eg8lGXRknxN+SC7bCtuvgtyyCO0Z9/f9dPMOGIAmoJ/y6WHXWQqcg== - dependencies: - bignumber.js "9.1.2" - dayjs "1.11.10" - -"@web3modal/core@5.1.11": - version "5.1.11" - resolved "https://registry.yarnpkg.com/@web3modal/core/-/core-5.1.11.tgz#96406333c00ca949dbd1e8469e05b65d9c15551e" - integrity sha512-ugUVFVml1vVW+V7yxkn/AYYdrUJzn4ulFbDlxDMpmukKY6sDYLMMGAJ84O8ZC/OPyC7009NYd3mKZurxEyWkHw== - dependencies: - "@web3modal/common" "5.1.11" - "@web3modal/wallet" "5.1.11" - valtio "1.11.2" - -"@web3modal/polyfills@5.1.11": - version "5.1.11" - resolved "https://registry.yarnpkg.com/@web3modal/polyfills/-/polyfills-5.1.11.tgz#15f946e22c8d97dd43edc6fa8b7ff724c80e613d" - integrity sha512-BDIDYA2LGTCquahbZ+wyWQy4IBOPeKVSgt4ZpFir1fnVJUPkEluSwZStcKLtCzQvxJgER1sLicUrjJQHF36TOg== - dependencies: - buffer "6.0.3" - -"@web3modal/scaffold-ui@5.1.11": - version "5.1.11" - resolved "https://registry.yarnpkg.com/@web3modal/scaffold-ui/-/scaffold-ui-5.1.11.tgz#8e0e30c5da898b23b63dc4da5b9682d6ce99ca67" - integrity sha512-fBqzd7DStUaEjtdbEU86rzY4XIgt8c8JN8oxS/xnUEopmjFYvBLCCVEfbTkZyJrRvAAphz7+oS4TVzXw9k6t5A== - dependencies: - "@web3modal/common" "5.1.11" - "@web3modal/core" "5.1.11" - "@web3modal/scaffold-utils" "5.1.11" - "@web3modal/siwe" "5.1.11" - "@web3modal/ui" "5.1.11" - "@web3modal/wallet" "5.1.11" - lit "3.1.0" - -"@web3modal/scaffold-utils@5.1.11": - version "5.1.11" - resolved "https://registry.yarnpkg.com/@web3modal/scaffold-utils/-/scaffold-utils-5.1.11.tgz#85d880ca2ddea253ffb2f9fbccb9c9c3922ad107" - integrity sha512-4bcYpQ3oxak5mDZMW5/7ayrhpaJHy6dCfUio15AGPHnQlFjkqcfSuuG0Io8Oj8VUXcK2UBLch9YiEDz4Xgce9Q== - dependencies: - "@web3modal/common" "5.1.11" - "@web3modal/core" "5.1.11" - "@web3modal/polyfills" "5.1.11" - "@web3modal/wallet" "5.1.11" - valtio "1.11.2" - -"@web3modal/siwe@5.1.11": - version "5.1.11" - resolved "https://registry.yarnpkg.com/@web3modal/siwe/-/siwe-5.1.11.tgz#f68a43e7d5c5417ebfb85f82ce3478db4c5bc780" - integrity sha512-1aKEtMosACyY0SRjHjdcA/g3bRtMojTxlK7S/T6zBk57X/P3xcEZq9J8UM73plmGewjZdLaqGMgv6B/k/WleZQ== - dependencies: - "@walletconnect/utils" "2.16.1" - "@web3modal/common" "5.1.11" - "@web3modal/core" "5.1.11" - "@web3modal/scaffold-utils" "5.1.11" - "@web3modal/ui" "5.1.11" - "@web3modal/wallet" "5.1.11" - lit "3.1.0" - valtio "1.11.2" - -"@web3modal/ui@5.1.11": - version "5.1.11" - resolved "https://registry.yarnpkg.com/@web3modal/ui/-/ui-5.1.11.tgz#1bb5bdf3a54bbdf7d0068fdb65a46bc921da160c" - integrity sha512-L0L+2YOK+ONx+W7GPtkSdKZuAQ8cjcS5N8kp+WZzKOMUTeDLuXKtSnES4p/ShOVmkpV6qB8r0pPA9xgFh1D3ow== - dependencies: - lit "3.1.0" - qrcode "1.5.3" - -"@web3modal/wagmi@^5.0.10": - version "5.1.11" - resolved "https://registry.yarnpkg.com/@web3modal/wagmi/-/wagmi-5.1.11.tgz#19835c4905458d879b797da556acbf9a9ab721ee" - integrity sha512-etV1qfBVvh41EMuBHXUpcO/W818jZVNh5/l9Z5kqRPZxlQmBaJbt5mTzw6nw/Lujoe1yYKugGQFhgjfEQK+eyA== - dependencies: - "@walletconnect/ethereum-provider" "2.16.1" - "@walletconnect/utils" "2.16.1" - "@web3modal/base" "5.1.11" - "@web3modal/common" "5.1.11" - "@web3modal/polyfills" "5.1.11" - "@web3modal/scaffold-utils" "5.1.11" - "@web3modal/siwe" "5.1.11" - "@web3modal/wallet" "5.1.11" - -"@web3modal/wallet@5.1.11": - version "5.1.11" - resolved "https://registry.yarnpkg.com/@web3modal/wallet/-/wallet-5.1.11.tgz#3118bb1fa370436c252d7d97c731eac585cdb8a7" - integrity sha512-/ooQZXK1h7LGBUemebldYPAV2oJAgxkgSiCMoHWynhuS0LO3BzhOhGL+jV19w4iU81bS1GSNFTxYT9LL6Scesw== - dependencies: - "@walletconnect/logger" "2.1.2" - "@web3modal/common" "5.1.11" - "@web3modal/polyfills" "5.1.11" - zod "3.22.4" - "@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" @@ -6356,6 +6145,11 @@ aes-js@3.0.0: resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" integrity sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw== +aes-js@4.0.0-beta.5: + version "4.0.0-beta.5" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-4.0.0-beta.5.tgz#8d2452c52adedebc3a3e28465d858c11ca315873" + integrity sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q== + agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -6538,11 +6332,6 @@ are-we-there-yet@^3.0.0: delegates "^1.0.0" readable-stream "^3.6.0" -arg@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" - integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== - argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -6799,18 +6588,6 @@ audit-ci@6.3.0: semver "^7.0.0" yargs "^17.0.0" -autoprefixer@^10.4.16: - version "10.4.20" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b" - integrity sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g== - dependencies: - browserslist "^4.23.3" - caniuse-lite "^1.0.30001646" - fraction.js "^4.3.7" - normalize-range "^0.1.2" - picocolors "^1.0.1" - postcss-value-parser "^4.2.0" - available-typed-arrays@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" @@ -6920,10 +6697,10 @@ base-x@^3.0.2: dependencies: safe-buffer "^5.0.1" -base-x@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a" - integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw== +base-x@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-5.0.1.tgz#16bf35254be1df8aca15e36b7c1dda74b2aa6b03" + integrity sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg== base64-js@^1.0.2, base64-js@^1.3.1: version "1.5.1" @@ -6974,7 +6751,7 @@ bignumber.js@9.0.1: resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== -bignumber.js@9.1.2, bignumber.js@^9.1.2: +bignumber.js@^9.1.2: version "9.1.2" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== @@ -7009,6 +6786,13 @@ bindings@1.5.0, bindings@^1.3.0: dependencies: file-uri-to-path "1.0.0" +bip39@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.1.0.tgz#c55a418deaf48826a6ceb34ac55b3ee1577e18a3" + integrity sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A== + dependencies: + "@noble/hashes" "^1.2.0" + bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -7043,7 +6827,7 @@ boolbase@^1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== -borsh@0.7.0, borsh@^0.7.0: +borsh@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/borsh/-/borsh-0.7.0.tgz#6e9560d719d86d90dc589bca60ffc8a6c51fec2a" integrity sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA== @@ -7252,22 +7036,12 @@ browserslist@^4.21.3: node-releases "^2.0.6" update-browserslist-db "^1.0.9" -browserslist@^4.23.3: - version "4.24.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.3.tgz#5fc2725ca8fb3c1432e13dac278c7cc103e026d2" - integrity sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA== - dependencies: - caniuse-lite "^1.0.30001688" - electron-to-chromium "^1.5.73" - node-releases "^2.0.19" - update-browserslist-db "^1.1.1" - -bs58@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/bs58/-/bs58-5.0.0.tgz#865575b4d13c09ea2a84622df6c8cbeb54ffc279" - integrity sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ== +bs58@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-6.0.0.tgz#a2cda0130558535dd281a2f8697df79caaf425d8" + integrity sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw== dependencies: - base-x "^4.0.0" + base-x "^5.0.0" bs58@^4.0.0, bs58@^4.0.1: version "4.0.1" @@ -7488,11 +7262,6 @@ camel-case@^4.1.2: pascal-case "^3.1.2" tslib "^2.0.3" -camelcase-css@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" - integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== - camelcase@7.0.1, camelcase@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-7.0.1.tgz#f02e50af9fd7782bc8b88a3558c32fd3a388f048" @@ -7518,11 +7287,6 @@ caniuse-lite@^1.0.30001587: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001616.tgz#4342712750d35f71ebba9fcac65e2cf8870013c3" integrity sha512-RHVYKov7IcdNjVHJFNY/78RdG4oGVjbayxv8u5IO74Wv7Hlq4PnJE6mo/OjFijjVFNy5ijnCt6H3IIo4t+wfEw== -caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001688: - version "1.0.30001688" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz#f9d3ede749f083ce0db4c13db9d828adaf2e8d0a" - integrity sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA== - caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -7782,11 +7546,6 @@ cli-width@^3.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== -client-only@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" - integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== - clipboardy@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-4.0.0.tgz#e73ced93a76d19dd379ebf1f297565426dffdca1" @@ -7963,11 +7722,6 @@ commander@^2.20.0, commander@^2.20.3, commander@^2.6.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -commander@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" - integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== - commander@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" @@ -8163,7 +7917,7 @@ create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: ripemd160 "^2.0.1" sha.js "^2.4.0" -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: +create-hmac@1.1.7, create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== @@ -8365,11 +8119,6 @@ dateformat@~3.0.3: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== -dayjs@1.11.10: - version "1.11.10" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" - integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== - debounce@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" @@ -8649,11 +8398,6 @@ detective@^5.2.0: defined "^1.0.0" minimist "^1.2.6" -didyoumean@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" - integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== - diff-sequences@^29.2.0: version "29.2.0" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.2.0.tgz#4c55b5b40706c7b5d2c5c75999a50c56d214e8f6" @@ -8685,11 +8429,6 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -dlv@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" - integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== - doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -8883,6 +8622,14 @@ eciesjs@^0.3.15: futoin-hkdf "^1.5.3" secp256k1 "^5.0.0" +ed25519-hd-key@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ed25519-hd-key/-/ed25519-hd-key-1.3.0.tgz#e0bd2be4c07e15c753d5ed3aca30744e321b7c78" + integrity sha512-IWwAyiiuJQhgu3L8NaHb68eJxTu2pgCwxIBdgpLJdKpYZM46+AXePSVTr7fkNKaUOfOL4IrjEUaQvyVRIDP7fg== + dependencies: + create-hmac "1.1.7" + tweetnacl "1.0.3" + eip55@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/eip55/-/eip55-2.1.1.tgz#28b743c4701ac3c811b1e9fe67e39cf1d0781b96" @@ -8900,11 +8647,6 @@ electron-to-chromium@^1.4.668: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.758.tgz#f39e530cae2ca4329a0f0e1840629d8d1da73156" integrity sha512-/o9x6TCdrYZBMdGeTifAP3wlF/gVT+TtWJe3BSmtNh92Mw81U9hrYwW9OAGUh+sEOX/yz5e34sksqRruZbjYrw== -electron-to-chromium@^1.5.73: - version "1.5.73" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz#f32956ce40947fa3c8606726a96cd8fb5bb5f720" - integrity sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg== - elliptic@6.5.4, elliptic@^6.5.3, elliptic@^6.5.4: version "6.5.4" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" @@ -8918,19 +8660,6 @@ elliptic@6.5.4, elliptic@^6.5.3, elliptic@^6.5.4: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" -elliptic@^6.5.7: - version "6.6.1" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.6.1.tgz#3b8ffb02670bf69e382c7f65bf524c97c5405c06" - integrity sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g== - dependencies: - bn.js "^4.11.9" - brorand "^1.1.0" - hash.js "^1.0.0" - hmac-drbg "^1.0.1" - inherits "^2.0.4" - minimalistic-assert "^1.0.1" - minimalistic-crypto-utils "^1.0.1" - emittery@^0.10.2: version "0.10.2" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.10.2.tgz#902eec8aedb8c41938c46e9385e9db7e03182933" @@ -9368,11 +9097,6 @@ escalade@^3.1.2: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== -escalade@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" - integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== - escape-goat@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-4.0.0.tgz#9424820331b510b0666b98f7873fe11ac4aa8081" @@ -9776,7 +9500,7 @@ ethereum-cryptography@2.1.2, ethereum-cryptography@^2.0.0, ethereum-cryptography "@scure/bip32" "1.3.1" "@scure/bip39" "1.2.1" -ethers@5.7.2, ethers@^5.0.0, ethers@^5.7.2: +ethers@5.7.2, ethers@^5.0.0: version "5.7.2" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== @@ -9812,6 +9536,19 @@ ethers@5.7.2, ethers@^5.0.0, ethers@^5.7.2: "@ethersproject/web" "5.7.1" "@ethersproject/wordlists" "5.7.0" +ethers@6: + version "6.13.5" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.13.5.tgz#8c1d6ac988ac08abc3c1d8fabbd4b8b602851ac4" + integrity sha512-+knKNieu5EKRThQJWwqaJ10a6HE9sSehGeqWN65//wE7j47ZpFhKAnHB/JJFibwwg61I/koxaPsXbXpD/skNOQ== + dependencies: + "@adraffy/ens-normalize" "1.10.1" + "@noble/curves" "1.2.0" + "@noble/hashes" "1.3.2" + "@types/node" "22.7.5" + aes-js "4.0.0-beta.5" + tslib "2.7.0" + ws "8.17.1" + ethjs-util@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/ethjs-util/-/ethjs-util-0.1.6.tgz#f308b62f185f9fe6237132fb2a9818866a5cd536" @@ -9865,7 +9602,7 @@ eventemitter3@5.0.1, eventemitter3@^5.0.1: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== -events@3.3.0, events@^3.0.0, events@^3.2.0, events@^3.3.0: +events@^3.0.0, events@^3.2.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -10101,17 +9838,6 @@ fast-glob@^3.3.0: merge2 "^1.3.0" micromatch "^4.0.4" -fast-glob@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" - integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - fast-json-patch@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-3.1.1.tgz#85064ea1b1ebf97a3f7ad01e23f9337e72c66947" @@ -10407,14 +10133,6 @@ for-own@^1.0.0: dependencies: for-in "^1.0.1" -foreground-child@^3.1.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" - integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== - dependencies: - cross-spawn "^7.0.0" - signal-exit "^4.0.1" - forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -10459,11 +10177,6 @@ formdata-polyfill@^4.0.10: dependencies: fetch-blob "^3.1.2" -fraction.js@^4.3.7: - version "4.3.7" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" - integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== - framer-motion@10.2.3: version "10.2.3" resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-10.2.3.tgz#80ac552648f05b707d0595be5e87551aa673689a" @@ -10780,18 +10493,6 @@ glob@9.3.0: minipass "^4.2.4" path-scurry "^1.6.1" -glob@^10.3.10: - version "10.4.5" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" - integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== - dependencies: - foreground-child "^3.1.0" - jackspeak "^3.1.2" - minimatch "^9.0.4" - minipass "^7.1.2" - package-json-from-dist "^1.0.0" - path-scurry "^1.11.1" - glob@^6.0.1: version "6.0.4" resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" @@ -11238,13 +10939,6 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" -hasown@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -11826,13 +11520,6 @@ is-core-module@^2.13.0: dependencies: has "^1.0.3" -is-core-module@^2.16.0: - version "2.16.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.0.tgz#6c01ffdd5e33c49c1d2abfa93334a85cb56bd81c" - integrity sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g== - dependencies: - hasown "^2.0.2" - is-core-module@^2.9.0: version "2.10.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed" @@ -12253,15 +11940,6 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jackspeak@^3.1.2: - version "3.4.3" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" - integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== - dependencies: - "@isaacs/cliui" "^8.0.2" - optionalDependencies: - "@pkgjs/parseargs" "^0.11.0" - javascript-stringify@^2.0.1: version "2.1.0" resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-2.1.0.tgz#27c76539be14d8bd128219a2d731b09337904e79" @@ -12285,6 +11963,24 @@ jayson@^4.1.0: uuid "^8.3.2" ws "^7.4.5" +jayson@^4.1.1: + version "4.1.3" + resolved "https://registry.yarnpkg.com/jayson/-/jayson-4.1.3.tgz#db9be2e4287d9fef4fc05b5fe367abe792c2eee8" + integrity sha512-LtXh5aYZodBZ9Fc3j6f2w+MTNcnxteMOrb+QgIouguGOulWi0lieEkOUg+HkjjFs0DGoWDds6bi4E9hpNFLulQ== + dependencies: + "@types/connect" "^3.4.33" + "@types/node" "^12.12.54" + "@types/ws" "^7.4.4" + JSONStream "^1.3.5" + commander "^2.20.3" + delay "^5.0.0" + es6-promisify "^5.0.0" + eyes "^0.1.8" + isomorphic-ws "^4.0.1" + json-stringify-safe "^5.0.1" + uuid "^8.3.2" + ws "^7.5.10" + jed@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/jed/-/jed-1.1.1.tgz#7a549bbd9ffe1585b0cd0a191e203055bee574b4" @@ -12670,11 +12366,6 @@ jiti@^1.21.0: resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== -jiti@^1.21.6: - version "1.21.6" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" - integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== - jju@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a" @@ -13109,11 +12800,6 @@ lilconfig@^2.0.5: resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4" integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg== -lilconfig@^3.0.0, lilconfig@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4" - integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== - lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -13195,15 +12881,6 @@ lit-element@^3.3.0: "@lit/reactive-element" "^1.3.0" lit-html "^2.7.0" -lit-element@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-4.1.1.tgz#07905992815076e388cf6f1faffc7d6866c82007" - integrity sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew== - dependencies: - "@lit-labs/ssr-dom-shim" "^1.2.0" - "@lit/reactive-element" "^2.0.4" - lit-html "^3.2.0" - lit-html@^2.7.0: version "2.7.5" resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.7.5.tgz#0c1b9d381abe20c01475ae53ea4b07bf4c923eb8" @@ -13218,13 +12895,6 @@ lit-html@^2.8.0: dependencies: "@types/trusted-types" "^2.0.2" -lit-html@^3.1.0, lit-html@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-3.2.1.tgz#8fc49e3531ee5947e4d93e8a5aa642ab1649833b" - integrity sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA== - dependencies: - "@types/trusted-types" "^2.0.2" - lit@2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/lit/-/lit-2.8.0.tgz#4d838ae03059bf9cafa06e5c61d8acc0081e974e" @@ -13234,15 +12904,6 @@ lit@2.8.0: lit-element "^3.3.0" lit-html "^2.8.0" -lit@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lit/-/lit-3.1.0.tgz#76429b85dc1f5169fed499a0f7e89e2e619010c9" - integrity sha512-rzo/hmUqX8zmOdamDAeydfjsGXbbdtAFqMhmocnh2j9aDYqbu0fjXygjCa0T99Od9VQ/2itwaGrjZz/ZELVl7w== - dependencies: - "@lit/reactive-element" "^2.0.0" - lit-element "^4.0.0" - lit-html "^3.1.0" - load-bmfont@^1.3.1: version "1.4.1" resolved "https://registry.yarnpkg.com/load-bmfont/-/load-bmfont-1.4.1.tgz#c0f5f4711a1e2ccff725a7b6078087ccfcddd3e9" @@ -13308,7 +12969,7 @@ lodash.clonedeep@4.5.0: resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== -lodash.isequal@4.5.0, lodash.isequal@^4.5.0: +lodash.isequal@4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== @@ -13333,11 +12994,6 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.uniq@4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" - integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== - lodash@*, lodash@4.17.21, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@~4.17.19, lodash@~4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -13598,7 +13254,7 @@ micro-ftch@^0.3.1: resolved "https://registry.yarnpkg.com/micro-ftch/-/micro-ftch-0.3.1.tgz#6cb83388de4c1f279a034fb0cf96dfc050853c5f" integrity sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg== -micromatch@4.0.8, micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.8: +micromatch@4.0.8, micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -13680,7 +13336,7 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== -"minimatch@2 || 3", minimatch@3.0.5, minimatch@4.2.3, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2, minimatch@^5.0.1, minimatch@^7.4.1, minimatch@^9.0.4, minimatch@~3.0.4: +"minimatch@2 || 3", minimatch@3.0.5, minimatch@4.2.3, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2, minimatch@^5.0.1, minimatch@^7.4.1, minimatch@~3.0.4: version "3.0.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.5.tgz#4da8f1290ee0f0f8e83d60ca69f8f134068604a3" integrity sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw== @@ -13760,11 +13416,6 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.2.tgz#58a82b7d81c7010da5bd4b2c0c85ac4b4ec5131e" integrity sha512-eL79dXrE1q9dBbDCLg7xfn/vl7MS4F1gvJAgjJrQli/jbQWdUttuVawphqpffoIYfRdq78LHx6GP4bU/EQ2ATA== -minipass@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" - integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== - minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" @@ -13949,7 +13600,7 @@ mv@~2: ncp "~2.0.0" rimraf "~2.4.0" -mz@2.7.0, mz@^2.7.0: +mz@2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== @@ -14122,11 +13773,6 @@ node-releases@^2.0.14: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== -node-releases@^2.0.19: - version "2.0.19" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" - integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== - node-releases@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" @@ -14179,11 +13825,6 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" - integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== - normalize-url@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-8.0.0.tgz#593dbd284f743e8dcf6a5ddf8fadff149c82701a" @@ -14244,11 +13885,6 @@ object-assign@^4.0.1, object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-hash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" - integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== - object-inspect@^1.12.2, object-inspect@^1.9.0: version "1.12.2" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" @@ -14642,11 +14278,6 @@ pac-resolver@^7.0.1: degenerator "^5.0.0" netmask "^2.0.2" -package-json-from-dist@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" - integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== - package-json@^8.1.0: version "8.1.1" resolved "https://registry.yarnpkg.com/package-json/-/package-json-8.1.1.tgz#3e9948e43df40d1e8e78a85485f1070bf8f03dc8" @@ -14856,14 +14487,6 @@ path-root@^0.1.1: dependencies: path-root-regex "^0.1.0" -path-scurry@^1.11.1: - version "1.11.1" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" - integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== - dependencies: - lru-cache "^10.2.0" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry@^1.6.1: version "1.10.1" resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" @@ -14937,11 +14560,6 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picocolors@^1.0.1, picocolors@^1.1.0, picocolors@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" - integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== - picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -14957,11 +14575,6 @@ pify@5.0.0, pify@^5.0.0: resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== -pify@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== - pify@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" @@ -15027,11 +14640,6 @@ pino@8.11.0: sonic-boom "^3.1.0" thread-stream "^2.0.0" -pirates@^4.0.1: - version "4.0.6" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" - integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== - pirates@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" @@ -15091,22 +14699,6 @@ pony-cause@^2.1.10: resolved "https://registry.yarnpkg.com/pony-cause/-/pony-cause-2.1.11.tgz#d69a20aaccdb3bdb8f74dd59e5c68d8e6772e4bd" integrity sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg== -postcss-import@^15.1.0: - version "15.1.0" - resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" - integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== - dependencies: - postcss-value-parser "^4.0.0" - read-cache "^1.0.0" - resolve "^1.1.7" - -postcss-js@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2" - integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw== - dependencies: - camelcase-css "^2.0.1" - postcss-load-config@^3.1.0: version "3.1.4" resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855" @@ -15115,14 +14707,6 @@ postcss-load-config@^3.1.0: lilconfig "^2.0.5" yaml "^1.10.2" -postcss-load-config@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3" - integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ== - dependencies: - lilconfig "^3.0.0" - yaml "^2.3.4" - postcss-modules-extract-imports@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" @@ -15151,13 +14735,6 @@ postcss-modules-values@^4.0.0: dependencies: icss-utils "^5.0.0" -postcss-nested@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.2.0.tgz#4c2d22ab5f20b9cb61e2c5c5915950784d068131" - integrity sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ== - dependencies: - postcss-selector-parser "^6.1.1" - postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: version "6.0.10" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d" @@ -15166,20 +14743,12 @@ postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss-selector-parser@^6.1.1, postcss-selector-parser@^6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" - integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: +postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@8.4.21, postcss@8.4.31, postcss@^8.3.6, postcss@^8.4.33, postcss@^8.4.38, postcss@^8.4.47, postcss@^8.4.7: +postcss@8.4.21, postcss@8.4.31, postcss@^8.3.6, postcss@^8.4.38, postcss@^8.4.7: version "8.4.31" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== @@ -15580,11 +15149,6 @@ react-flatten-children@1.1.2: resolved "https://registry.yarnpkg.com/react-flatten-children/-/react-flatten-children-1.1.2.tgz#8e843b1080c7fd6ccf5ab2877fa6c2e9aa3cf473" integrity sha512-9pnG/uw2Wa0n97s+yBZg/WgfMPE8RC4qNcr6iYbyb19sacCk3gRJCmCzAhTuANSWesFsK9v/yTKW42pkenaAfw== -react-icons@^5.3.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.4.0.tgz#443000f6e5123ee1b21ea8c0a716f6e7797f7416" - integrity sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ== - react-image@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/react-image/-/react-image-4.1.0.tgz#92f2d4a809a178b3bf69acd7bad7da7aa5e7364c" @@ -15693,13 +15257,6 @@ react@18.2.0, react@^18.2.0: dependencies: loose-envify "^1.1.0" -read-cache@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" - integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== - dependencies: - pify "^2.3.0" - read-cmd-shim@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-4.0.0.tgz#640a08b473a49043e394ae0c7a34dd822c73b9bb" @@ -16009,15 +15566,6 @@ resolve@^1.1.4, resolve@^1.17.0, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22 path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^1.1.7, resolve@^1.22.8: - version "1.22.9" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.9.tgz#6da76e4cdc57181fa4471231400e8851d0a924f3" - integrity sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A== - dependencies: - is-core-module "^2.16.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - resolve@^1.22.4: version "1.22.4" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.4.tgz#1dc40df46554cdaf8948a486a10f6ba1e2026c34" @@ -16143,6 +15691,22 @@ rpc-websockets@^7.5.1: bufferutil "^4.0.1" utf-8-validate "^5.0.2" +rpc-websockets@^9.0.2: + version "9.1.1" + resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-9.1.1.tgz#5764336f3623ee1c5cc8653b7335183e3c0c78bd" + integrity sha512-1IXGM/TfPT6nfYMIXkJdzn+L4JEsmb0FL1O2OBjaH03V3yuUDdKFulGLMFG6ErV+8pZ5HVC0limve01RyO+saA== + dependencies: + "@swc/helpers" "^0.5.11" + "@types/uuid" "^8.3.4" + "@types/ws" "^8.2.2" + buffer "^6.0.3" + eventemitter3 "^5.0.1" + uuid "^8.3.2" + ws "^8.5.0" + optionalDependencies: + bufferutil "^4.0.1" + utf-8-validate "^5.0.2" + run-applescript@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-5.0.0.tgz#e11e1c932e055d5c6b40d98374e0268d9b11899c" @@ -16843,7 +16407,7 @@ 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@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, 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== @@ -16939,7 +16503,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@^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== @@ -17036,19 +16600,6 @@ subarg@^1.0.0: dependencies: minimist "^1.1.0" -sucrase@^3.35.0: - version "3.35.0" - resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263" - integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA== - dependencies: - "@jridgewell/gen-mapping" "^0.3.2" - commander "^4.0.0" - glob "^10.3.10" - lines-and-columns "^1.1.6" - mz "^2.7.0" - pirates "^4.0.1" - ts-interface-checker "^0.1.9" - superstruct@^0.14.2: version "0.14.2" resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.14.2.tgz#0dbcdf3d83676588828f1cf5ed35cda02f59025b" @@ -17059,6 +16610,11 @@ superstruct@^1.0.3: resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-1.0.3.tgz#de626a5b49c6641ff4d37da3c7598e7a87697046" integrity sha512-8iTn3oSS8nRGn+C2pgXSKPI3jmpm6FExNazNpjvqS6ZUJQCej3PUXEKM8NjHBOs54ExM+LPW/FBRhymrdcCiSg== +superstruct@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-2.0.2.tgz#3f6d32fbdc11c357deff127d591a39b996300c54" + integrity sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A== + supports-color@5.5.0, supports-color@^5.3.0, supports-color@^7.1.0, supports-color@^8.0.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -17078,14 +16634,6 @@ svg-path-commander@1.0.5: dependencies: dommatrix "^1.0.3" -swr@^2.2.5: - version "2.2.5" - resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.5.tgz#063eea0e9939f947227d5ca760cc53696f46446b" - integrity sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg== - dependencies: - client-only "^0.0.1" - use-sync-external-store "^1.2.0" - synckit@^0.8.5: version "0.8.5" resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.5.tgz#b7f4358f9bb559437f9f167eb6bc46b3c9818fa3" @@ -17106,34 +16654,6 @@ system-architecture@^0.1.0: resolved "https://registry.yarnpkg.com/system-architecture/-/system-architecture-0.1.0.tgz#71012b3ac141427d97c67c56bc7921af6bff122d" integrity sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA== -tailwindcss@^3.4.1: - version "3.4.16" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.16.tgz#35a7c3030844d6000fc271878db4096b6a8d2ec9" - integrity sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw== - dependencies: - "@alloc/quick-lru" "^5.2.0" - arg "^5.0.2" - chokidar "^3.6.0" - didyoumean "^1.2.2" - dlv "^1.1.3" - fast-glob "^3.3.2" - glob-parent "^6.0.2" - is-glob "^4.0.3" - jiti "^1.21.6" - lilconfig "^3.1.3" - micromatch "^4.0.8" - normalize-path "^3.0.0" - object-hash "^3.0.0" - picocolors "^1.1.1" - postcss "^8.4.47" - postcss-import "^15.1.0" - postcss-js "^4.0.1" - postcss-load-config "^4.0.2" - postcss-nested "^6.2.0" - postcss-selector-parser "^6.1.2" - resolve "^1.22.8" - sucrase "^3.35.0" - tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" @@ -17443,16 +16963,16 @@ tslib@1.14.1, tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@2.7.0, tslib@^2.0.1: + version "2.7.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== + tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.4.1: version "2.5.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== -tslib@^2.0.1: - version "2.7.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" - integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== - tslib@^2.3.1: version "2.6.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.0.tgz#b295854684dbda164e181d259a22cd779dcd7bc3" @@ -17468,6 +16988,11 @@ tslib@^2.6.1, tslib@^2.6.2: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== +tslib@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -17492,16 +17017,16 @@ tweetnacl-util@^0.15.1: resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz#b80fcdb5c97bcc508be18c44a4be50f022eea00b" integrity sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw== +tweetnacl@1.0.3, tweetnacl@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" + integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== -tweetnacl@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" - integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -17605,13 +17130,6 @@ ufo@^1.4.0, ufo@^1.5.3: resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.3.tgz#3325bd3c977b6c6cd3160bf4ff52989adc9d3344" integrity sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw== -uint8arrays@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.1.0.tgz#8186b8eafce68f28bd29bd29d683a311778901e2" - integrity sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog== - dependencies: - multiformats "^9.4.2" - uint8arrays@^3.0.0, uint8arrays@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.1.1.tgz#2d8762acce159ccd9936057572dade9459f65ae0" @@ -17668,6 +17186,11 @@ underscore@1.12.1: resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e" integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw== +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + undici@5.28.4: version "5.28.4" resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068" @@ -17806,14 +17329,6 @@ update-browserslist-db@^1.0.9: escalade "^3.1.1" picocolors "^1.0.0" -update-browserslist-db@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5" - integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== - dependencies: - escalade "^3.2.0" - picocolors "^1.1.0" - update-notifier@6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-6.0.2.tgz#a6990253dfe6d5a02bd04fbb6a61543f55026b60" @@ -17897,11 +17412,6 @@ use-sync-external-store@1.2.0: resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== -use-sync-external-store@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz#adbc795d8eeb47029963016cefdf89dc799fcebc" - integrity sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw== - useragent@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972" @@ -18549,19 +18059,19 @@ worker-loader@3.0.8: loader-utils "^2.0.0" schema-utils "^3.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.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== +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== dependencies: ansi-styles "^4.0.0" string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== +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" @@ -18607,7 +18117,7 @@ write-file-atomic@^5.0.0: imurmurhash "^0.1.4" signal-exit "^4.0.1" -ws@7.4.6, ws@8.13.0, ws@8.17.1, ws@8.18.0, ws@^7.2.0, ws@^7.3.1, ws@^7.4.5, ws@^7.5.1, ws@^8.12.0, ws@^8.18.0, ws@^8.5.0, ws@~8.11.0, ws@~8.2.3: +ws@7.4.6, ws@8.13.0, ws@8.17.1, ws@8.18.0, ws@^7.2.0, ws@^7.3.1, ws@^7.4.5, ws@^7.5.1, ws@^7.5.10, ws@^8.12.0, ws@^8.18.0, ws@^8.5.0, ws@~8.11.0, ws@~8.2.3: version "8.17.1" resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== @@ -18683,7 +18193,7 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@2.2.2, yaml@^1.10.2, yaml@^2.1.1, yaml@^2.3.4: +yaml@2.2.2, yaml@^1.10.2, yaml@^2.1.1: version "2.2.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.2.tgz#ec551ef37326e6d42872dad1970300f8eb83a073" integrity sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA== @@ -18783,7 +18293,7 @@ zip-dir@2.0.0: async "^3.2.0" jszip "^3.2.2" -zod@3.22.4, zod@3.23.8, zod@^1.11.11: +zod@3.23.8, zod@^1.11.11: version "3.23.8" resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==