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 26143e8b7f..ba39d57b47 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,11 @@ screenshots/ anvil*.log rainbowbx.xpi +orbyplaygroundbx.xpi +orbyplaygroundbx.zip .idea/** -static/data \ No newline at end of file +static/data + +.tool-versions \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 17a753acb8..1db3447785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,1793 +21,14 @@ 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.2](https://github.com/orb-labs/playground-extension/releases/tag/v0.0.2) ### Added -- Degen Mode is here to make Swapping even faster. Turn it on in Swap Settings to skip the review steps #1652 +- Updating the orby npm package -### 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 +## [v0.0.1](https://github.com/orb-labs/playground-extension/releases/tag/v0.0.1) ### 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 dc82825506..906b36bad5 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,19 @@ ![](.github/hero.png) -## 🌈️ Rainbow Extension +## OrbyPlayground Extension -Built for speed. Built for power. Built for you. +Unify Accounts. Unify Ethereum. -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 Polygon +- Built-in Send and Swap using your unified balance to power all of your DeFi needs ...and a lot more. @@ -20,36 +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/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/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 04ce2c5123..a567568a98 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.5", "scripts": { "//enable dev mode": "", "devmode:on": "sed -i'' -e 's/IS_DEV.*/IS_DEV=true/g' .env", @@ -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": "", @@ -45,11 +46,11 @@ "// 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": "", - "zip": "cd build/ && zip -r ../rainbowbx.zip *", + "zip": "cd build/ && zip -r ../orbyplaygroundbx.zip *", "// Build and zip": "", "bundle": "yarn build && yarn zip", "// Runs tests": "", @@ -72,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'" }, @@ -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.57", "@radix-ui/react-accordion": "1.1.2", "@radix-ui/react-context-menu": "2.1.1", "@radix-ui/react-dropdown-menu": "2.0.1", @@ -112,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", @@ -122,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", @@ -131,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", @@ -334,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 } } -} \ No newline at end of file +} 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 a10f72f4c4..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(); } @@ -536,10 +540,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)) { + if ( + accounts + .map((a) => validateAndFormatAddress(a)) + .includes(validateAndFormatAddress(address)) + ) { return keychain; } } + throw new Error('No keychain found for account'); } @@ -547,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/RainbowSigner.ts b/src/core/keychain/RainbowSigner.ts index d34416ad80..7284181996 100644 --- a/src/core/keychain/RainbowSigner.ts +++ b/src/core/keychain/RainbowSigner.ts @@ -5,11 +5,16 @@ 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 { Wallet } from '@ethersproject/wallet'; import { personalSign } from '@metamask/eth-sig-util'; import { bytesToHex } from 'ethereum-cryptography/utils'; import { Address } from 'viem'; @@ -49,6 +54,16 @@ export class RainbowSigner extends Signer { return signature; } + async signTypedData( + domain: TypedDataDomain, + types: Record>, + value: Record, + ): Promise { + const pkey = this.#getPrivateKeyBuffer(); + const wallet = new Wallet(pkey); + return wallet._signTypedData(domain, types, value); + } + 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..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/providers/proxy.ts b/src/core/providers/proxy.ts index 71c01b2b88..4fff1e8e64 100644 --- a/src/core/providers/proxy.ts +++ b/src/core/providers/proxy.ts @@ -16,18 +16,6 @@ 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)}`; - } + // NOTE: you'll need your .env file to have the correct RPC URLs return endpoint; }; 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 5e4a30d7e5..d03a4a991f 100644 --- a/src/core/references/assets.ts +++ b/src/core/references/assets.ts @@ -1,8 +1,12 @@ import { + arbitrum, arbitrumNova, + arbitrumSepolia, aurora, auroraTestnet, avalanche, + base, + baseSepolia, blast, blastSepolia, bob, @@ -27,6 +31,7 @@ import { harmonyOne, hedera, hederaTestnet, + holesky, immutableZkEvm, immutableZkEvmTestnet, kava, @@ -37,6 +42,7 @@ import { linea, lineaSepolia, lyra, + mainnet, manta, mantaSepoliaTestnet, mantle, @@ -49,9 +55,13 @@ import { moonbeam, opBNB, opBNBTestnet, + optimism, + optimismSepolia, palm, palmTestnet, pgn, + polygon, + polygonAmoy, polygonZkEvm, polygonZkEvmCardona, polygonZkEvmTestnet, @@ -64,6 +74,7 @@ import { saigon, scroll, scrollSepolia, + sepolia, shapeSepolia, xai, xaiTestnet, @@ -76,10 +87,14 @@ import { 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', @@ -115,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', @@ -124,6 +140,9 @@ export const customChainIdsToAssetNames: Record = { [lineaSepolia.id]: 'lineasepolia', [ChainId.loot]: 'loot', [lyra.id]: 'lyra', + [mainnet.id]: 'ethereum', + [sepolia.id]: 'ethereum', + [holesky.id]: 'ethereum', [manta.id]: 'manta', [mantaSepoliaTestnet.id]: 'mantasepolia', [mantle.id]: 'mantle', @@ -140,9 +159,13 @@ export const customChainIdsToAssetNames: Record = { 7701: 'nativecantotestnet', [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 c025d5e3be..ddf9e80848 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 @@ -115,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 e71466d365..4f7b1154a8 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,40 @@ export const rainbowChainsStore = createStore( return false; } }, + + addAllCustomRPC: (rpcs: { rpcUrl: string; chainId: ChainId }[]) => { + const rainbowChains = get().rainbowChains; + rpcs.forEach(({ chainId, rpcUrl }) => { + let rainbowChain = rainbowChains[chainId] || { + chains: [], + activeRpcUrl: '', + }; + const currentRpcs = rainbowChain.chains.map( + (chain) => chain.rpcUrls.default.http[0], + ); + + if (!currentRpcs.includes(rpcUrl)) { + const chains = getInitialRainbowChains(); + rainbowChain = chains[chainId]; + + 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/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/numbers.ts b/src/core/utils/numbers.ts index 8d4a91671b..dc877c95a3 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 diff --git a/src/core/utils/orb.ts b/src/core/utils/orb.ts new file mode 100644 index 0000000000..b30b19f24f --- /dev/null +++ b/src/core/utils/orb.ts @@ -0,0 +1,305 @@ +import { TypedDataField } from '@ethersproject/abstract-signer'; +import { + FungibleTokenAmount, + OnchainOperation, + OperationDataFormat, + 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'; +import { Address, TypedDataDomain, formatUnits } from 'viem'; + +import { ParsedUserAsset } from '~/core/types/assets'; +import { ChainName } from '~/core/types/chains'; +import { convertRawAmountToDecimalFormat } from '~/core/utils/numbers'; + +import { keychainManager } from '../keychain/KeychainManager'; + +export const convertStandardizedBalanceToParsedUserAsset = ( + balance: StandardizedBalance, +): ParsedUserAsset => { + const relatedChainIds = balance.tokenBalancesOnChains.map((balance) => + balance.token.chainId.toString(), + ); + + const relatedAssets = convertTokenBalancesOnChainsToParsedUserAssets( + balance.tokenBalancesOnChains, + balance, + ); + + const tokenAmount = _.sample(balance.tokenBalancesOnChains); + + return { + 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: tokenAmount?.token.address as Address, + chainId: Number(tokenAmount?.token.chainId), + chainName: ChainName.mainnet, + balance: { + amount: formatUnits( + balance.total.toRawAmount(), + balance.total.currency.decimals, + ), + 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: { + amount: '', + display: '', // this is the price + }, + price: { + change: '', + amount: Number(balance.total.toRawAmount()), + display: 'foo', + }, + }, + icon_url: balance.total.currency.logoUrl, + relatedChainIds, + relatedAssets, + }; +}; + +export const convertStandardizedBalanceToParsedUserAssets = ( + balances: StandardizedBalance[], +): ParsedUserAsset[] => { + 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 = ( + 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}`, + }, + native: { + balance: { + amount: '', + display: '', // this is the price + }, + price: { + change: '', + amount: Number(standardizedBalance.total.toRawAmount()), + display: 'foo', + }, + }, + icon_url: standardizedBalance.total.currency.logoUrl, + }; +}; + +export const convertFungibleTokenAmountsToParsedUserAssets = ( + balances: StandardizedBalance[], +): ParsedUserAsset[] => { + return balances + .map((standardizedBalance) => { + return convertTokenBalancesOnChainsToParsedUserAssets( + standardizedBalance.tokenBalancesOnChains, + standardizedBalance, + ); + }) + .flat(); +}; + +export const convertTokenBalancesOnChainsToParsedUserAssets = ( + tokenBalancesOnChains: FungibleTokenAmount[], + standardizedBalance: StandardizedBalance, +): ParsedUserAsset[] => { + return tokenBalancesOnChains.map((balance) => + convertFungibleTokenAmountToParsedUserAsset(balance, standardizedBalance), + ); +}; + +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 { + 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); + + 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, + }; + + // eslint-disable-next-line no-await-in-loop + const tx = await wallet.populateTransaction(txData); + return await signer.signTransaction(tx); + } else { + const parsedData = JSON.parse(operation.data) as { + domain: TypedDataDomain; + types: Record>; + message: Record; + }; + + delete parsedData.types['EIP712Domain']; + // @ts-ignore + const signature = await wallet.signTypedData( + parsedData.domain, + parsedData.types, + parsedData.message, + ); + + 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/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/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 7096533383..37dd8b68f9 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,13 @@ initializeSentry('background'); const popupMessenger = initializeMessenger({ connect: 'popup' }); const inpageMessenger = initializeMessenger({ connect: 'inpage' }); +unifyBalancesOnApps( + '/', + `${process.env.ORBY_BASE_URL}/${process.env.ORBY_PRIVATE_API_KEY}`, + true, + 6_000_00, +); + handleInstallExtension(); handleProviderRequest({ popupMessenger, inpageMessenger }); handleTabAndWindowUpdates(); 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 e11a5cf215..50f15faf63 100644 --- a/src/entries/popup/App.tsx +++ b/src/entries/popup/App.tsx @@ -1,3 +1,5 @@ +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'; @@ -13,9 +15,14 @@ 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 { getWalletVirtualEnvironment } from '~/core/utils/orb'; import { WagmiConfigUpdater, wagmiConfig } from '~/core/wagmi'; import { Box, ThemeProvider } from '~/design-system'; @@ -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,22 @@ export function App() { const { currentTheme } = useCurrentThemeStore(); const isFullScreen = useIsFullScreen(); + const { currentAddresses } = useCurrentAddressStore(); + + const orbyConfig = React.useMemo(() => { + const accounts = currentAddresses?.map((address) => { + const vm = getWalletVirtualEnvironment(address); + return new Account(address, AccountType.EOA, vm!, undefined); + }); + + return { + instancePrivateAPIKey: process.env.ORBY_PRIVATE_API_KEY as string, + instancePublicAPIKey: process.env.ORBY_PUBLIC_API_KEY as string, + appName: 'Rainbow', + accounts, + }; + }, [currentAddresses]); + return ( <> @@ -104,22 +123,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..2b0c3ed994 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,7 +44,12 @@ export function CoinRow({ - + 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/Tabs/TabBar.tsx b/src/entries/popup/components/Tabs/TabBar.tsx index 2615fbb4c9..8838718a67 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,6 @@ const tabConfig: TabConfigType[] = [ SelectedIcon: ActivitySelected, name: 'activity', }, - { - 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..75e5dfc388 --- /dev/null +++ b/src/entries/popup/components/TransactionFee/GasTokenMenu.tsx @@ -0,0 +1,204 @@ +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..53e6f7bbd7 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,32 +97,6 @@ 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 onSpeedOpenChange = useCallback( - (isOpen: boolean) => { - isOpen && - analyticsEvents?.transactionSpeedClicked && - analytics.track(analyticsEvents?.transactionSpeedClicked); - }, - [analyticsEvents?.transactionSpeedClicked], - ); - useKeyboardShortcut({ handler: (e: KeyboardEvent) => { if (!disableShortcuts) { @@ -179,16 +146,9 @@ function Fee({ - - - - {isLoading - ? '~' - : `${ - gasFeeParamsForSelectedSpeed?.gasFee.display || '~' - }`} + ${aggregateFee} @@ -211,7 +171,15 @@ function Fee({ - + {/* - + */} @@ -272,6 +240,9 @@ type TransactionFeeProps = { transactionSpeedSwitched: keyof EventProperties; transactionSpeedClicked: keyof EventProperties; }; + selectedGasToken?: GasTokenInput; + setSelectedGasToken?: (gasToken?: GasTokenInput) => void; + aggregateFee?: string; }; export function TransactionFee({ @@ -284,6 +255,9 @@ export function TransactionFee({ plainTriggerBorder, analyticsEvents, flashbotsEnabled, + selectedGasToken, + setSelectedGasToken, + aggregateFee, }: TransactionFeeProps) { const { defaultTxSpeed } = useDefaultTxSpeed({ chainId }); const { @@ -323,6 +297,9 @@ export function TransactionFee({ baseFeeTrend={baseFeeTrend} flashbotsEnabled={!!flashbotsEnabled} feeType={feeType} + selectedGasToken={selectedGasToken} + setSelectedGasToken={setSelectedGasToken} + aggregateFee={aggregateFee} /> ); } @@ -339,6 +316,9 @@ type SwapFeeProps = { flashbotsEnabled?: boolean; speedMenuMarginRight?: Space; quoteServiceTime?: number; + selectedGasToken: GasTokenInput; + setSelectedGasToken: (gasToken?: GasTokenInput) => void; + aggregateFee: string; }; export function SwapFee({ @@ -353,6 +333,9 @@ export function SwapFee({ flashbotsEnabled, speedMenuMarginRight, quoteServiceTime, + selectedGasToken, + setSelectedGasToken, + aggregateFee, }: SwapFeeProps) { const { defaultTxSpeed } = useDefaultTxSpeed({ chainId }); const { @@ -394,6 +377,9 @@ export function SwapFee({ flashbotsEnabled={!!flashbotsEnabled} speedMenuMarginRight={speedMenuMarginRight} feeType={feeType} + selectedGasToken={selectedGasToken} + setSelectedGasToken={setSelectedGasToken} + aggregateFee={aggregateFee} /> ); } @@ -414,6 +400,9 @@ type ApprovalFeeProps = { transactionSpeedClicked: keyof EventProperties; }; assetType: 'erc20' | 'nft'; + selectedGasToken: GasTokenInput; + setSelectedGasToken: (gasToken?: GasTokenInput) => void; + aggregateFee: string; }; export function ApprovalFee({ @@ -428,6 +417,9 @@ export function ApprovalFee({ analyticsEvents, flashbotsEnabled, assetType, + selectedGasToken, + setSelectedGasToken, + aggregateFee, }: ApprovalFeeProps) { const { defaultTxSpeed } = useDefaultTxSpeed({ chainId }); const { @@ -468,6 +460,9 @@ export function ApprovalFee({ baseFeeTrend={baseFeeTrend} flashbotsEnabled={!!flashbotsEnabled} feeType={feeType} + selectedGasToken={selectedGasToken} + setSelectedGasToken={setSelectedGasToken} + aggregateFee={aggregateFee} /> ); } 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/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/approveAppRequest/useApproveAppRequestValidations.ts b/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts index db740645a8..cfb1b2f82b 100644 --- a/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts +++ b/src/entries/popup/hooks/approveAppRequest/useApproveAppRequestValidations.ts @@ -1,48 +1,44 @@ +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'; - -import { useHasEnoughGas } from '../../pages/messages/useHasEnoughGas'; export const useApproveAppRequestValidations = ({ - session, dappStatus, + operationSet, }: { session: ActiveSession; dappStatus?: DAppStatus; + operationSet?: OperationSet; }) => { - const { connectedToHardhat, connectedToHardhatOp } = - useConnectedToHardhatStore(); - - const enoughNativeAssetForGas = useHasEnoughGas(session); + 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.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'); + } + + 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/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/send/useSendAsset.ts b/src/entries/popup/hooks/send/useSendAsset.ts index 40c5c4aa66..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'; @@ -31,9 +37,10 @@ export const useSendAsset = () => { const [selectedAssetAddress, setSelectedAssetAddress] = useState< AddressOrEth | '' >(''); - const [selectedAssetChain, setSelectedAssetChain] = useState( - ChainId.mainnet, - ); + const [selectedAssetChain, setSelectedAssetChain] = useState< + ChainId | undefined + >(undefined); + const { data: assets = [] } = useUserAssets( { address, @@ -64,7 +71,7 @@ export const useSendAsset = () => { [], ); - const combinedAssets = useMemo( + let combinedAssets = useMemo( () => Array.from( new Map( @@ -77,6 +84,17 @@ export const useSendAsset = () => { [assets, customNetworkAssets], ); + const { testnetMode } = useTestnetModeStore(); + const { portfolio } = usePortfolio(testnetMode); + + combinedAssets = useMemo(() => { + if (!portfolio) { + return []; + } + + return convertStandardizedBalanceToParsedUserAssets(portfolio); + }, [portfolio]); + const allAssets = useMemo( () => combinedAssets.sort( @@ -87,21 +105,33 @@ export const useSendAsset = () => { [combinedAssets], ); + const flattenedAssets = useMemo(() => { + if (!portfolio) { + return []; + } + + return convertFungibleTokenAmountsToParsedUserAssets(portfolio); + }, [portfolio]); + const asset = useMemo( () => - allAssets?.find( + flattenedAssets?.find( ({ address, chainId }) => isLowerCaseMatch(address, selectedAssetAddress) && chainId === selectedAssetChain, ) || null, - [allAssets, selectedAssetAddress, selectedAssetChain], + [flattenedAssets, selectedAssetAddress, selectedAssetChain], ); return { selectAssetAddressAndChain, + setSelectedAssetAddress, + setSelectedAssetChain, asset, assets: allAssets, sortMethod, setSortMethod, + portfolio, + chainId: selectedAssetChain, }; }; diff --git a/src/entries/popup/hooks/send/useSendState.ts b/src/entries/popup/hooks/send/useSendState.ts index 0507f65e39..4c243faa7b 100644 --- a/src/entries/popup/hooks/send/useSendState.ts +++ b/src/entries/popup/hooks/send/useSendState.ts @@ -70,14 +70,9 @@ export const useSendState = ({ }, [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 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/send/useSendValidations.ts b/src/entries/popup/hooks/send/useSendValidations.ts index 16632ac167..6a853251ac 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,58 +124,73 @@ 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) + + 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, }); - 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.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) && - enoughAssetBalance && - enoughNativeAssetForGas, - [ - 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..2a1bda6608 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,6 @@ const sortBy = (by: SortMethod) => { }; export const useSwapAssets = ({ bridge }: { bridge: boolean }) => { - const { currentAddress } = useCurrentAddressStore(); const { currentCurrency } = useCurrentCurrencyStore(); const [assetToSell, setAssetToSellState] = useState< @@ -58,19 +57,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 +149,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..d4360ec7fe 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,29 +8,21 @@ 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 enoughAssetBalance = useMemo(() => { if (assetToSellValue) { if (!assetToSell?.isNativeAsset) { @@ -54,22 +47,8 @@ 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; + }, [operationSet]); const buttonLabel = useMemo(() => { if (!enoughAssetBalance) diff --git a/src/entries/popup/hooks/useAppSession.ts b/src/entries/popup/hooks/useAppSession.ts index f1e7d58141..a7af70461e 100644 --- a/src/entries/popup/hooks/useAppSession.ts +++ b/src/entries/popup/hooks/useAppSession.ts @@ -1,3 +1,5 @@ +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,20 +23,38 @@ export function useAppSession({ host = '' }: { host?: string }) { getActiveSession, } = useAppSessionsStore(); + const { baseMainnetClient, accountCluster } = useOrby(); + const activeSession = getActiveSession({ host }); const clearAppHasInteractedWithNudgeSheet = useAppConnectionWalletSwitcherStore.use.clearAppHasInteractedWithNudgeSheet(); const updateAppSessionAddress = React.useCallback( ({ address }: { address: Address }) => { + if (!accountCluster?.accountClusterId) { + return; + } + storeUpdateActiveSession({ host, address }); messenger.send(`accountsChanged:${host}`, address); messenger.send( `chainChanged:${host}`, appSessions[host].sessions[address], ); + + connectAppSession( + accountCluster?.accountClusterId, + host, + baseMainnetClient, + ); }, - [appSessions, host, storeUpdateActiveSession], + [ + appSessions, + baseMainnetClient, + host, + storeUpdateActiveSession, + accountCluster?.accountClusterId, + ], ); const addSession = React.useCallback( @@ -49,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) { @@ -57,8 +81,14 @@ export function useAppSession({ host = '' }: { host?: string }) { chainId: toHex(String(chainId)), }); } + + connectAppSession( + accountCluster?.accountClusterId, + host, + baseMainnetClient, + ); }, - [storeAddSession], + [accountCluster?.accountClusterId, storeAddSession, baseMainnetClient], ); const updateAppSessionChainId = React.useCallback( @@ -95,25 +125,33 @@ export function useAppSession({ host = '' }: { host?: string }) { const disconnectSession = React.useCallback( ({ address, host }: { address: Address; host: string }) => { const newActiveSession = removeSession({ host, address }); - if (newActiveSession) { + if (newActiveSession && accountCluster?.accountClusterId) { + connectAppSession( + accountCluster?.accountClusterId, + 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], + [ + accountCluster?.accountClusterId, + 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..2270bb12e5 --- /dev/null +++ b/src/entries/popup/hooks/useConnectAppSessions.ts @@ -0,0 +1,17 @@ +import { useBulkConnectAppSessions } from '@orb-labs/orby-react'; +import * as React from 'react'; + +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 }; +} 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..4a324c048f 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,71 @@ 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, + }); + + if (!transaction) { + return; + } + + 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 +158,11 @@ export const useInfiniteTransactionList = ({ () => Object.entries( selectTransactionsByDate([ - ...pendingTransactions, + // ...pendingTransactions, ...transactionsAfterCutoff, ]), ).flat(2), - [pendingTransactions, transactionsAfterCutoff], + [transactionsAfterCutoff], ); const infiniteRowVirtualizer = useVirtualizer({ @@ -146,7 +206,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 +226,13 @@ export const useInfiniteTransactionList = ({ fetchNextPage(); } }, [ - data?.pages?.length, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, - transactions.length, transactionsAfterCutoff.length, rows, + transactions, ]); const refetchTransactions = async () => { @@ -194,7 +253,7 @@ export const useInfiniteTransactionList = ({ return { error, fetchNextPage, - isFetching, + isFetching: isFetching || isLoading || !transactions, isFetchingNextPage, isInitialLoading, status, diff --git a/src/entries/popup/hooks/useNavigateToSwaps.tsx b/src/entries/popup/hooks/useNavigateToSwaps.tsx index a0ccede3e5..9bb99bfc96 100644 --- a/src/entries/popup/hooks/useNavigateToSwaps.tsx +++ b/src/entries/popup/hooks/useNavigateToSwaps.tsx @@ -1,44 +1,20 @@ -import React from 'react'; - -import config from '~/core/firebase/remoteConfig'; -import { i18n } from '~/core/languages'; -import { useFeatureFlagsStore } from '~/core/state/currentSettings/featureFlags'; -import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode'; import { KeychainType } from '~/core/types/keychainTypes'; import { POPUP_URL, goToNewTab } from '~/core/utils/tabs'; -import { triggerAlert } from '~/design-system/components/Alert/Alert'; import { ROUTES } from '../urls'; import { useCurrentWalletTypeAndVendor } from './useCurrentWalletType'; import { useIsFullScreen } from './useIsFullScreen'; import { useRainbowNavigate } from './useRainbowNavigate'; -import { useWallets } from './useWallets'; export const useNavigateToSwaps = () => { const { type } = useCurrentWalletTypeAndVendor(); const isFullScreen = useIsFullScreen(); - const { isWatchingWallet } = useWallets(); const navigate = useRainbowNavigate(); - const { featureFlags } = useFeatureFlagsStore(); - const { testnetMode } = useTestnetModeStore(); - - const allowSwap = React.useMemo( - () => - (!isWatchingWallet || featureFlags.full_watching_wallets) && - config.swaps_enabled, - [featureFlags.full_watching_wallets, isWatchingWallet], - ); 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); - } + return type === KeychainType.HardwareWalletKeychain && !isFullScreen + ? goToNewTab({ url: POPUP_URL + `#${ROUTES.SWAP}?hideBack=true` }) + : navigate(ROUTES.SWAP); }; }; 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 66a2c53881..c519e85404 100644 --- a/src/entries/popup/hooks/useUserAssetsBalance.ts +++ b/src/entries/popup/hooks/useUserAssetsBalance.ts @@ -1,3 +1,4 @@ +import { useGetPortfolioOverview } 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 { fungibleTokenOverview } = useGetPortfolioOverview(testnetMode); + return { amount: totalAssetsBalance, - display: totalAssetsBalance - ? convertAmountToNativeDisplay(totalAssetsBalance, currency || currentCurrency) + display: fungibleTokenOverview + ? convertAmountToNativeDisplay( + convertRawAmountToDecimalFormat( + fungibleTokenOverview?.totalValueInFiat?.toRawAmount()?.toString(), + fungibleTokenOverview?.totalValueInFiat?.currency.decimals, + ), + currentCurrency, + ) : undefined, isLoading: knownNetworksIsLoading || customNetworksIsLoading, }; 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/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 941a6425bb..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')} /> - + /> */} - navigate(ROUTES.BUY)} tooltipHint={shortcuts.home.BUY.display} tooltipText={i18n.t('tooltip.buy_crypto')} - /> + /> */} )} diff --git a/src/entries/popup/pages/home/Tokens.tsx b/src/entries/popup/pages/home/Tokens.tsx index c13d9ed4fc..79d94308fb 100644 --- a/src/entries/popup/pages/home/Tokens.tsx +++ b/src/entries/popup/pages/home/Tokens.tsx @@ -1,34 +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 { 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, @@ -44,14 +35,12 @@ 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'; @@ -60,21 +49,20 @@ import { TokenMarkedHighlighter } from './TokenMarkedHighlighter'; 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 ? ( + + + + ) : ( + + + + )} ); @@ -99,140 +89,71 @@ const TokenRow = memo(function TokenRow({ 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 { - data: customNetworkAssets = [], - refetch: refetchCustomNetworkAssets, - } = useCustomNetworkAssets( - { - address: currentAddress, - currency, - }, - { - select: (data) => - selectorFilterByUserChains({ - data, - selector: hideSmallBalances - ? selectUserAssetsFilteringSmallBalancesList - : selectUserAssetsList, - }), - }, - ); - - const isPinned = useCallback( - (assetUniqueId: string) => - !!pinnedStore[currentAddress]?.[assetUniqueId]?.pinned, - [currentAddress, pinnedStore], - ); + const { testnetMode } = useTestnetModeStore(); + const { portfolio, isLoading } = usePortfolio(testnetMode); - const combinedAssets = useMemo( - () => - Array.from( - new Map( - [...customNetworkAssets, ...assets].map((item) => [ - item.uniqueId, - item, - ]), - ).values(), - ), - [assets, customNetworkAssets], - ); + useEffect(() => { + if (!portfolio) { + return; + } - const unhiddenAssets = useMemo(() => { - return combinedAssets.filter((asset) => !isHidden(asset)); - }, [combinedAssets, isHidden]); + setCombinedAssets(convertStandardizedBalanceToParsedUserAssets(portfolio)); + }, [portfolio]); - 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 parentAsset = combinedAssets[index]; - const sortedAssets = filteredAssets.sort((a, b) => { - const pinnedFirstAsset = pinnedStore[currentAddress]?.[a.uniqueId]; - const pinnedSecondAsset = pinnedStore[currentAddress]?.[b.uniqueId]; - - // 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({ @@ -243,7 +164,6 @@ export function Tokens({ scrollY }: { scrollY: MotionValue }) { type: 'tokens.refresh', }); setManuallyRefetchingTokens(true); - await Promise.all([refetchUserAssets(), refetchCustomNetworkAssets()]); setManuallyRefetchingTokens(false); } }, @@ -255,13 +175,13 @@ export function Tokens({ scrollY }: { scrollY: MotionValue }) { 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 (!filteredAssets?.length) { + if (!combinedAssets?.length) { return ; } @@ -299,7 +219,7 @@ export function Tokens({ scrollY }: { scrollY: MotionValue }) { {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; @@ -316,7 +236,11 @@ export function Tokens({ scrollY }: { scrollY: MotionValue }) { }} > {pinned && } - + ); })} @@ -340,10 +264,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( () => @@ -356,10 +291,10 @@ export const AssetRow = memo(function AssetRow({ ) : ( - {asset?.balance?.display} + {display} ), - [asset?.balance?.display, asset?.symbol, hideAssetBalances], + [display, asset?.symbol, hideAssetBalances], ); const nativeBalanceDisplay = useMemo( @@ -405,6 +340,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( () => ( @@ -413,21 +370,10 @@ export const AssetRow = memo(function AssetRow({ {balanceDisplay} - - - - {priceChangeDisplay} - - - + {chainsList} ), - [balanceDisplay, priceChangeColor, priceChangeDisplay, uniqueId], + [balanceDisplay, chainsList, uniqueId], ); return ( @@ -436,6 +382,8 @@ export const AssetRow = memo(function AssetRow({ asset={asset} topRow={topRow} bottomRow={bottomRow} + size={size} + isParent={isParent} /> ); }); 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/SendTransactionActions.tsx b/src/entries/popup/pages/messages/SendTransaction/SendTransactionActions.tsx index a1442ac2de..13262c3928 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,9 +27,17 @@ export const SendTransactionActions = ({ waitingForDevice: boolean; loading: boolean; dappStatus?: DAppStatus; + operationSet?: OperationSet; }) => { - const { enoughNativeAssetForGas, buttonLabel } = - useApproveAppRequestValidations({ session, dappStatus }); + const { buttonLabel } = useApproveAppRequestValidations({ + session, + dappStatus, + operationSet, + }); + + const disabled = useMemo(() => { + return operationSet?.status != CreateOperationsStatus.SUCCESS; + }, [operationSet]); const { trackShortcut } = useKeyboardAnalytics(); useKeyboardShortcut({ @@ -48,15 +60,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 1543276a6c..20411e6341 100644 --- a/src/entries/popup/pages/messages/SendTransaction/SendTransactionsInfo.tsx +++ b/src/entries/popup/pages/messages/SendTransaction/SendTransactionsInfo.tsx @@ -1,6 +1,7 @@ import { TransactionRequest } from '@ethersproject/abstract-provider'; +import { CreateOperationsStatus, OperationSet } from '@orb-labs/orby-core'; import { AnimatePresence, motion } from 'framer-motion'; -import { ReactNode, memo, useState } from 'react'; +import { ReactNode, memo, useMemo, useState } from 'react'; import { Address } from 'viem'; import { DAppStatus } from '~/core/graphql/__generated__/metadata'; @@ -34,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'; @@ -44,9 +46,7 @@ import { MaliciousRequestWarning, getDappStatusBadge, } from '../DappScanStatus'; -import { SimulationOverview } from '../Simulation'; import { CopyButton, TabContent, Tabs } from '../Tabs'; -import { useHasEnoughGas } from '../useHasEnoughGas'; import { SimulationError, TransactionSimulation, @@ -60,6 +60,7 @@ interface SendTransactionProps { }: { preventWindowClose?: boolean; }) => void; + operationSet?: OperationSet; } const InfoRow = ({ @@ -102,9 +103,6 @@ const InfoRow = ({ const Overview = memo(function Overview({ chainId, - simulation, - status, - error, metadata, }: { chainId: ChainId; @@ -121,7 +119,7 @@ const Overview = memo(function Overview({ return ( - + {/* {i18n.t('simulation.title')} @@ -131,7 +129,7 @@ const Overview = memo(function Overview({ error={error} /> - + */} {chainId && chainName && ( + + {operationSet && } + {simulation && ( { + return true; + }, []); return ( setExpanded((e) => !e)} + operationSet={operationSet} /> ) : ( activeSession && ( diff --git a/src/entries/popup/pages/messages/SendTransaction/index.tsx b/src/entries/popup/pages/messages/SendTransaction/index.tsx index 762b950416..62323b2781 100644 --- a/src/entries/popup/pages/messages/SendTransaction/index.tsx +++ b/src/entries/popup/pages/messages/SendTransaction/index.tsx @@ -1,7 +1,16 @@ import { TransactionRequest } from '@ethersproject/abstract-provider'; -import { getAddress } from '@ethersproject/address'; +import { + OperationStatus, + OperationStatusType, + validateAndFormatAddress, +} 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'; +import { Address, Hex, PublicClient } from 'viem'; import { analytics } from '~/analytics'; import { event } from '~/analytics/event'; @@ -9,15 +18,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,6 +36,7 @@ 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'; @@ -56,75 +65,143 @@ 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 [selectedGasToken, setSelectedGasToken] = useState({ + name: 'no gas abstraction', + standardizedTokenId: undefined, + isDefault: true, + }); + const { flashbotsEnabled } = useFlashbotsEnabledStore(); - const flashbotsEnabledGlobally = - config.flashbots_enabled && - flashbotsEnabled && - activeSession?.chainId === ChainId.mainnet; + 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, getCachedVirtualNode } = useOrby(); + + const gasToken = useMemo(() => { + return selectedGasToken?.standardizedTokenId + ? { standardizedTokenId: selectedGasToken.standardizedTokenId } + : undefined; + }, [selectedGasToken]); + + const { operationSet, virtualNode, aggregateFee, isLoading } = + useGetOperationsToExecuteTransaction( + validateAndFormatAddress(activeSession?.address), + activeSession?.chainId ? BigInt(activeSession.chainId) : undefined, + txRequest.to as string, + txRequest.data as string, + txRequest.value ? BigInt(txRequest.value.toString()) : 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 ( + activeSession && + [OperationStatusType.FAILED, OperationStatusType.NOT_FOUND].includes( + statusSummary, + ) + ) { + approveRequest(finalTransactionStatus?.hash); + setWaitingForDevice(false); + setLoading(false); + } else if ( + activeSession && + [OperationStatusType.SUCCESSFUL, OperationStatusType.PENDING].includes( + statusSummary, + ) + ) { + const virtualNode = getCachedVirtualNode( + activeSession.address, + BigInt(activeSession.chainId), + ) as unknown as PublicClient; + + let transactionHash = finalTransactionStatus?.hash; + if (request.method != 'signAndSendTransaction') { + const receipt = await virtualNode?.waitForTransactionReceipt({ + hash: finalTransactionStatus?.hash as Hex, + }); + transactionHash = receipt?.transactionHash; + } + + approveRequest(transactionHash); + setWaitingForDevice(false); + setLoading(false); + } + }, + [activeSession, approveRequest, getCachedVirtualNode, request.method], + ); 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); // Change the label while we wait for confirmation if (type === 'HardwareWalletKeychain') { setWaitingForDevice(true); } - 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', - 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); + + if (!accountCluster) { + approveRequest(null); + setWaitingForDevice(false); + setLoading(false); + return; + } else if (!accountCluster || !virtualNode || !operationSet) { + approveRequest(null); setWaitingForDevice(false); + setLoading(false); + return; + } + + const { operationResponses, success } = + await virtualNode.sendOperationSet( + accountCluster, + operationSet, + signOperation, + undefined, + signOperation, + ); - analytics.track(event.dappPromptSendTransactionApproved, { - chainId: txData.chainId, - dappURL: dappMetadata?.appHost || '', - dappName: dappMetadata?.appName, - }); + if (!success) { + approveRequest(null); + setWaitingForDevice(false); + setLoading(false); + return; } + + const ids = operationResponses + ?.map((op) => op.id) + .filter((id) => !_.isUndefined(id)); + + baseMainnetClient?.subscribeToOperationStatuses( + ids, + operationStatusesUpdated, + ); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { showLedgerDisconnectedAlertIfNeeded(e); @@ -139,22 +216,18 @@ export function SendTransaction({ text: i18n.t('errors.sending_transaction'), description: extractedError, }); - } finally { setWaitingForDevice(false); setLoading(false); } }, [ selectedWallet, activeSession, - request?.params, - connectedToHardhat, - connectedToHardhatOp, - asset, - flashbotsEnabledGlobally, - selectedGas.transactionGasParams, + accountCluster, + virtualNode, + operationSet, + baseMainnetClient, + operationStatusesUpdated, approveRequest, - dappMetadata?.appHost, - dappMetadata?.appName, ]); const onRejectRequest = useCallback(() => { @@ -173,6 +246,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); @@ -206,13 +288,25 @@ 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 ( - + @@ -226,19 +320,22 @@ 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 e33dea996c..5a81d99a3b 100644 --- a/src/entries/popup/pages/messages/SignMessage/SignMessageInfo.tsx +++ b/src/entries/popup/pages/messages/SignMessage/SignMessageInfo.tsx @@ -1,3 +1,4 @@ +import { OnchainOperation, OperationSet } from '@orb-labs/orby-core'; import { AnimatePresence, motion } from 'framer-motion'; import { useState } from 'react'; @@ -12,6 +13,7 @@ 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'; @@ -25,6 +27,8 @@ import { interface SignMessageProps { request: ProviderRequestPayload; + operations?: OnchainOperation[]; + operationSet?: OperationSet; } function Overview({ @@ -88,7 +92,10 @@ function Overview({ ); } -export const SignMessageInfo = ({ request }: SignMessageProps) => { +export const SignMessageInfo = ({ + request, + operationSet, +}: SignMessageProps) => { const dappUrl = request?.meta?.sender?.url || ''; const { currentCurrency } = useCurrentCurrencyStore(); const { data: dappMetadata } = useDappMetadata({ url: dappUrl }); @@ -167,7 +174,7 @@ export const SignMessageInfo = ({ request }: SignMessageProps) => { setExpanded((e) => !e)} > @@ -190,6 +197,9 @@ export const SignMessageInfo = ({ request }: SignMessageProps) => { } /> + + {operationSet && } + {!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 b611f69844..697b7a4ccc 100644 --- a/src/entries/popup/pages/messages/SignMessage/index.tsx +++ b/src/entries/popup/pages/messages/SignMessage/index.tsx @@ -1,22 +1,34 @@ +import { OperationStatus, OperationStatusType } from '@orb-labs/orby-core'; +import { + useGetOperationsToSignTransactionOrSignTypedData, + useOrby, +} from '@orb-labs/orby-react'; +import _ from 'lodash'; 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, signSVMTransaction } 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'; @@ -48,6 +60,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, }); @@ -57,13 +75,89 @@ export function SignMessage({ const selectedWallet = activeSession?.address; + const requestPayload = useMemo(() => { + return getSigningRequestDisplayDetails(request); + }, [request]); + + const { accountCluster, baseMainnetClient, getVirtualNodeRpcUrl } = useOrby(); + + const gasToken = useMemo(() => { + return selectedGasToken?.standardizedTokenId + ? { standardizedTokenId: selectedGasToken.standardizedTokenId } + : undefined; + }, [selectedGasToken]); + + const { operations, operationSet, virtualNode, isLoading, aggregateFee } = + useGetOperationsToSignTransactionOrSignTypedData( + requestPayload.orbyCallData ?? '', + undefined, + undefined, + activeSession?.address?.toLowerCase(), + activeSession?.chainId ? BigInt(activeSession.chainId) : undefined, + gasToken, + ); + + 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 (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, + ) + ) { + let result = null; + // this is for signing Solana Transaction + if (request.method == 'signTransaction') { + const txRpcUrl = getVirtualNodeRpcUrl( + activeSession?.address, + BigInt(activeSession?.chainId), + ); + + result = await signSVMTransaction( + txRpcUrl.virtualNodeRpcUrl, + requestPayload.msgData, + activeSession?.address, + ); + } else { + result = await wallet.signTypedData( + requestPayload.msgData, + activeSession?.address, + ); + } + + approveRequest(result); + } + }, + [ + activeSession, + approveRequest, + getVirtualNodeRpcUrl, + request.method, + requestPayload.msgData, + ], + ); + const onAcceptRequest = useCallback(async () => { const walletAction = getWalletActionMethod(request?.method); const requestPayload = getSigningRequestDisplayDetails(request); if (!requestPayload.msgData || !requestPayload.address || !selectedWallet) return; const { type } = await wallet.getWallet(selectedWallet); - let result = null; setLoading(true); try { @@ -73,25 +167,51 @@ 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, }); + + approveRequest(result); } else if (walletAction === 'sign_typed_data') { - result = await wallet.signTypedData( - requestPayload.msgData, - requestPayload.address, - ); - analytics.track(event.dappPromptSignTypedDataApproved, { - dappURL: dappMetadata?.appHost || '', - dappName: dappMetadata?.appName, - }); + if (!accountCluster || !virtualNode || !operationSet) { + console.error('Missing data for sign typed data'); + approveRequest(null); + return; + } + + const { success, operationResponses } = + await virtualNode.sendOperationSet( + accountCluster, + operationSet, + signOperation, + undefined, + signOperation, + ); + + if (!success) { + console.error('Error sending operation set'); + approveRequest(null); + return; + } + + if (operationResponses && operationResponses.length === 0) { + operationStatusesUpdated(OperationStatusType.SUCCESSFUL); + } else { + const ids = operationResponses + ?.map((op) => op.id) + .filter((id) => !_.isUndefined(id)); + baseMainnetClient?.subscribeToOperationStatuses( + ids, + operationStatusesUpdated, + ); + } } - approveRequest(result); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { showLedgerDisconnectedAlertIfNeeded(e); @@ -102,11 +222,16 @@ export function SignMessage({ setLoading(false); } }, [ - approveRequest, - dappMetadata?.appHost, - dappMetadata?.appName, request, selectedWallet, + dappMetadata?.appHost, + dappMetadata?.appName, + approveRequest, + accountCluster, + virtualNode, + operationSet, + baseMainnetClient, + operationStatusesUpdated, ]); const onRejectRequest = useCallback(() => { @@ -144,22 +269,64 @@ 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/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 new file mode 100644 index 0000000000..b7523a8cc1 --- /dev/null +++ b/src/entries/popup/pages/send/ChainInput.tsx @@ -0,0 +1,213 @@ +import { BlockchainInformation } from '@orb-labs/orby-core'; +import { AnimatePresence, motion } from 'framer-motion'; +import React, { + useCallback, + useImperativeHandle, + useRef, + useState, +} from 'react'; + +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 { CursorTooltip } from '../../components/Tooltip/CursorTooltip'; + +import { InputActionButton } from './InputActionButton'; + +interface ChainInputProps { + selectedChain?: BlockchainInformation; + availableChains?: BlockchainInformation[]; + onSelectChain: (chain: BlockchainInformation) => void; + onClearSelection: () => void; + onDropdownOpen: (open: boolean) => void; +} + +interface InputRefAPI { + blur: () => void; + 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 { + 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: BlockchainInformation) => { + onSelectChain(chain); + onDropdownAction(); + }, + [onDropdownAction, onSelectChain], + ); + + // useEffect(() => { + // if (!selectedChain) { + // openDropdown(); + // } + // }, [selectedChain, openDropdown]); + + const inputActionButton = ( + + ); + + return ( + <> + + } + centerComponent={ + + + + + {!selectedChain ? ( + + + + ) : ( + + + {selectedChain.name} + + + )} + + + + + } + dropdownComponent={ + + } + dropdownVisible={dropdownVisible} + rightComponent={ + selectedChain ? ( + + {inputActionButton} + + ) : ( + inputActionButton + ) + } + /> + + ); + }, +); + +const ChainList = ({ + chains, + selectChainAndCloseDropdown, +}: { + chains?: BlockchainInformation[]; + selectChainAndCloseDropdown: (chain: BlockchainInformation) => void; +}) => { + return ( + + {chains?.map((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..6ffc0cc799 100644 --- a/src/entries/popup/pages/send/ReviewSheet.tsx +++ b/src/entries/popup/pages/send/ReviewSheet.tsx @@ -1,3 +1,4 @@ +import { OperationSet, OperationStatusType } from '@orb-labs/orby-core'; import { motion } from 'framer-motion'; import React, { useCallback, @@ -6,7 +7,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 +17,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'; @@ -73,6 +76,8 @@ import playSound from '../../utils/playSound'; import { ContactAction } from './ContactPrompt'; +import { OperationSetStatus } from '.'; + const EditContactDropdown = ({ chainId, children, @@ -253,7 +258,11 @@ export const ReviewSheet = ({ onCancel, onSend, onSaveContactAction, + operationSet, + operationSetStatus, }: { + operationSetStatus?: OperationSetStatus; + operationSet?: OperationSet; show: boolean; toAddress: Address; asset?: ParsedUserAsset | null; @@ -274,7 +283,6 @@ export const ReviewSheet = ({ const [notSendingOnEthereumChecks, setNotSendingOnEthereumChecks] = useState(false); const prevShow = usePrevious(show); - const [sending, setSending] = useState(false); const confirmSendButtonRef = useRef(null); const { chains } = wagmiConfig; const assetChainId = @@ -315,18 +323,15 @@ export const ReviewSheet = ({ ); const handleSend = useCallback(async () => { - if (sendEnabled && !sending) { - setSending(true); + if (sendEnabled && !operationSetStatus?.statusSummary) { try { await onSend(); playSound('SendSound'); } catch (e) { - // - } finally { - setSending(false); + console.error(e); } } - }, [onSend, sendEnabled, sending]); + }, [onSend, sendEnabled, operationSetStatus?.statusSummary]); const { explainerSheetParams, showExplainerSheet, hideExplainerSheet } = useExplainerSheetParams(); @@ -436,7 +441,11 @@ export const ReviewSheet = ({ ) : ( - + )} @@ -542,6 +551,40 @@ export const ReviewSheet = ({ + {operationSet?.inputState && ( + + {operationSet?.inputState.getFungibleTokens()?.map((input, i) => ( + + + + + + Use{' '} + {handleSignificantDecimalsWithThreshold( + formatUnits( + input.toRawAmount(), + input.token.decimals, + ), + 2, + )}{' '} + {input.token.symbol} from{' '} + { + getChain({ chainId: Number(input.token.chainId) }) + .name + } + + + + + ))} + + )} + {notSendingOnEthereum && !isToWalletOwner && ( @@ -648,11 +691,15 @@ export const ReviewSheet = ({ testId="review-confirm-button" tabIndex={0} ref={confirmSendButtonRef} - disabled={sending} + disabled={ + operationSetStatus?.finalTransactionStatus?.status == + OperationStatusType.WAITING_PRECONDITION + } > {sendEnabled ? ( - {sending ? ( + {operationSetStatus?.finalTransactionStatus?.status == + OperationStatusType.WAITING_PRECONDITION ? ( void; + selectAssetAddressAndChain: (asset?: ParsedUserAsset | null) => void; nft?: UniqueAsset; collections?: SimpleHashCollectionDetails[]; nftSortMethod: NftSort; @@ -266,9 +262,9 @@ export const SendTokenInput = React.forwardRef< }, [dropdownVisible, inputRef]); const onSelectAsset = useCallback( - (address: AddressOrEth | '', chainId: ChainId) => { + (asset?: ParsedUserAsset | null) => { selectNft(); - selectAssetAddressAndChain(address, chainId); + selectAssetAddressAndChain(asset); setDropdownVisible(false); }, [selectAssetAddressAndChain, selectNft], @@ -277,7 +273,7 @@ export const SendTokenInput = React.forwardRef< const onSelectNft = useCallback( (nft?: UniqueAsset) => { selectNft(nft); - selectAssetAddressAndChain('', ChainId.mainnet); + selectAssetAddressAndChain(); setDropdownVisible(false); }, [selectAssetAddressAndChain, selectNft], @@ -315,7 +311,7 @@ export const SendTokenInput = React.forwardRef< }, [inputValue, collections]); const onCloseDropdown = useCallback(() => { - onSelectAsset('', ChainId.mainnet); + onSelectAsset(); onSelectNft(); setTimeout(() => { inputRef?.current?.focus(); @@ -324,8 +320,8 @@ export const SendTokenInput = React.forwardRef< }, [inputRef, onSelectAsset, onSelectNft, onDropdownAction]); const selectAsset = useCallback( - (address: AddressOrEth | '', chainId: ChainId) => { - onSelectAsset(address, chainId); + (asset?: ParsedUserAsset | null) => { + onSelectAsset(asset); setInputValue(''); }, [onSelectAsset], @@ -434,7 +430,7 @@ export const SendTokenInput = React.forwardRef< ) : ( - + ) } @@ -488,7 +484,7 @@ export const SendTokenInput = React.forwardRef< selectAsset(asset.address, asset.chainId)} + onClick={() => selectAsset(asset)} testId={`token-input-asset-${asset?.uniqueId}`} > 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 ( <> boolean; } +export interface GasTokenInput { + name: string; + standardizedTokenId?: string; + isDefault: boolean; + url?: string; +} + +export interface OperationSetStatus { + allOperationStatuses?: OperationStatus[]; + finalTransactionStatus?: OperationStatus; + statusSummary: OperationStatusType; +} + export function Send() { const [waitingForDevice, setWaitingForDevice] = useState(false); const [showReviewSheet, setShowReviewSheet] = useState(false); @@ -119,6 +146,19 @@ export function Send() { const { hidden } = useHiddenAssetStore(); const [urlSearchParams] = useSearchParams(); + const [operationSetStatus, setOperationSetStatus] = useState< + OperationSetStatus | undefined + >(undefined); + + const [selectedGasToken, setSelectedGasToken] = useState({ + name: 'no gas abstraction', + standardizedTokenId: undefined, + isDefault: true, + }); + + const { testnetMode } = useTestnetModeStore(); + const { blockchains } = useGetBlockchains(testnetMode); + const queryToAddress = urlSearchParams.get('to'); const validatedQueryToAddress = isAddress(queryToAddress as Address) ? queryToAddress @@ -140,14 +180,28 @@ export function Send() { const { asset, selectAssetAddressAndChain, + setSelectedAssetAddress, + setSelectedAssetChain, + chainId, assets, setSortMethod, sortMethod, + portfolio, } = useSendAsset(); const unhiddenAssets = useMemo( - () => assets.filter((asset) => !isHidden(asset)), - [assets, isHidden], + () => + assets.filter((asset) => { + if (chainId) { + return ( + !isHidden(asset) && + asset.relatedChainIds?.includes(chainId.toString()) + ); + } + + return !isHidden(asset); + }), + [assets, isHidden, chainId], ); const { nft, collections, nftSortMethod, setNftSortMethod, selectNft } = @@ -180,7 +234,6 @@ export function Send() { const { currentCurrency, maxAssetBalanceParams, - chainId, data, fromAddress, toAddress, @@ -191,21 +244,6 @@ export function Send() { setToAddressOrName, } = useSendState({ assetAmount, rawMaxAssetBalanceAmount, asset, nft }); - const { - buttonLabel, - isValidToAddress, - readyForReview, - validateToAddress, - toAddressIsSmartContract, - } = useSendValidations({ - asset, - assetAmount, - nft, - selectedGas, - toAddress, - toAddressOrName, - }); - const controls = useAnimationControls(); const transactionRequestForGas: TransactionRequest = useMemo(() => { if (nft) { @@ -245,18 +283,6 @@ export function Send() { [setToAddressOrName], ); - const openReviewSheet = useCallback(() => { - if (readyForReview) { - setShowReviewSheet(true); - } else { - controls.start({ - rotate: [1, -1.4, 0, 1, -1.4, 0], - transition: { duration: 0.2 }, - }); - independentFieldRef?.current?.focus(); - } - }, [readyForReview, controls, independentFieldRef]); - const closeReviewSheet = useCallback(() => setShowReviewSheet(false), []); const { @@ -271,7 +297,7 @@ export function Send() { const activeChainId = chainIdToUse( connectedToHardhat, connectedToHardhatOp, - chainId, + chainId ?? 0, // TODO(felix): this should be 1 ); const { flashbotsEnabled } = useFlashbotsEnabledStore(); @@ -348,44 +374,150 @@ export function Send() { ], ); + const { accountCluster, baseMainnetClient } = useOrby(); + + const to = useMemo(() => { + if (nft) { + return nft.asset_contract.address; + } + + 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, + activeChainId ? BigInt(activeChainId as number) : undefined, + to, + data as string, + value ? BigInt(value.toString()) : undefined, + gasToken, + ); + + const { + buttonLabel, + isValidToAddress, + readyForReview, + validateToAddress, + toAddressIsSmartContract, + } = useSendValidations({ + asset, + assetAmount, + nft, + selectedGas, + toAddress, + toAddressOrName, + operationSet, + isLoading, + }); + + const openReviewSheet = useCallback(() => { + if (readyForReview) { + setShowReviewSheet(true); + } else { + controls.start({ + rotate: [1, -1.4, 0, 1, -1.4, 0], + transition: { duration: 0.2 }, + }); + independentFieldRef?.current?.focus(); + } + }, [readyForReview, controls, independentFieldRef]); + + const operationStatusesUpdated = useCallback( + ( + 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 ( + operations && + [OperationStatusType.SUCCESSFUL, OperationStatusType.PENDING].includes( + statusSummary, + ) + ) { + const finalTransaction = operations[operations.length - 1]; + + navigate(ROUTES.HOME, { + state: { tab: 'activity' }, + }); + // callback?.(); + analytics.track(event.sendSubmitted, { + assetSymbol: asset?.symbol, + assetName: asset?.name, + assetAddress: asset?.address, + assetAmount, + chainId: Number(finalTransaction.chainId ?? 0), + }); + } + }, + [ + asset?.address, + asset?.name, + asset?.symbol, + assetAmount, + navigate, + operations, + ], + ); + const handleSend = useCallback( async (callback?: () => void) => { if (!config.send_enabled) return; try { - if (asset) { + if ( + asset && + operations && + accountCluster && + operationSet && + virtualNode + ) { + setOperationSetStatus({ + statusSummary: OperationStatusType.PENDING, + finalTransactionStatus: { + type: OperationType.FINAL_TRANSACTION, + status: OperationStatusType.WAITING_PRECONDITION, + errorReason: '', + }, + }); + const { type } = await getWallet(fromAddress); // Change the label while we wait for confirmation if (type === 'HardwareWalletKeychain') { setWaitingForDevice(true); } resetSendValues(); - const result = await sendTransaction({ - from: fromAddress, - to: txToAddress, - value, - chainId: activeChainId, - data, + + const { operationResponses, primaryOperationStatus } = + await virtualNode.sendOperationSet( + accountCluster, + operationSet, + signOperation, + undefined, + signOperation, + ); + + setOperationSetStatus({ + allOperationStatuses: operationResponses, + finalTransactionStatus: primaryOperationStatus, + statusSummary: OperationStatusType.PENDING, }); - if (result && asset) { - const transaction: NewTransaction = buildPendingTransaction(result); - addNewTransaction({ - address: fromAddress, - chainId: activeChainId, - transaction, - }); - callback?.(); - navigate(ROUTES.HOME, { - state: { tab: 'activity' }, - }); - analytics.track(event.sendSubmitted, { - assetSymbol: asset?.symbol, - assetName: asset?.name, - assetAddress: asset?.address, - assetAmount, - chainId, - }); - } + + const ids = operationResponses + ?.map((op) => op.id) + .filter((id) => !_.isUndefined(id)); + baseMainnetClient?.subscribeToOperationStatuses( + ids, + operationStatusesUpdated, + ); } else if (nft) { const { type } = await getWallet(fromAddress); // Change the label while we wait for confirmation @@ -399,7 +531,8 @@ export function Send() { chainId: activeChainId, data, }); - if (result && nft) { + + if (result && nft && chainId) { const transaction: NewTransaction = buildPendingTransaction(result); addNewTransaction({ address: fromAddress, @@ -436,18 +569,21 @@ export function Send() { } }, [ + asset, + operations, + accountCluster, + operationSet, + virtualNode, + nft, fromAddress, resetSendValues, - txToAddress, - value, + operationStatusesUpdated, + baseMainnetClient, + chainId, + buildPendingTransaction, activeChainId, data, - asset, - assetAmount, - buildPendingTransaction, - chainId, navigate, - nft, ], ); @@ -470,6 +606,25 @@ export function Send() { ], ); + const onSelectAssetAddressAndChain = useCallback( + (asset?: ParsedUserAsset | null) => { + if (asset) { + const token = portfolio + ?.find((sBalance) => sBalance.standardizedTokenId == asset?.uniqueId) + ?.tokenBalancesOnChains.find( + (tBalance) => chainId && tBalance.token.chainId == BigInt(chainId), + )?.token; + + if (token) { + selectAsset(token?.address as Address, Number(token.chainId)); + } + } else { + setSelectedAssetAddress(''); + } + }, + [chainId, portfolio, selectAsset, setSelectedAssetAddress], + ); + useEffect(() => { return () => { clearCustomGasModified(); @@ -499,9 +654,17 @@ export function Send() { }, [hideExplainerSheet, showExplainerSheet]); useEffect(() => { + const token = portfolio + ?.find( + (sBalance) => sBalance.standardizedTokenId == selectedToken?.uniqueId, + ) + ?.tokenBalancesOnChains.find( + (tBalance) => chainId && tBalance.token.chainId == BigInt(chainId), + )?.token; + // navigating from token row - if (selectedToken) { - selectAsset(selectedToken.address, selectedToken.chainId); + if (token) { + selectAsset(token?.address as Address, Number(token.chainId)); // clear selected token setSelectedToken(); } else if (selectedNft) { @@ -627,6 +790,7 @@ export function Send() { @@ -689,6 +854,22 @@ export function Send() { /> + + Number(c.chainId) === chainId, + )} + onSelectChain={(chain) => + setSelectedAssetChain( + chain?.chainId ? Number(chain.chainId) : undefined, + ) + } + onDropdownOpen={() => {}} + onClearSelection={() => setSelectedAssetChain(undefined)} + /> + + - {isValidToAddress && (!!asset || !!nft) ? ( + {isValidToAddress && (!!asset || !!nft) && chainId ? ( @@ -760,6 +941,13 @@ export function Send() { transactionRequest={transactionRequestForGas} accentColor={assetAccentColor} flashbotsEnabled={flashbotsEnabledGlobally} + selectedGasToken={selectedGasToken} + setSelectedGasToken={(token) => { + if (token) { + setSelectedGasToken(token); + } + }} + aggregateFee={aggregateFee} /> diff --git a/src/entries/popup/pages/swap/SwapReviewSheet/SwapReviewSheet.tsx b/src/entries/popup/pages/swap/SwapReviewSheet/SwapReviewSheet.tsx index 15f70ce2b5..a1cc3077a5 100644 --- a/src/entries/popup/pages/swap/SwapReviewSheet/SwapReviewSheet.tsx +++ b/src/entries/popup/pages/swap/SwapReviewSheet/SwapReviewSheet.tsx @@ -1,5 +1,14 @@ +import { + OperationSet, + OperationStatus, + OperationStatusType, + OperationType, +} from '@orb-labs/orby-core'; +import { useOrby } from '@orb-labs/orby-react'; +import { OrbyActions } from '@orb-labs/orby-viem-extension'; import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; import { motion } from 'framer-motion'; +import _ from 'lodash'; import React, { useCallback, useEffect, @@ -7,7 +16,7 @@ import React, { useRef, useState, } from 'react'; -import { Address } from 'viem'; +import { Address, Client, HttpTransport, PublicRpcSchema } from 'viem'; import { i18n } from '~/core/languages'; import { useGasStore } from '~/core/state'; @@ -16,6 +25,7 @@ import { ChainId } from '~/core/types/chains'; import { KeychainType } from '~/core/types/keychainTypes'; import { truncateAddress } from '~/core/utils/address'; import { processExchangeRateArray } from '~/core/utils/numbers'; +import { signOperation } from '~/core/utils/orb'; import { isUnwrapEth, isWrapEth } from '~/core/utils/swaps'; import { Bleed, @@ -54,6 +64,7 @@ import { useUserAsset } from '~/entries/popup/hooks/useUserAsset'; import { ROUTES } from '~/entries/popup/urls'; import { zIndexes } from '~/entries/popup/utils/zIndexes'; +import { GasTokenInput } from '../../send'; import { onSwap } from '../onSwap'; import { SwapAssetCard } from './SwapAssetCard'; @@ -163,8 +174,19 @@ export type SwapReviewSheetProps = { assetToSellValue?: string; assetToBuy?: ParsedSearchAsset | null; quote?: Quote | CrosschainQuote | QuoteError; + operationSet?: OperationSet | null; + virtualNode: Client< + HttpTransport, + undefined, + undefined, + PublicRpcSchema, + OrbyActions + >; flashbotsEnabled: boolean; hideSwapReview: () => void; + selectedGasToken: GasTokenInput; + setSelectedGasToken: (gasToken?: GasTokenInput) => void; + aggregateFee: string; }; export const SwapReviewSheet = ({ @@ -175,6 +197,11 @@ export const SwapReviewSheet = ({ quote, flashbotsEnabled, hideSwapReview, + operationSet, + virtualNode, + selectedGasToken, + setSelectedGasToken, + aggregateFee, }: SwapReviewSheetProps) => { if (!quote || !assetToBuy || !assetToSell || (quote as QuoteError)?.error) return null; @@ -187,6 +214,11 @@ export const SwapReviewSheet = ({ quote={quote as Quote | CrosschainQuote} flashbotsEnabled={flashbotsEnabled} hideSwapReview={hideSwapReview} + selectedGasToken={selectedGasToken} + setSelectedGasToken={setSelectedGasToken} + aggregateFee={aggregateFee} + operationSet={operationSet} + virtualNode={virtualNode} /> ); }; @@ -199,6 +231,17 @@ type SwapReviewSheetWithQuoteProps = { quote: Quote | CrosschainQuote; flashbotsEnabled: boolean; hideSwapReview: () => void; + operationSet?: OperationSet | null; + virtualNode?: Client< + HttpTransport, + undefined, + undefined, + PublicRpcSchema, + OrbyActions + >; + selectedGasToken: GasTokenInput; + setSelectedGasToken: (gasToken?: GasTokenInput) => void; + aggregateFee: string; }; const SwapReviewSheetWithQuote = ({ @@ -209,11 +252,18 @@ const SwapReviewSheetWithQuote = ({ quote, flashbotsEnabled, hideSwapReview, + operationSet, + virtualNode, + selectedGasToken, + setSelectedGasToken, + aggregateFee, }: SwapReviewSheetWithQuoteProps) => { const navigate = useRainbowNavigate(); const [showMoreDetails, setShowDetails] = useState(false); const [sendingSwap, setSendingSwap] = useState(false); + const [isSendingFinalTransaction, setIsSendSendingFinalTransaction] = + useState(false); const selectedGas = useGasStore.use.selectedGas(); const confirmSwapButtonRef = useRef(null); const { type } = useCurrentWalletTypeAndVendor(); @@ -229,6 +279,7 @@ const SwapReviewSheetWithQuote = ({ assetToSell, assetToSellValue, selectedGas, + operationSet, }); const { minimumReceived, swappingRoute, includedFee, exchangeRate } = @@ -264,6 +315,41 @@ const SwapReviewSheetWithQuote = ({ const openMoreDetails = useCallback(() => setShowDetails(true), []); const closeMoreDetails = useCallback(() => setShowDetails(false), []); + const { accountCluster, baseMainnetClient } = useOrby(); + + 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 ( + !isSendingFinalTransaction && + [OperationStatusType.SUCCESSFUL, OperationStatusType.PENDING].includes( + statusSummary, + ) + ) { + setIsSendSendingFinalTransaction(true); + const swapExecutedSuccessfully = await onSwap({ + quote: quote as Quote, + assetToSell, + assetToBuy, + degenMode: true, + }); + + setSendingSwap(false); + setIsSendSendingFinalTransaction(false); + + if (swapExecutedSuccessfully) { + navigate(ROUTES.HOME, { state: { tab: 'tokens' } }); + } + } + }, + [isSendingFinalTransaction, quote, assetToSell, assetToBuy, navigate], + ); + const handleSwap = useCallback(async () => { if (!enoughNativeAssetBalanceForGas) { alert( @@ -272,27 +358,51 @@ const SwapReviewSheetWithQuote = ({ }), ); return; + } else if (!accountCluster || !operationSet || !virtualNode) { + return; } setSendingSwap(true); - const swapExecutedSuccessfully = await onSwap({ - assetToSell, - assetToBuy, - quote, - degenMode: false, - }); - setSendingSwap(false); + if (operationSet?.intents && operationSet?.intents?.length == 0) { + operationStatusesUpdated(OperationStatusType.SUCCESSFUL, undefined, []); + return; + } - if (swapExecutedSuccessfully) { - navigate(ROUTES.HOME, { state: { tab: 'tokens' } }); + operationSet.primaryOperation = undefined; + + const operation = operationSet.intents + ?.map((intent) => intent.intentOperations) + ?.flat() + ?.find((operation) => operation?.type == OperationType.SUBMIT_INTENT); + + if (operation) { + operation.type = OperationType.FINAL_TRANSACTION; } + + const { operationResponses } = await virtualNode.sendOperationSet( + accountCluster, + operationSet, + signOperation, + undefined, + signOperation, + ); + + const ids = operationResponses + ?.map((op) => op.id) + .filter((id) => !_.isUndefined(id)); + + baseMainnetClient?.subscribeToOperationStatuses( + ids, + operationStatusesUpdated, + ); }, [ - assetToBuy, - assetToSell, + accountCluster, + baseMainnetClient, enoughNativeAssetBalanceForGas, nativeAsset?.symbol, - navigate, - quote, + operationSet, + operationStatusesUpdated, + virtualNode, ]); const goBack = useCallback(() => { @@ -620,6 +730,13 @@ const SwapReviewSheetWithQuote = ({ enabled={show} defaultSpeed={selectedGas.option} speedMenuMarginRight="12px" + selectedGasToken={selectedGasToken} + setSelectedGasToken={(token) => { + if (token) { + setSelectedGasToken(token); + } + }} + aggregateFee={aggregateFee} /> 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..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'; @@ -26,8 +25,7 @@ export type TokenToSellRowProps = { uniqueId: UniqueId; }; -export function TokenToSellRow({ uniqueId }: TokenToSellRowProps) { - const { data: asset } = useUserAsset(uniqueId); +export function TokenToSellRow({ asset }) { const { hideAssetBalances } = useHideAssetBalancesStore(); const { currentCurrency } = useCurrentCurrencyStore(); @@ -102,7 +100,7 @@ export function TokenToSellRow({ uniqueId }: TokenToSellRowProps) { - + {leftColumn} {rightColumn} diff --git a/src/entries/popup/pages/swap/index.tsx b/src/entries/popup/pages/swap/index.tsx index 9b57f71ecb..3949e08ab7 100644 --- a/src/entries/popup/pages/swap/index.tsx +++ b/src/entries/popup/pages/swap/index.tsx @@ -1,10 +1,15 @@ +import { PopulatedTransaction } from '@ethersproject/contracts'; +import { OperationSet } from '@orb-labs/orby-core'; +import { useGetOperationsToExecuteTransaction } from '@orb-labs/orby-react'; +import { OrbyActions } from '@orb-labs/orby-viem-extension'; import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { Address } from 'viem'; +import { Address, Client, HttpTransport, PublicRpcSchema } from 'viem'; import config from '~/core/firebase/remoteConfig'; import { i18n } from '~/core/languages'; +import { populateSwap } from '~/core/raps/utils'; import { shortcuts } from '~/core/references/shortcuts'; import { useCurrentAddressStore, useGasStore } from '~/core/state'; import { useFeatureFlagsStore } from '~/core/state/currentSettings/featureFlags'; @@ -19,6 +24,7 @@ import { ParsedSearchAsset, ParsedUserAsset } from '~/core/types/assets'; import { ChainId } from '~/core/types/chains'; import { SearchAsset } from '~/core/types/search'; import { getQuoteServiceTime } from '~/core/utils/swaps'; +import { getProvider } from '~/core/wagmi/clientToProvider'; import { Box, Button, @@ -71,6 +77,7 @@ import { useTranslationContext, } from '../../hooks/useTranslationContext'; import { getActiveElement, getInputIsFocused } from '../../utils/activeElement'; +import { GasTokenInput } from '../send'; import { SwapReviewSheet } from './SwapReviewSheet/SwapReviewSheet'; import { SwapSettings } from './SwapSettings/SwapSettings'; @@ -305,6 +312,8 @@ function SwapButton({ showSwapReviewSheet, showExplainerSheet, hideExplainerSheet, + operationSet, + virtualNode, }: { quote: Quote | CrosschainQuote | QuoteError | undefined; assetToSell: ParsedSearchAsset | null; @@ -312,21 +321,26 @@ function SwapButton({ assetToBuy: ParsedSearchAsset | null; timeEstimate: SwapTimeEstimate | null; isLoadingQuote: boolean; + operationSet?: OperationSet | null; showSwapReviewSheet: () => void; showExplainerSheet: (p: ExplainerSheetProps) => void; hideExplainerSheet: () => void; + virtualNode?: Client< + HttpTransport, + undefined, + undefined, + PublicRpcSchema, + OrbyActions + >; }) { const { selectedGas } = useGasStore(); - const { - buttonLabel: validationButtonLabel, - enoughAssetsForSwap, - readyForReview, - } = useSwapValidations({ - assetToSell, - assetToSellValue, - selectedGas, - }); + const { buttonLabel: validationButtonLabel, enoughAssetsForSwap } = + useSwapValidations({ + assetToSell, + assetToSellValue, + selectedGas, + }); const isDegenModeEnabled = useDegenMode((s) => s.isDegenModeEnabled); @@ -348,10 +362,12 @@ function SwapButton({ showExplainerSheet, hideExplainerSheet, showSwapReviewSheet() { - if (readyForReview) showSwapReviewSheet(); + showSwapReviewSheet(); }, isDegenModeEnabled, timeEstimate, + operationSet, + virtualNode, }); return ( @@ -393,6 +409,13 @@ export function Swap({ bridge = false }: { bridge?: boolean }) { useState(false); const { isFirefox } = useBrowser(); + const [selectedGasToken, setSelectedGasToken] = useState({ + name: 'no gas abstraction', + standardizedTokenId: undefined, // this isn't used + isDefault: true, + // url is not used for default, instead we use the chain logo + }); + // translate based on the context, bridge or swap const translationContext = { Action: i18n.t(`swap._actions.${bridge ? 'Bridge' : 'Swap'}`), @@ -544,6 +567,50 @@ export function Swap({ bridge = false }: { bridge?: boolean }) { : slippage, }); + const quoteData = useMemo(() => { + if (quote && !('error' in quote)) { + return quote as Quote | CrosschainQuote; + } + return undefined; + }, [quote]); + + const [swapTransaction, setSwapTransaction] = + useState(null); + useEffect(() => { + const fetchSwapTransaction = async () => { + if (!quoteData) { + return undefined; + } + + const transaction = await populateSwap({ + provider: getProvider({ chainId: quoteData?.chainId }), + quote: quoteData, + }); + + setSwapTransaction(transaction); + }; + + fetchSwapTransaction(); + }, [quoteData]); + + const gasToken = useMemo(() => { + return selectedGasToken?.standardizedTokenId + ? { standardizedTokenId: selectedGasToken.standardizedTokenId } + : undefined; + }, [selectedGasToken]); + + const { operationSet, virtualNode, aggregateFee, isLoading } = + useGetOperationsToExecuteTransaction( + swapTransaction?.from?.toLowerCase(), + quoteData?.chainId ? BigInt(quoteData?.chainId as number) : undefined, + swapTransaction?.to, + swapTransaction?.data as string, + swapTransaction?.value + ? BigInt(swapTransaction?.value.toString()) + : undefined, + gasToken, + ); + const { assetToSellNativeDisplay, assetToBuyNativeDisplay } = useSwapNativeAmounts({ assetToBuy, @@ -563,9 +630,11 @@ export function Swap({ bridge = false }: { bridge?: boolean }) { }); const showSwapReviewSheet = useCallback(() => { - setShowSwapReview(true); - setInReviewSheet(true); - }, []); + if (operationSet) { + setShowSwapReview(true); + setInReviewSheet(true); + } + }, [operationSet]); const timeEstimate = getSwapTimeEstimate(quote); @@ -594,6 +663,13 @@ export function Swap({ bridge = false }: { bridge?: boolean }) { resetSwapValues, } = usePopupInstanceStore(); + const setOutputChain = useCallback( + (chainId: ChainId) => { + setOutputChainId?.(chainId); + }, + [setOutputChainId], + ); + const [didPopulateSavedTokens, setDidPopulateSavedTokens] = useState(false); const [didPopulateSavedInputValues, setDidPopulateSavedInputValues] = useState(false); @@ -604,6 +680,7 @@ export function Swap({ bridge = false }: { bridge?: boolean }) { const selectedSearchAsset = assetsToSell.find( (asset) => asset?.uniqueId === selectedTokenId, ); + if (selectedSearchAsset) { selectAssetToSell(selectedSearchAsset); // clear selected token @@ -738,6 +815,16 @@ export function Swap({ bridge = false }: { bridge?: boolean }) { flashbotsEnabled={flashbotsEnabledGlobally} hideSwapReview={hideSwapReviewSheet} assetToSellValue={assetToSellValue} + selectedGasToken={selectedGasToken} + setSelectedGasToken={(token) => { + if (token) { + setSelectedGasToken(token); + } + }} + aggregateFee={aggregateFee} + operationSet={operationSet} + // @ts-ignore + virtualNode={virtualNode} /> { + if (token) { + setSelectedGasToken(token); + } + }} + aggregateFee={aggregateFee} /> void; isDegenModeEnabled: boolean; timeEstimate: SwapTimeEstimate | null; + operationSet?: OperationSet | null; + virtualNode?: Client< + HttpTransport, + undefined, + undefined, + PublicRpcSchema, + OrbyActions + >; } interface SwapButton { @@ -50,23 +70,57 @@ interface SwapButton { export const useSwapButton = ({ quote, + operationSet, isLoading, assetToSell, assetToBuy, - enoughAssetsForSwap, validationButtonLabel, hideExplainerSheet, showExplainerSheet, showSwapReviewSheet, isDegenModeEnabled, timeEstimate, + virtualNode, }: UseSwapButtonArgs): SwapButton => { const [status, setStatus] = useState<'idle' | 'degen_swapping'>('idle'); + const [isSendingFinalTransaction, setIsSendSendingFinalTransaction] = + useState(false); const t = useTranslationContext(); const navigate = useRainbowNavigate(); const { type } = useCurrentWalletTypeAndVendor(); + const { accountCluster, baseMainnetClient } = useOrby(); const isHardwareWallet = type === KeychainType.HardwareWalletKeychain; + 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 ( + !isSendingFinalTransaction && + [ + (OperationStatusType.SUCCESSFUL, OperationStatusType.PENDING), + ].includes(statusSummary) + ) { + setIsSendSendingFinalTransaction(true); + const swapExecutedSuccessfully = await onSwap({ + quote: quote as Quote, + assetToSell, + assetToBuy, + degenMode: true, + }); + + if (swapExecutedSuccessfully) { + navigate(ROUTES.HOME, { state: { tab: 'activity' } }); + } + } + }, + [isSendingFinalTransaction, navigate, quote, assetToSell, assetToBuy], + ); + if (isLoading) { return { buttonColor: 'surfaceSecondary', @@ -101,7 +155,7 @@ export const useSwapButton = ({ } if (!(quote as QuoteError).error) { - if (!enoughAssetsForSwap) { + if (operationSet?.status == CreateOperationsStatus.INSUFFICIENT_FUNDS) { return { buttonColor: 'fillSecondary', buttonDisabled: true, @@ -145,16 +199,39 @@ export const useSwapButton = ({ 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' } }); + if (virtualNode && accountCluster && operationSet) { + operationSet.primaryOperation = undefined; + + const operation = operationSet.intents + ?.map((intent) => intent.intentOperations) + ?.flat() + ?.find( + (operation) => operation?.type == OperationType.SUBMIT_INTENT, + ); + + if (operation) { + operation.type = OperationType.FINAL_TRANSACTION; + } + + const { operationResponses } = await virtualNode.sendOperationSet( + accountCluster, + operationSet, + signOperation, + undefined, + signOperation, + ); + + const ids = operationResponses + ?.map((op) => op.id) + .filter((id) => !_.isUndefined(id)); + + baseMainnetClient?.subscribeToOperationStatuses( + ids, + operationStatusesUpdated, + ); } + + setStatus('idle'); }, status: 'ready', }; @@ -176,7 +253,11 @@ export const useSwapButton = ({ icon: ( - + @@ -275,7 +356,9 @@ export const useSwapButton = ({ buttonAction: () => showExplainerSheet({ show: true, - header: { icon: }, + header: { + icon: , + }, title: t('swap.explainers.fee_on_transfer_token.title'), description: [ @@ -327,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 7c9faa6e72..e94f63e3d4 100644 --- a/static/allowlist.json +++ b/static/allowlist.json @@ -1,5 +1,14 @@ { "urls": [ + "ws://chrome-extension", + "http://localhost:4001", + "http://localhost:4001", + "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", @@ -42,4 +51,4 @@ "https://nftp.rainbow.me/", "https://gateway-arbitrum.network.thegraph.com" ] -} \ No newline at end of file +} diff --git a/static/assets/rainbow/og-orblabs.png b/static/assets/rainbow/og-orblabs.png new file mode 100644 index 0000000000..c75dcaf53d Binary files /dev/null and b/static/assets/rainbow/og-orblabs.png differ diff --git a/static/assets/rainbow/og-orby.png b/static/assets/rainbow/og-orby.png new file mode 100644 index 0000000000..02c61565b6 Binary files /dev/null and b/static/assets/rainbow/og-orby.png differ diff --git a/static/assets/rainbow/og-orby@2x.png b/static/assets/rainbow/og-orby@2x.png new file mode 100644 index 0000000000..ca6ca3d98d Binary files /dev/null and b/static/assets/rainbow/og-orby@2x.png differ diff --git a/static/images/icon-16.png b/static/images/icon-16.png index e94b73398f..9a331736d9 100644 Binary files a/static/images/icon-16.png and b/static/images/icon-16.png differ diff --git a/static/images/icon-16@2x.png b/static/images/icon-16@2x.png index f1de91c809..33d8709498 100644 Binary files a/static/images/icon-16@2x.png and b/static/images/icon-16@2x.png differ diff --git a/static/images/icon-16@32x.png b/static/images/icon-16@32x.png index 4110f7f894..bb67359eae 100644 Binary files a/static/images/icon-16@32x.png and b/static/images/icon-16@32x.png differ diff --git a/static/images/icon-16@4x.png b/static/images/icon-16@4x.png index f2ba1ea412..f6c25697e5 100644 Binary files a/static/images/icon-16@4x.png and b/static/images/icon-16@4x.png differ diff --git a/static/images/icon-16@8x.png b/static/images/icon-16@8x.png index cc702c4547..37bca444cf 100644 Binary files a/static/images/icon-16@8x.png and b/static/images/icon-16@8x.png differ diff --git a/static/images/icon-19.png b/static/images/icon-19.png index 2b9e3ab5e1..ae6fd73690 100644 Binary files a/static/images/icon-19.png and b/static/images/icon-19.png differ diff --git a/static/images/icon-19@2x.png b/static/images/icon-19@2x.png index 533f26f78b..f713beb176 100644 Binary files a/static/images/icon-19@2x.png and b/static/images/icon-19@2x.png differ diff --git a/static/json/languages/en_US.json b/static/json/languages/en_US.json index f572f46de6..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", @@ -662,7 +662,10 @@ "enter_valid_address": "Enter a valid address", "enter_address": "Enter an address", "enter_amount": "Enter an amount", + "processing": "Loading...", "insufficient_asset": "Insufficient %{symbol}", + "insufficient_gas_funds": "Insufficient funds for gas", + "no_execution_path": "No execution path", "insufficient_native_asset_for_gas": "Insufficient %{symbol} for gas" }, "wallets_list": { @@ -781,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", @@ -812,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", @@ -911,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" @@ -984,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", @@ -1010,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", @@ -1068,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" @@ -1123,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", @@ -1614,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", @@ -1637,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 7d104e245b..b87b2cfc42 100644 --- a/static/manifest.json +++ b/static/manifest.json @@ -11,41 +11,28 @@ }, "default_popup": "popup.html" }, - "author": "https://rainbow.me", + "author": "https://www.orblabs.xyz/", "background": { "service_worker": "background.js" }, "content_scripts": [ { "all_frames": true, - "js": [ - "contentscript.js" - ], - "matches": [ - "http://*/*", - "https://*/*" - ], + "js": ["contentscript.js"], + "matches": ["http://*/*", "https://*/*"], "run_at": "document_start" }, { - "matches": [ - "*://connect.trezor.io/9/popup.html" - ], - "js": [ - "./vendor/trezor-content-script.js" - ] + "matches": ["*://connect.trezor.io/9/popup.html"], + "js": ["./vendor/trezor-content-script.js"] } ], "content_security_policy": { "extension_pages": "frame-ancestors 'none'; script-src 'self'; object-src 'self'; connect-src 'self'" }, "default_locale": "en_US", - "description": "DEV VERSION", - "host_permissions": [ - "http://*/*", - "https://*/*", - "wss://*/*" - ], + "description": "OrbyPlayground is a playground extension for showcasing the power of Orby.", + "host_permissions": ["http://*/*", "https://*/*", "wss://*/*"], "icons": { "16": "images/icon-16.png", "19": "images/icon-19.png", @@ -57,7 +44,7 @@ }, "manifest_version": 3, "minimum_chrome_version": "88", - "name": "Rainbow DEVELOPMENT BUILD", + "name": "OrbyPlayground", "permissions": [ "activeTab", "clipboardWrite", @@ -67,18 +54,17 @@ "unlimitedStorage", "notifications" ], - "short_name": "Rainbow", - "version": "1.5.48", + "short_name": "OrbyPlayground", + "version": "0.0.5", "web_accessible_resources": [ { - "matches": [ - "" - ], + "matches": [""], "resources": [ "inpage.js", "*.woff2", "popup.css", - "assets/badges/*.png" + "assets/badges/*.png", + "webpage.js" ] } ], @@ -90,7 +76,7 @@ "chromeos": "Alt+Shift+R", "linux": "Alt+Shift+R" }, - "description": "Open the Rainbow Wallet extension" + "description": "Open the OrbyPlayground Wallet extension" } } -} \ No newline at end of file +} diff --git a/tsconfig.json b/tsconfig.json index 5cc3c6dcf7..bd84eea54a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,8 +17,16 @@ "baseUrl": ".", "paths": { "~/*": ["src/*"] - }, + } }, - "include": ["src", "e2e", ".env.d.ts"], - "exclude": ["vitest.config.*", "e2e/serial/vitest.config.ts", "design-system/docs", "static/", "node_modules", "dist", "build"], + "include": ["src", "e2e", ".env.d.ts", "build/webpage.js"], + "exclude": [ + "vitest.config.*", + "e2e/serial/vitest.config.ts", + "design-system/docs", + "static/", + "node_modules", + "dist", + "build" + ] } diff --git a/yarn.lock b/yarn.lock index c80b73785e..fd5449777a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +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== + "@ampproject/remapping@^2.1.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" @@ -472,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" @@ -942,7 +959,7 @@ "@ethersproject/logger" "^5.7.0" "@ethersproject/properties" "^5.7.0" -"@ethersproject/address@5.7.0", "@ethersproject/address@^5.7.0": +"@ethersproject/address@5.7.0", "@ethersproject/address@^5.0.2", "@ethersproject/address@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.7.0.tgz#19b56c4d74a3b0a46bfdbb6cfcc0a153fc697f37" integrity sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA== @@ -2870,6 +2887,13 @@ dependencies: "@noble/hashes" "1.4.0" +"@noble/curves@1.7.0", "@noble/curves@^1.6.0", "@noble/curves@~1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.7.0.tgz#0512360622439256df892f21d25b388f52505e45" + integrity sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw== + dependencies: + "@noble/hashes" "1.6.0" + "@noble/curves@^1.2.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.3.0.tgz#01be46da4fd195822dab821e72f71bf4aeec635e" @@ -2884,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" @@ -2911,6 +2942,21 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== +"@noble/hashes@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.0.tgz#d4bfb516ad6e7b5111c216a5cc7075f4cf19e6c5" + integrity sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ== + +"@noble/hashes@1.6.1", "@noble/hashes@^1.5.0", "@noble/hashes@~1.6.0": + version "1.6.1" + 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" @@ -2989,6 +3035,39 @@ 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.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.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.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.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.21" + ethers "6" + "@parcel/watcher-android-arm64@2.4.1": version "2.4.1" resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz#c2c19a3c442313ff007d2d7a9c2c1dd3e1c9ca84" @@ -4102,6 +4181,11 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.7.tgz#fe973311a5c6267846aa131bc72e96c5d40d2b30" integrity sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g== +"@scure/base@~1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.1.tgz#dd0b2a533063ca612c17aa9ad26424a2ff5aa865" + integrity sha512-DGmGtC8Tt63J5GfHgfl5CuAXh96VF/LD8K9Hr/Gv0J2lAoRGlPOMpqMpMbCTOoOJMZCk2Xt+DskdDyn6dEFdzQ== + "@scure/bip32@1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.1.tgz#7248aea723667f98160f593d621c47e208ccbb10" @@ -4129,7 +4213,16 @@ "@noble/hashes" "~1.4.0" "@scure/base" "~1.1.6" -"@scure/bip39@1.2.1", "@scure/bip39@1.3.0": +"@scure/bip32@1.6.0", "@scure/bip32@^1.5.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.6.0.tgz#6dbc6b4af7c9101b351f41231a879d8da47e0891" + integrity sha512-82q1QfklrUUdXJzjuRU7iG7D7XiFx5PHYVS0+oeNKhyDLT7WPqs6pBcM2W5ZdwOwKCwoE1Vy1se+DHjcXwCYnA== + dependencies: + "@noble/curves" "~1.7.0" + "@noble/hashes" "~1.6.0" + "@scure/base" "~1.2.1" + +"@scure/bip39@1.2.1", "@scure/bip39@1.3.0", "@scure/bip39@1.5.0", "@scure/bip39@^1.4.0": version "1.2.1" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a" integrity sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg== @@ -4249,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" @@ -4270,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" @@ -4404,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" @@ -4728,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" @@ -4898,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" @@ -4962,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" @@ -5112,6 +5270,21 @@ "@typescript-eslint/types" "6.4.1" eslint-visitor-keys "^3.4.1" +"@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" + integrity sha512-OME7WR6+5QwQs45A2079r+/FS0zU944+JCQwUX9GyIriCxqw2pGu4F9IEqmlwD+zSIMml0+MJnJJ47pFgSyWDw== + dependencies: + "@ethersproject/address" "^5.0.2" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "5.7.0" + "@ethersproject/strings" "5.7.0" + big.js "^5.2.2" + decimal.js-light "^2.5.0" + jsbi "^3.1.4" + tiny-invariant "^1.1.0" + toformat "^2.0.0" + "@vanilla-extract/babel-plugin-debug-ids@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@vanilla-extract/babel-plugin-debug-ids/-/babel-plugin-debug-ids-1.0.0.tgz#82e18dc1b9aa897e3a5798720d6b14cdfeffe908" @@ -5351,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" @@ -5834,6 +6019,11 @@ abitype@1.0.5: resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.5.tgz#29d0daa3eea867ca90f7e4123144c1d1270774b6" integrity sha512-YzDhti7cjlfaBhHutMaboYB21Ha3rXR9QTkNJFzYC4kC8YclaiwPBBBJY8ejFdu2wnJeZCVZSMlQJ7fi8S6hsw== +abitype@1.0.7, abitype@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.7.tgz#876a0005d211e1c9132825d45bcee7b46416b284" + integrity sha512-ZfYYSktDQUwc2eduYu8C4wOs+RDPmnRYMh7zNfzeMtGGgb0U+6tLGjixUic6mXf5xKKCcgT5Qp6cv39tOARVFw== + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -5955,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" @@ -6502,6 +6697,11 @@ base-x@^3.0.2: dependencies: safe-buffer "^5.0.1" +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" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -6586,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" @@ -6829,6 +7036,13 @@ browserslist@^4.21.3: node-releases "^2.0.6" update-browserslist-db "^1.0.9" +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 "^5.0.0" + bs58@^4.0.0, bs58@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" @@ -7703,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== @@ -7948,6 +8162,11 @@ decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== +decimal.js-light@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" + integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== + decode-uri-component@0.2.1, decode-uri-component@^0.2.2: version "0.2.1" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.1.tgz#e9d7afd716fc1a7ec6ae7cc0aa3e540a1eac2e9d" @@ -8403,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" @@ -9309,6 +9536,19 @@ ethers@5.7.2, ethers@^5.0.0: "@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" @@ -11648,6 +11888,11 @@ isows@1.0.4: resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.4.tgz#810cd0d90cc4995c26395d2aa4cfa4037ebdf061" integrity sha512-hEzjY+x9u9hPmBom9IIAqdJCwNLax+xrPb51vEPpERoFlIxgmZcHzsT5jKG06nvInKOBGvReAVz80Umed5CczQ== +isows@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.6.tgz#0da29d706fa51551c663c627ace42769850f86e7" + integrity sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw== + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -11718,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" @@ -12163,6 +12426,11 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +jsbi@^3.1.4: + version "3.2.5" + resolved "https://registry.yarnpkg.com/jsbi/-/jsbi-3.2.5.tgz#b37bb90e0e5c2814c1c2a1bcd8c729888a2e37d6" + integrity sha512-aBE4n43IPvjaddScbvWRA2YlTzKEynHzu7MqOyTipdHucf/VxS63ViCjxYRg86M8Rxwbt/GfzHl1kKERkt45fQ== + jsbn@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" @@ -13906,6 +14174,19 @@ outvariant@^1.4.0: resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.4.0.tgz#e742e4bda77692da3eca698ef5bfac62d9fba06e" integrity sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw== +ox@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ox/-/ox-0.1.2.tgz#0f791be2ccabeaf4928e6d423498fe1c8094e560" + integrity sha512-ak/8K0Rtphg9vnRJlbOdaX9R7cmxD2MiSthjWGaQdMk3D7hrAlDoM+6Lxn7hN52Za3vrXfZ7enfke/5WjolDww== + dependencies: + "@adraffy/ens-normalize" "^1.10.1" + "@noble/curves" "^1.6.0" + "@noble/hashes" "^1.5.0" + "@scure/bip32" "^1.5.0" + "@scure/bip39" "^1.4.0" + abitype "^1.0.6" + eventemitter3 "5.0.1" + p-cancelable@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050" @@ -15410,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" @@ -16313,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" @@ -16515,6 +16817,11 @@ tiny-invariant@^1.0.6: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== +tiny-invariant@^1.1.0: + version "1.3.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== + tinybench@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.5.0.tgz#4711c99bbf6f3e986f67eb722fed9cddb3a68ba5" @@ -16576,6 +16883,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +toformat@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/toformat/-/toformat-2.0.0.tgz#7a043fd2dfbe9021a4e36e508835ba32056739d8" + integrity sha512-03SWBVop6nU8bpyZCx7SodpYznbZF5R4ljwNLBcTQzKOD9xuihRo/psX58llS1BMFhhAI08H3luot5GoXJz2pQ== + tosource@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/tosource/-/tosource-1.0.0.tgz#42d88dd116618bcf00d6106dd5446f3427902ff1" @@ -16651,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" @@ -16676,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" @@ -16700,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" @@ -16869,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" @@ -17239,6 +17561,21 @@ viem@2.21.0: webauthn-p256 "0.0.5" ws "8.17.1" +viem@^2.9.25: + version "2.21.55" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.21.55.tgz#a57ad31fcf2a0f6c011b1909f02c94421ec4f781" + integrity sha512-PgXew7C11cAuEtOSgRyQx2kJxEOPUwIwZA9dMglRByqJuFVA7wSGZZOOo/93iylAA8E15bEdqy9xulU3oKZ70Q== + dependencies: + "@noble/curves" "1.7.0" + "@noble/hashes" "1.6.1" + "@scure/bip32" "1.6.0" + "@scure/bip39" "1.5.0" + abitype "1.0.7" + isows "1.0.6" + ox "0.1.2" + webauthn-p256 "0.0.10" + ws "8.18.0" + vite-node@0.34.6: version "0.34.6" resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.34.6.tgz#34d19795de1498562bf21541a58edcd106328a17" @@ -17393,6 +17730,14 @@ web-streams-polyfill@^3.2.1: resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== +webauthn-p256@0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/webauthn-p256/-/webauthn-p256-0.0.10.tgz#877e75abe8348d3e14485932968edf3325fd2fdd" + integrity sha512-EeYD+gmIT80YkSIDb2iWq0lq2zbHo1CxHlQTeJ+KkCILWpVy3zASH3ByD4bopzfk0uCwXxLqKGLqp2W4O28VFA== + dependencies: + "@noble/curves" "^1.4.0" + "@noble/hashes" "^1.4.0" + webauthn-p256@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/webauthn-p256/-/webauthn-p256-0.0.5.tgz#0baebd2ba8a414b21cc09c0d40f9dd0be96a06bd" @@ -17772,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@^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==