diff --git a/.eslintrc.js b/.eslintrc.js index 4e8368a6..87b95af4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -38,6 +38,8 @@ module.exports = { 'simple-import-sort/exports': 'warn', 'unused-imports/no-unused-imports': 'error', '@typescript-eslint/triple-slash-reference': 'off', + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': ['error'], '@typescript-eslint/no-unused-vars': [ 'warn', { diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..9f1b7fc6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,87 @@ +name: Bug report +description: Create a bug report to help us improve +labels: + - 'bug' + - 'triage' +body: + - type: markdown + attributes: + value: | + Thank you for filling out a report! + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the bug you encountered. + options: + - label: I have searched the existing issues + required: true + - type: textarea + attributes: + label: Description + description: A clear and concise description of what the bug is. + validations: + required: true + - type: textarea + attributes: + label: Steps to reproduce + description: What are the steps to reproduce the bug? + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: true + - type: textarea + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + - type: textarea + attributes: + label: Screenshot + description: If applicable, add screenshots to help explain your problem. + - type: dropdown + id: version + attributes: + label: Where is the problem happening? + options: + - Mobile + - Desktop + - Both + validations: + required: true + - type: textarea + attributes: + label: Environment + description: Please complete the following information + placeholder: | + Device: [e.g. iPhone6] + OS: [e.g. iOS8.1] + Browser [e.g. stock browser, safari] + Version [e.g. 22] + validations: + required: true + - type: input + attributes: + label: What version of `react-celo` were you using? (This can be found in the list of wallets in the "Connect" modal) + validations: + required: true + - type: input + attributes: + label: What wallet were you using? + placeholder: e.g. Node Wallet, MetaMask, Valora, etc + validations: + required: false + - type: input + attributes: + label: What is the dApp URL? (if applicable) + validations: + required: false + - type: textarea + attributes: + label: Additional context + description: Add any other context about the problem here. + validations: + required: false diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 00000000..45291c13 --- /dev/null +++ b/.yarnrc @@ -0,0 +1 @@ +registry "https://registry.npmjs.org/" diff --git a/development.md b/development.md new file mode 100644 index 00000000..3f115476 --- /dev/null +++ b/development.md @@ -0,0 +1,63 @@ +# Development + +## Workflow + +To run all the packages locally at once, simply clone this repository and run: + +```sh +yarn; +yarn build; #only needs to be run the first time +yarn dev; +``` + +A hot reloading server should come up on localhost:3000, it's the exact same as what's at react-celo-canary.vercel.app. + +Alternatively, you can individually run `react-celo` and the `example` app in parallel. + +For that, you still need to have run `yarn` in the root. + +Then, you can run `react-celo` in one tab: + +```sh +cd packages/react-celo +yarn dev +``` + +and run the `example` app in another: + +```sh +cd packages/example +yarn dev +``` + +## Bumping the packages + +All packages under `/packages` are meant to published with the same version. + +To bump the version of all the packages at once, use the `bump-versions` script. +You'll need to specify which semver increase you want to use. + +For example, to bump to a prerelease: + +```sh +yarn bump-versions prerelease --preId alpha +``` + +or for bumping a `major` version (same for `minor` or `patch`): + +```sh +yarn bump-versions major +``` + +## Publishing the packages + +Once the packages are ready to be published, ensure you've checked master and it is updated. Then, you can run the npm following npm script: + +```sh +yarn publish-packages +``` + +Hint: + +- You can use the `--dry-run` flag to check everything would run properly +- If you need to run the script multiple times without changing the code, using the `--skip-build` flag can be useful to reduce the time. diff --git a/jest.config.js b/jest.config.js index cecf5e7d..265edf6f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -13,4 +13,8 @@ module.exports = { coveragePathIgnorePatterns: ['__tests__', 'lib'], coverageReporters: [Boolean(process.env.CI) ? 'clover' : 'html'], verbose: Boolean(process.env.CI), + moduleNameMapper: { + '^preact$': require.resolve('preact'), + '^preact/hooks$': require.resolve('preact/hooks'), + }, }; diff --git a/package.json b/package.json index df3ac2d4..7fe30c93 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,9 @@ "workspaces": [ "packages/*" ], + "pkgs": { + "path": "./packages" + }, "scripts": { "dev": "lerna run dev --stream --parallel", "build": "lerna link && lerna run clean && lerna run build --scope 'example' --include-dependencies", @@ -13,10 +16,13 @@ "test": "lerna link && COVERAGE=ON lerna run --stream --no-bail test", "prepare": "husky install", "reset-modules": "rm -rf node_modules/ packages/*/node_modules", - "publish-packages": "ts-node ./scripts/publish-packages.ts", - "deprecate-version": "ts-node ./scripts/deprecate-version.ts" + "publish-packages-script": "ts-node ./scripts/publish-packages.ts", + "deprecate-version": "ts-node ./scripts/deprecate-version.ts", + "publish-packages": "pkgs publish", + "bump-versions": "pkgs bump-versions" }, "devDependencies": { + "@clabs/packages-publisher": "0.0.1-alpha.3", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.2.0", "@types/jest": "^27.5.1", diff --git a/packages/example/components/buttons.tsx b/packages/example/components/buttons.tsx index 625015a8..ad5f2fe7 100644 --- a/packages/example/components/buttons.tsx +++ b/packages/example/components/buttons.tsx @@ -6,11 +6,9 @@ export function PrimaryButton( return ( ); diff --git a/packages/example/components/test-plan/perform-action.test.tsx b/packages/example/components/test-plan/perform-action.test.tsx new file mode 100644 index 00000000..6f1edff9 --- /dev/null +++ b/packages/example/components/test-plan/perform-action.test.tsx @@ -0,0 +1,95 @@ +import '@testing-library/jest-dom'; + +import { CeloProvider, localStorageKeys, Mainnet } from '@celo/react-celo'; +import { render, waitFor } from '@testing-library/react'; +import { generateTestingUtils } from 'eth-testing'; +import { TestingUtils } from 'eth-testing/lib/testing-utils'; + +import { mockLogger } from '../mock-logger'; +import { SendTransaction } from './perform-actions'; + +declare global { + interface Window { + ethereum: ReturnType & { send?: () => void }; + } +} + +describe('SendTransaction', () => { + const testingUtils = generateTestingUtils({ + providerType: 'MetaMask', + }); + + beforeAll(() => { + // Manually inject the mocked provider in the window as MetaMask does + const provider = testingUtils.getProvider(); + global.window.ethereum = provider; + }); + + beforeEach(() => { + testingUtils.clearAllMocks(); + }); + + it('should show error if wallet has no funds', async () => { + const mockAddress = '0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf'; + testingUtils.mockConnectedWallet([mockAddress], { + chainId: Mainnet.chainId, + }); + + localStorage.setItem(localStorageKeys.lastUsedNetwork, Mainnet.name); + localStorage.setItem(localStorageKeys.lastUsedAddress, mockAddress); + localStorage.setItem(localStorageKeys.lastUsedWalletType, 'MetaMask'); + + /** + * As far as I can tell, the balance is retrieved with an `eth_call` request, + * which is sent using this function. So this might be a good point to investigate + * how to mock it. I left an attempt below but that's not really getting to the + * balance call from contractkit. + * + */ + global.window.ethereum.send = jest.fn().mockResolvedValue({ + jsonrpc: '2.0', + result: + '0x000000000000000000000000471ece3750da237f93b8e339c536989b8978a438', + }); + + testingUtils.lowLevel.mockRequest('eth_call', {}); + const screen = render( + + + + ); + + expect(screen.getByRole('status')).toHaveTextContent('not started'); + + await waitFor(() => { + expect( + screen.getByText( + 'This sends a very small transaction to impact market contract.' + ) + ).toBeVisible(); + }); + + await waitFor(() => + expect( + screen.getByText( + /Your wallet does not have enough funds for the transaction/, + { exact: false } + ) + ).toBeInTheDocument() + ); + + screen.unmount(); + }); +}); diff --git a/packages/example/components/test-plan/perform-actions.tsx b/packages/example/components/test-plan/perform-actions.tsx new file mode 100644 index 00000000..0ae2aff5 --- /dev/null +++ b/packages/example/components/test-plan/perform-actions.tsx @@ -0,0 +1,120 @@ +import { UseCelo, useCelo } from '@celo/react-celo'; +import { useEffect, useState } from 'react'; + +import { sendTestTransaction } from '../../utils/send-test-transaction'; +import { signTest } from '../../utils/sign-test'; +import { signTestTypedData } from '../../utils/sign-test-typed-data'; +import { assertHasBalance } from './assert-has-balance'; +import { SuccessIcon } from './success-icon'; +import { Result, TestBlock } from './ui'; +import { useTestStatus } from './useTestStatus'; + +/** + * PerformActionInWallet is a wrapper component that takes an action + * that will be performed using a wallet. The common logic of all actions + * is that we need to check if the wallet has funds. + */ +export function PerformActionInWallet({ + title, + action, + description, + successMessage, +}: { + title: string; + description: string; + action: (performActions: UseCelo['performActions']) => Promise; + successMessage: string; +}) { + const { performActions, address, kit, feeCurrency } = useCelo(); + const { status, errorMessage, wrapActionWithStatus, setStatus } = + useTestStatus(); + const [disabled, setDisabled] = useState(true); + + const onRunTest = wrapActionWithStatus(async () => { + setDisabled(true); + await action(performActions); + }); + + useEffect(() => { + if (address) { + assertHasBalance(address, kit, feeCurrency) + .then(() => { + setDisabled(false); + setStatus.notStarted(); + }) + .catch((assertError) => { + setDisabled(true); + if (assertError instanceof Error) { + setStatus.failed(assertError.message); + } else { + setStatus.failed( + `Error when checking balance: ${JSON.stringify(assertError)}` + ); + } + }); + } else { + setDisabled(true); + } + }, [address, feeCurrency, kit, setDisabled, setStatus]); + + return ( + <> + + +

{description}

+ +

You'll need to approve it in your wallet.

+
+ + {successMessage} + + {errorMessage} +
+
+ + ); +} + +export const SendTransaction = () => { + const { performActions } = useCelo(); + const action = () => sendTestTransaction(performActions); + return ( + + ); +}; + +export const SignTypedData = () => { + const { performActions } = useCelo(); + const action = () => signTestTypedData(performActions); + return ( + + ); +}; + +export const Sign = () => { + const { performActions } = useCelo(); + const action = () => signTest(performActions); + return ( + + ); +}; diff --git a/packages/example/components/test-plan/switch-networks.tsx b/packages/example/components/test-plan/switch-networks.tsx index 65e10ad6..80b4f3ff 100644 --- a/packages/example/components/test-plan/switch-networks.tsx +++ b/packages/example/components/test-plan/switch-networks.tsx @@ -1,30 +1,55 @@ -import { Alfajores, useCelo } from '@celo/react-celo'; -import { useEffect, useState } from 'react'; +import { MiniContractKit } from '@celo/contractkit/lib/mini-kit'; +import { Alfajores, Mainnet, useCelo } from '@celo/react-celo'; import { SuccessIcon } from './success-icon'; import { Result, TestBlock } from './ui'; import { useDisabledTest } from './useDisabledTest'; -import { Status, useTestStatus } from './useTestStatus'; +import { useTestStatus } from './useTestStatus'; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +// Hacky workaround to wait for the network to change. +const hasNetworkUpdated = async ( + kit: MiniContractKit, + expectedChainId: number +) => { + let attempts = 0; + let isNetworkUpdated = false; + while (!isNetworkUpdated) { + attempts++; + if (attempts >= 3) { + throw new Error('Network did not change'); + } + const chainId = await kit.connection.chainId(); + if (chainId === expectedChainId) { + isNetworkUpdated = true; + return; + } + await sleep(500); + } +}; export function SwitchNetwork() { - const { updateNetwork, network } = useCelo(); + const { updateNetwork, kit } = useCelo(); const { status, errorMessage, wrapActionWithStatus, setStatus } = useTestStatus(); const [disabledTest, setDisabledTest] = useDisabledTest(); - const [connectedNetwork, setConnectedNetwork] = useState(''); const onSwitchNetworks = wrapActionWithStatus(async () => { setDisabledTest(true); await updateNetwork(Alfajores); - }); - useEffect(() => { - setConnectedNetwork(network.name); - if (status === Status.NotStarted && network.name === Alfajores.name) { - setStatus.error('Already set to Alfajores'); - setDisabledTest(true); + try { + await hasNetworkUpdated(kit, Alfajores.chainId); + await updateNetwork(Mainnet); + await hasNetworkUpdated(kit, Mainnet.chainId); + } catch (error) { + if (error instanceof Error) { + setStatus.failed(error.message); + } else { + setStatus.failed('Update network did not succeed'); + } } - }, [network.name, setStatus, status, setDisabledTest]); + }); return ( -

Currently connected to {connectedNetwork}.

+

This test will switch networks (possibly twice).

+

The last switch will be to go back to Mainnet.

- Switched to Alfajores + Switching networks was successful {errorMessage}
diff --git a/packages/example/components/test-plan/ui.tsx b/packages/example/components/test-plan/ui.tsx index a2dc2f86..e7104aaf 100644 --- a/packages/example/components/test-plan/ui.tsx +++ b/packages/example/components/test-plan/ui.tsx @@ -5,8 +5,8 @@ import { ErrorIcon } from './error-icon'; import { Status } from './useTestStatus'; export function TestTag({ type }: { type: Status }) { - const getText = (type: Status) => { - return type.replace('-', ' '); + const getText = (text: Status) => { + return text.replace('-', ' '); }; return ( @@ -29,7 +29,7 @@ export function TestBlock({ onRunTest: () => void; }>) { return ( -
+
@@ -52,7 +52,7 @@ export function TestBlock({ } export const Header: React.FC = (props) => ( -

+

); export const Text: React.FC = (props) => ( @@ -88,7 +88,7 @@ export const Success: React.FC = (props) => { export const ErrorText: React.FC = (props) => { const context = useResultContext(); - return context === Status.Error ? ( + return context === Status.Failed ? (

{props.children}

diff --git a/packages/example/components/test-plan/update-fee-currency.tsx b/packages/example/components/test-plan/update-fee-currency.tsx index 9087c216..73377be5 100644 --- a/packages/example/components/test-plan/update-fee-currency.tsx +++ b/packages/example/components/test-plan/update-fee-currency.tsx @@ -17,7 +17,7 @@ export function UpdateFeeCurrency() { useEffect(() => { if (address && supportsFeeCurrency !== undefined && !supportsFeeCurrency) { setDisabledTest(true); - setStatus.error('Wallet does not support updating fee currency.'); + setStatus.failed('Wallet does not support updating fee currency.'); } }, [address, supportsFeeCurrency, setStatus, setDisabledTest]); diff --git a/packages/example/components/test-plan/useTestStatus.tsx b/packages/example/components/test-plan/useTestStatus.tsx index 9a0c15b9..3426d361 100644 --- a/packages/example/components/test-plan/useTestStatus.tsx +++ b/packages/example/components/test-plan/useTestStatus.tsx @@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from 'react'; export enum Status { NotStarted = 'not-started', Success = 'success', - Error = 'error', + Failed = 'failed', Pending = 'pending', } @@ -26,8 +26,8 @@ export const useTestStatus = () => { success: () => { setStatus(Status.Success); }, - error: (error: unknown) => { - setStatus(Status.Error); + failed: (error: unknown) => { + setStatus(Status.Failed); if (hasMessage(error)) { setErrorMessage(error.message); @@ -60,7 +60,7 @@ export const useTestStatus = () => { await action(); set.success(); } catch (error) { - set.error(error); + set.failed(error); } }; diff --git a/packages/example/components/theme-button.tsx b/packages/example/components/theme-button.tsx index 5f64ac5e..9a231412 100644 --- a/packages/example/components/theme-button.tsx +++ b/packages/example/components/theme-button.tsx @@ -10,6 +10,7 @@ export function ThemeButton({ theme, currentTheme, onClick }: Props) { const selected = currentTheme === theme; return (

-
-
Find it on:
- -
- -
-
Used by:
-
    - {[ - { - name: 'Celo Tracker ', - url: 'https://www.celotracker.com/', - }, - { - name: 'Mobius Money', - url: 'https://mobius.money/', - }, - { - name: 'Impact Market', - url: 'https://mobius.money/', - }, - { - name: 'Add yours to the list...', - url: 'https://github.com/celo-org/react-celo/', - }, - ].map(({ name, url }) => ( -
  • - - {name} - -
  • - ))} -
-
+ {address ? ( + Disconnect + ) : ( + + connect().catch((e) => toast.error((e as Error).message)) + } + > + Use Celo Now + + )} -
-
Try it out
-
- Connect to your wallet of choice and sign something for send a test +
+ Connect to your wallet of choice, then sign and send a test transaction -
- - Example wallet -
-
-

Styling

-

+

+ + Test sendTransaction + + + Test signTypedData + + + Test signPersonal + +
+ + +
+
+

+ Styling +

+

React Celo will go dark when tailwinds tw-dark class is on body or you can provide a theme

-
-

+

Try out some of the pre-made themes below

@@ -344,75 +254,19 @@ export default function Home(): React.ReactElement { key={i} theme={theme} currentTheme={_theme} - onClick={(theme) => { - updateTheme(theme); - selectTheme(theme); + onClick={(newTheme) => { + updateTheme(newTheme); + selectTheme(newTheme); }} /> ))}
-
-
- - {address ? ( - Disconnect - ) : ( - - connect().catch((e) => toast.error((e as Error).message)) - } - > - Connect - - )} -
- -
- - Test sendTransaction - - - Test signTypedData - - - Test signPersonal - -
- +
{address && ( -
+
-
+
Account Summary on {network.name}
@@ -428,7 +282,7 @@ export default function Home(): React.ReactElement {
-
+
Balances
@@ -446,7 +300,7 @@ export default function Home(): React.ReactElement {
-
+
Fee Currency{' '} {supportsFeeCurrency || `not supported on ${walletType}`}
@@ -460,7 +314,7 @@ export default function Home(): React.ReactElement { > {Object.keys(feeTokenMap).map((token) => ( ))} @@ -493,3 +347,83 @@ async function getBalances( }) ); } + +export default function Home(): React.ReactElement { + return ( + + + + ); +} + +function UsedBy() { + return ( +
+

Used by

+
    + {[ + { + name: 'Celo Tracker ', + url: 'https://www.celotracker.com/', + }, + { + name: 'Mobius Money', + url: 'https://mobius.money/', + }, + { + name: 'Impact Market', + url: 'https://www.impactmarket.com/', + }, + { + name: 'Add yours to the list...', + url: 'https://github.com/celo-org/react-celo/', + }, + ].map(({ name, url }) => ( +
  • + + {name} + +
  • + ))} +
+
+ ); +} + +function SelectChain() { + const { network, networks, updateNetwork } = useCelo(); + return ( + + ); +} diff --git a/packages/example/pages/wallet-test-plan.tsx b/packages/example/pages/wallet-test-plan.tsx index 7875e768..6b1ad6d9 100644 --- a/packages/example/pages/wallet-test-plan.tsx +++ b/packages/example/pages/wallet-test-plan.tsx @@ -3,7 +3,11 @@ import React from 'react'; import { ConnectWalletCheck } from '../components/test-plan/connect-wallet'; import DisconnectButton from '../components/test-plan/disconnect-button'; -import { SendTransaction } from '../components/test-plan/send-transaction'; +import { + SendTransaction, + Sign, + SignTypedData, +} from '../components/test-plan/perform-actions'; import { SwitchNetwork } from '../components/test-plan/switch-networks'; import { UpdateFeeCurrency } from '../components/test-plan/update-fee-currency'; @@ -22,8 +26,10 @@ export default function WalletTestPlan(): React.ReactElement { }} >
-
Wallet Test Plan
-
+
+ Wallet Test Plan +
+
A set of steps to help verify how well a given wallet interacts with react-celo.
@@ -37,6 +43,8 @@ export default function WalletTestPlan(): React.ReactElement { + +
); diff --git a/packages/example/pages/wallet.tsx b/packages/example/pages/wallet.tsx index 882b490b..11038f2a 100644 --- a/packages/example/pages/wallet.tsx +++ b/packages/example/pages/wallet.tsx @@ -70,7 +70,7 @@ export default function Wallet(): React.ReactElement { kit.contracts.getStableToken(StableToken.cEUR), ]); - const [summary, celo, cusd, ceur] = await Promise.all([ + const [accountSummary, celo, cusd, ceur] = await Promise.all([ accounts.getAccountSummary(account.address).catch((e) => { console.error(e); return defaultSummary; @@ -81,7 +81,7 @@ export default function Wallet(): React.ReactElement { ]); setSummary({ - ...summary, + ...accountSummary, celo, cusd, ceur, @@ -253,7 +253,7 @@ export default function Wallet(): React.ReactElement { const handleNewRequests = useCallback( ( - error: Error | null, + err: Error | null, payload: | AccountsProposal | SignTransactionProposal @@ -262,7 +262,7 @@ export default function Wallet(): React.ReactElement { | DecryptProposal | ComputeSharedSecretProposal ): void => { - if (error) return setError(error.message); + if (err) return setError(err.message); console.log('call_request', payload); let decodedMessage: string; @@ -367,14 +367,16 @@ export default function Wallet(): React.ReactElement { connector.on( CLIENT_EVENTS.session_request, - (error, payload: SessionProposal) => { - if (error) return setError(error.message); + (requestError, payload: SessionProposal) => { + if (requestError) return setError(requestError.message); setApprovalData({ accept: approveConnection, reject: rejectConnection, meta: { - title: `new connection from dApp ${payload.params[0].peerMeta.name}`, + title: `new connection from dApp ${ + payload?.params[0]?.peerMeta?.name || '' + }`, raw: payload, }, }); @@ -383,18 +385,18 @@ export default function Wallet(): React.ReactElement { connector.on( CLIENT_EVENTS.connect, - (error, payload: Request) => { - if (error) return setError(error.message); + (connectError, payload: Request) => { + if (connectError) return setError(connectError.message); console.log(CLIENT_EVENTS.connect, payload); connector.on( CLIENT_EVENTS.disconnect, - (error, payload: Request) => { - if (error) return setError(error.message); + (disconnectError, disconnectPayload: Request) => { + if (disconnectError) return setError(disconnectError.message); if (!connector) return; - console.log(CLIENT_EVENTS.disconnect, payload); + console.log(CLIENT_EVENTS.disconnect, disconnectPayload); setConnector(null); setApprovalData(null); } @@ -404,24 +406,24 @@ export default function Wallet(): React.ReactElement { connector.on( CLIENT_EVENTS.session_update, - (error, payload: Request) => { - if (error) return setError(error.message); + (updateError, payload: Request) => { + if (updateError) return setError(updateError.message); console.log(CLIENT_EVENTS.session_update, payload); } ); connector.on( CLIENT_EVENTS.wc_sessionRequest, - (error, payload: Request) => { - if (error) return setError(error.message); + (requestError, payload: Request) => { + if (requestError) return setError(requestError.message); console.log(CLIENT_EVENTS.wc_sessionRequest, payload); } ); connector.on( CLIENT_EVENTS.wc_sessionUpdate, - (error, payload: Request) => { - if (error) return setError(error.message); + (updateError, payload: Request) => { + if (updateError) return setError(updateError.message); console.log(CLIENT_EVENTS.wc_sessionUpdate, payload); } @@ -444,7 +446,9 @@ export default function Wallet(): React.ReactElement {
-
react-celo wallet
+
+ react-celo wallet +
{error}
-
+
-
+
Account summary
@@ -496,7 +500,7 @@ export default function Wallet(): React.ReactElement {
-
+
Balances
@@ -524,13 +528,13 @@ export default function Wallet(): React.ReactElement { }} contentLabel="Approve walletconnect request?" > -

+

Approve: {approvalData?.meta.title} ?

             {JSON.stringify(approvalData, null, 2)}
           
-
+
{ + if (kit.connection.defaultAccount) { + return await kit.connection.signTypedData( + kit.connection.defaultAccount, + TYPED_DATA + ); + } else { + throw new Error('No default account'); + } + }); +} diff --git a/packages/example/utils/sign-test.ts b/packages/example/utils/sign-test.ts new file mode 100644 index 00000000..d7d47ef2 --- /dev/null +++ b/packages/example/utils/sign-test.ts @@ -0,0 +1,14 @@ +import { UseCelo } from '@celo/react-celo'; +import { ensureLeading0x } from '@celo/utils/lib/address'; + +export async function signTest(performActions: UseCelo['performActions']) { + await performActions(async (k) => { + if (!k.connection.defaultAccount) { + throw new Error('No default account'); + } + return await k.connection.sign( + ensureLeading0x(Buffer.from('Hello').toString('hex')), + k.connection.defaultAccount + ); + }); +} diff --git a/packages/react-celo/__tests__/connectors/celo-extension-wallet.test.ts b/packages/react-celo/__tests__/connectors/celo-extension-wallet.test.ts new file mode 100644 index 00000000..7c5d78e7 --- /dev/null +++ b/packages/react-celo/__tests__/connectors/celo-extension-wallet.test.ts @@ -0,0 +1,108 @@ +import { CeloContract } from '@celo/contractkit/lib/base'; +import { generateTestingUtils } from 'eth-testing'; + +import { + CeloExtensionWalletConnector, + ConnectorEvents, +} from '../../src/connectors'; +import { Alfajores, Baklava, WalletTypes } from '../../src/constants'; +import { setApplicationLogger } from '../../src/utils/logger'; +import { mockLogger } from '../test-logger'; + +const ACCOUNT = '0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const CEW_CALLBACKS = new Map void>(); + +describe('CeloExtensionWalletConnector', () => { + const testingUtils = generateTestingUtils({ + providerType: 'MetaMask', + verbose: false, + }); + + const provider = testingUtils.getProvider(); + + beforeAll(() => { + // Manually inject the mocked provider in the window as MetaMask does + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + (global.window.celo = { + ...provider, + send: (params, cb) => + cb(null, { + jsonrpc: params.jsonrpc, + id: Number(params.id), + result: [ACCOUNT], + }), + enable: () => Promise.resolve(undefined), + publicConfigStore: { + on: (e: string, cb: (args: { networkVersion: number }) => void) => { + CEW_CALLBACKS.set(e, cb); + }, + }, + }), + testingUtils.mockConnectedWallet([ACCOUNT], { + chainId: Alfajores.chainId, + }); + testingUtils.mockAccounts([ACCOUNT]); + testingUtils.mockRequestAccounts([ACCOUNT]); + setApplicationLogger(mockLogger); + }); + let connector: CeloExtensionWalletConnector; + const onConnect = jest.fn(); + const onDisconnect = jest.fn(); + const onChangeAddress = jest.fn(); + const onChangeNetwork = jest.fn(); + beforeEach(() => { + connector = new CeloExtensionWalletConnector( + Alfajores, + CeloContract.GoldToken + ); + connector.on(ConnectorEvents.CONNECTED, onConnect); + connector.on(ConnectorEvents.DISCONNECTED, onDisconnect); + connector.on(ConnectorEvents.ADDRESS_CHANGED, onChangeAddress); + connector.on(ConnectorEvents.NETWORK_CHANGED, onChangeNetwork); + }); + + afterEach(() => { + // Clear all mocks between tests + testingUtils.clearAllMocks(); + }); + describe('initialise()', () => { + it('emits CONNECTED with network, address, walletType', async () => { + await connector.initialise(); + expect(connector.initialised).toBe(true); + expect(onConnect).toBeCalledWith({ + networkName: Alfajores.name, + address: ACCOUNT, + walletType: WalletTypes.CeloExtensionWallet, + }); + }); + }); + + describe('startNetworkChangeFromApp()', () => { + it('throws since CEW doesnt support that', () => { + expect(() => connector.startNetworkChangeFromApp()).toThrowError(); + }); + }); + + describe('continueNetworkUpdateFromWallet()', () => { + it('emits NETWORK_CHANGED EVENT', () => { + connector.continueNetworkUpdateFromWallet(Baklava); + expect(onChangeNetwork).toBeCalledWith(Baklava.name); + }); + + it('creates a new kit', () => { + const originalKit = connector.kit; + connector.continueNetworkUpdateFromWallet(Baklava); + expect(connector.kit).not.toBe(originalKit); + }); + }); + + describe('close()', () => { + it('emits DISCONNECTED event', () => { + connector.close(); + expect(onDisconnect).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/react-celo/__tests__/connectors/coinbase-wallet.test.ts b/packages/react-celo/__tests__/connectors/coinbase-wallet.test.ts new file mode 100644 index 00000000..67549247 --- /dev/null +++ b/packages/react-celo/__tests__/connectors/coinbase-wallet.test.ts @@ -0,0 +1,122 @@ +import crypto from 'crypto'; +import { generateTestingUtils } from 'eth-testing'; + +import { CoinbaseWalletConnector, ConnectorEvents } from '../../src/connectors'; +import { Alfajores, Baklava } from '../../src/constants'; +import { setApplicationLogger } from '../../src/utils/logger'; +import { mockLogger } from '../test-logger'; + +const ACCOUNT = '0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf'; + +const testingUtils = generateTestingUtils({ + providerType: 'default', + verbose: false, +}); + +// eslint-disable-next-line @typescript-eslint/no-unsafe-return +jest.mock('@coinbase/wallet-sdk', () => ({ + ...jest.requireActual('@coinbase/wallet-sdk'), + CoinbaseWalletSDK: jest.fn(() => ({ + makeWeb3Provider: () => testingUtils.getProvider(), + })), +})); + +describe('CoinbaseWalletConnector', () => { + beforeAll(() => { + // @ts-expect-error global override + global.crypto = crypto; + Object.defineProperty(global.self, 'crypto', { + value: { + getRandomValues: (arr: number[]) => crypto.randomBytes(arr.length), + }, + }); + setApplicationLogger(mockLogger); + testingUtils.mockConnectedWallet([ACCOUNT], { + chainId: `0x${Alfajores.chainId.toString(16)}`, + }); + testingUtils.mockRequestAccounts([ACCOUNT]); + testingUtils.lowLevel.mockRequest('wallet_switchEthereumChain', { + chainId: `0x${Alfajores.chainId.toString(16)}`, + }); + }); + + afterEach(() => { + // Clear all mocks between tests + testingUtils.clearAllMocks(); + }); + + const dapp = { name: 'CB', icon: 'wallet.png' }; + + const onConnect = jest.fn(); + + it('initialises', async () => { + const connector = new CoinbaseWalletConnector(Alfajores, dapp); + connector.on(ConnectorEvents.CONNECTED, onConnect); + await connector.initialise(); + expect(connector.account).toEqual(ACCOUNT); + expect(connector.initialised).toBe(true); + expect(onConnect).toHaveBeenCalledWith({ + address: '0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf', + networkName: 'Alfajores', + walletType: 'CoinbaseWallet', + }); + }); + + describe('when network change', () => { + let connector: CoinbaseWalletConnector; + const onChangeNetwork = jest.fn(); + testingUtils.mockChainId(`0x${Baklava.chainId.toString(16)}`); + + beforeEach(() => { + testingUtils.mockConnectedWallet([ACCOUNT], { + chainId: `0x${Alfajores.chainId.toString(16)}`, + }); + connector = new CoinbaseWalletConnector(Alfajores, dapp); + connector.on(ConnectorEvents.NETWORK_CHANGED, onChangeNetwork); + }); + + describe('continueNetworkUpdateFromWallet()', () => { + it('emits NETWORK_CHANGED EVENT', () => { + connector.continueNetworkUpdateFromWallet(Baklava); + expect(onChangeNetwork).toBeCalledWith(Baklava.name); + }); + + it('creates a new kit', () => { + const originalKit = connector.kit; + connector.continueNetworkUpdateFromWallet(Baklava); + expect(connector.kit).not.toBe(originalKit); + }); + }); + }); + describe('when wallet changes address', () => { + const onAddressChange = jest.fn(); + let connector: CoinbaseWalletConnector; + beforeEach(async () => { + testingUtils.mockConnectedWallet([ACCOUNT], { + chainId: `0x${Alfajores.chainId.toString(16)}`, + }); + connector = new CoinbaseWalletConnector(Alfajores, dapp); + connector.on(ConnectorEvents.ADDRESS_CHANGED, onAddressChange); + // Seems to only work when init is called after the callback is set + await connector.initialise(); + }); + it('emits ADDRESS_CHANGED', () => { + const newAddress = '0x11eed0F399d76Fe419FAf19a80ae7a52DE948D76'; + testingUtils.mockAccountsChanged([newAddress]); + expect(onAddressChange).toBeCalledWith(newAddress); + }); + }); + + describe('close()', () => { + const onDisconnect = jest.fn(); + let connector: CoinbaseWalletConnector; + beforeEach(() => { + connector = new CoinbaseWalletConnector(Alfajores, dapp); + connector.on(ConnectorEvents.DISCONNECTED, onDisconnect); + }); + it('emits DISCONNECTED event', () => { + connector.close(); + expect(onDisconnect).toBeCalled(); + }); + }); +}); diff --git a/packages/react-celo/__tests__/connectors/ledger.test.ts b/packages/react-celo/__tests__/connectors/ledger.test.ts index e8ca024b..7e1dcab7 100644 --- a/packages/react-celo/__tests__/connectors/ledger.test.ts +++ b/packages/react-celo/__tests__/connectors/ledger.test.ts @@ -1,47 +1,88 @@ import { CeloContract } from '@celo/contractkit'; +import { MiniContractKit } from '@celo/contractkit/lib/mini-kit'; +import { WalletBase } from '@celo/wallet-base'; +import { LedgerSigner } from '@celo/wallet-ledger'; -import { Alfajores, localStorageKeys, WalletTypes } from '../../src'; -import { LedgerConnector } from '../../src/connectors'; +import { Alfajores, Baklava, WalletTypes } from '../../src'; +import { ConnectorEvents } from '../../src/connectors/common'; +import LedgerConnector from '../../src/connectors/ledger'; +import { setApplicationLogger } from '../../src/utils/logger'; +import { mockLogger } from '../test-logger'; + +class StubWallet extends WalletBase {} describe('LedgerConnector', () => { let connector: LedgerConnector; - beforeEach(() => { + let walletStub: StubWallet; + const onDisconnect = jest.fn(); + const onConnect = jest.fn(); + const onChangeNetwork = jest.fn(); + let originalKit: MiniContractKit; + + beforeAll(() => setApplicationLogger(mockLogger)); + + beforeEach(async () => { + walletStub = new StubWallet(); connector = new LedgerConnector(Alfajores, 0, CeloContract.GoldToken); + connector.on(ConnectorEvents.DISCONNECTED, onDisconnect); + connector.on(ConnectorEvents.CONNECTED, onConnect); + connector.on(ConnectorEvents.NETWORK_CHANGED, onChangeNetwork); + + jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(connector, 'createWallet') + .mockImplementation(function () { + return walletStub; + }); + + jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(connector, 'getWallet') + .mockImplementation(function () { + return walletStub; + }); + jest.spyOn(walletStub, 'getAccounts'); + await connector.initialise(); + originalKit = connector.kit; }); - it('remembers info in localStorage', () => { - expect(localStorage.getItem(localStorageKeys.lastUsedFeeCurrency)).toEqual( - null - ); + // it.skip( + // 'does not need to support ADDRESS CHANGE since the device cannot do this' + // ); - expect(localStorage.getItem(localStorageKeys.lastUsedWalletType)).toEqual( - WalletTypes.Ledger - ); + describe('initialise', () => { + it('emits CONNECTED with index, network, walletType params', () => { + expect(onConnect).toBeCalledWith({ + networkName: Alfajores.name, + walletType: WalletTypes.Ledger, + index: 0, + }); + }); + it('gets account from wallet', () => { + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(walletStub.getAccounts).toBeCalled(); + }); + }); - expect( - localStorage.getItem(localStorageKeys.lastUsedWalletArguments) - ).toEqual('[0]'); + describe('startNetworkChangeFromApp()', () => { + beforeEach(async () => { + await connector.startNetworkChangeFromApp(Baklava); + }); + it('emits NETWORK_CHANGED EVENT', () => { + expect(onChangeNetwork).toBeCalledWith(Baklava.name); + }); - expect(localStorage.getItem(localStorageKeys.lastUsedNetwork)).toEqual( - 'Alfajores' - ); + it('creates a new kit', () => { + expect(connector.kit).not.toBe(originalKit); + }); }); describe('close()', () => { - it('clears out localStorage', () => { + beforeEach(() => { connector.close(); - - expect( - localStorage.getItem(localStorageKeys.lastUsedFeeCurrency) - ).toEqual(null); - - expect( - localStorage.getItem(localStorageKeys.lastUsedWalletArguments) - ).toEqual(null); - - expect(localStorage.getItem(localStorageKeys.lastUsedNetwork)).toEqual( - null - ); + }); + it('emits DISCONNECTED event', () => { + expect(onDisconnect).toBeCalled(); }); }); describe('updateFeeCurrency', () => { diff --git a/packages/react-celo/__tests__/connectors/metamask.test.ts b/packages/react-celo/__tests__/connectors/metamask.test.ts index 9d199c0f..a9bf3120 100644 --- a/packages/react-celo/__tests__/connectors/metamask.test.ts +++ b/packages/react-celo/__tests__/connectors/metamask.test.ts @@ -1,9 +1,11 @@ import { CeloContract } from '@celo/contractkit/lib/base'; import { generateTestingUtils } from 'eth-testing'; -import { MetaMaskConnector } from '../../src/connectors'; -import { Alfajores, Baklava, localStorageKeys } from '../../src/constants'; -import localStorage from '../../src/utils/localStorage'; +import { WalletTypes } from '../../src'; +import { ConnectorEvents, MetaMaskConnector } from '../../src/connectors'; +import { Alfajores, Baklava } from '../../src/constants'; +import { setApplicationLogger } from '../../src/utils/logger'; +import { mockLogger } from '../test-logger'; const ACCOUNT = '0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf'; @@ -12,52 +14,114 @@ describe('MetaMaskConnector', () => { providerType: 'MetaMask', verbose: false, }); + const onConnect = jest.fn(); beforeAll(() => { // Manually inject the mocked provider in the window as MetaMask does // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore global.window.ethereum = testingUtils.getProvider(); testingUtils.mockNotConnectedWallet(); - testingUtils.mockAccounts([ACCOUNT]); testingUtils.mockRequestAccounts([ACCOUNT]); - testingUtils.lowLevel.mockRequest('wallet_switchEthereumChain', {}); + testingUtils.lowLevel.mockRequest('wallet_switchEthereumChain', { + chainId: `0x${Alfajores.chainId.toString(16)}`, + }); + setApplicationLogger(mockLogger); }); afterEach(() => { // Clear all mocks between tests testingUtils.clearAllMocks(); }); - it('initialises', async () => { - const connector = new MetaMaskConnector(Alfajores, CeloContract.GoldToken); - await connector.initialise(); - expect(connector.account).toEqual(ACCOUNT); - expect(connector.initialised).toBe(true); + describe('initialise()', () => { + beforeEach(() => { + testingUtils.mockConnectedWallet([ACCOUNT], { + chainId: `0x${Alfajores.chainId.toString(16)}`, + }); + }); + + it('is idempotent', async () => { + const connector = new MetaMaskConnector( + Alfajores, + CeloContract.GoldToken + ); + connector.on(ConnectorEvents.CONNECTED, onConnect); + await connector.initialise(); + await connector.initialise(); + expect(onConnect).toBeCalledTimes(1); + }); + + it('initialises', async () => { + const connector = new MetaMaskConnector( + Alfajores, + CeloContract.GoldToken + ); + connector.on(ConnectorEvents.CONNECTED, onConnect); + await connector.initialise(); + + expect(connector.account).toEqual(ACCOUNT); + expect(connector.initialised).toBe(true); + expect(onConnect).toBeCalledWith({ + walletType: WalletTypes.MetaMask, + networkName: Alfajores.name, + address: ACCOUNT, + }); + }); }); describe('when network change', () => { let connector: MetaMaskConnector; + const onChangeNetwork = jest.fn(); + beforeEach(() => { - testingUtils.mockConnectedWallet([ACCOUNT]); + testingUtils.mockConnectedWallet([ACCOUNT], { + chainId: `0x${Alfajores.chainId.toString(16)}`, + }); connector = new MetaMaskConnector(Alfajores, CeloContract.GoldToken); + connector.on(ConnectorEvents.NETWORK_CHANGED, onChangeNetwork); }); - it('sets network in local storage and in connection', async () => { - await connector.updateKitWithNetwork(Baklava); - expect(localStorage.getItem(localStorageKeys.lastUsedNetwork)).toEqual( - Baklava.name - ); - }); - - const callback = jest.fn(); + describe('continueNetworkUpdateFromWallet()', () => { + it('emits NETWORK_CHANGED EVENT', () => { + connector.continueNetworkUpdateFromWallet(Baklava); + expect(onChangeNetwork).toBeCalledWith(Baklava.name); + }); - it('reacts to network being changed from metamask side', async () => { - connector.onNetworkChange(callback); + it('creates a new kit', () => { + const originalKit = connector.kit; + connector.continueNetworkUpdateFromWallet(Baklava); + expect(connector.kit).not.toBe(originalKit); + }); + }); + }); + describe('when wallet changes address', () => { + const onAddressChange = jest.fn(); + let connector: MetaMaskConnector; + beforeEach(async () => { + testingUtils.mockConnectedWallet([ACCOUNT], { + chainId: `0x${Alfajores.chainId.toString(16)}`, + }); + connector = new MetaMaskConnector(Alfajores, CeloContract.GoldToken); + connector.on(ConnectorEvents.ADDRESS_CHANGED, onAddressChange); // Seems to only work when init is called after the callback is set await connector.initialise(); + }); + it('emits ADDRESS_CHANGED', () => { + const newAddress = '0x11eed0F399d76Fe419FAf19a80ae7a52DE948D76'; + testingUtils.mockAccountsChanged([newAddress]); + expect(onAddressChange).toBeCalledWith(newAddress); + }); + }); - testingUtils.mockChainChanged('0x1'); - - expect(callback).toHaveBeenLastCalledWith(1); + describe('close()', () => { + let connector: MetaMaskConnector; + const onDisconnect = jest.fn(); + beforeEach(() => { + connector = new MetaMaskConnector(Alfajores, CeloContract.GoldToken); + connector.on(ConnectorEvents.DISCONNECTED, onDisconnect); + }); + it('emits DISCONNECTED event', () => { + connector.close(); + expect(onDisconnect).toBeCalled(); }); }); }); diff --git a/packages/react-celo/__tests__/connectors/private-key.test.ts b/packages/react-celo/__tests__/connectors/private-key.test.ts index d8960484..f911a833 100644 --- a/packages/react-celo/__tests__/connectors/private-key.test.ts +++ b/packages/react-celo/__tests__/connectors/private-key.test.ts @@ -1,80 +1,82 @@ import { CeloContract } from '@celo/contractkit'; -import { Alfajores, localStorageKeys } from '../../src'; -import { PrivateKeyConnector } from '../../src/connectors'; +import { Alfajores, WalletTypes } from '../../src'; +import { ConnectorEvents } from '../../src/connectors/common'; +import PrivateKeyConnector from '../../src/connectors/private-key'; +import { setApplicationLogger } from '../../src/utils/logger'; +import { mockLogger } from '../test-logger'; const TEST_KEY = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abbdef'; describe('PrivateKeyConnector', () => { let connector: PrivateKeyConnector; + beforeAll(() => setApplicationLogger(mockLogger)); describe('initialise()', () => { - beforeEach(() => { + const onConnect = jest.fn(); + beforeEach(async () => { connector = new PrivateKeyConnector( Alfajores, TEST_KEY, CeloContract.StableTokenEUR ); - }); - it('sets the account', async () => { + connector.on(ConnectorEvents.CONNECTED, onConnect); await connector.initialise(); - + }); + it('sets the account', () => { expect(connector.account).toEqual( '0x6df18c5837718a83581ead5e26bfcdb8a548e409' ); }); - it('sets and uses the fee currency', async () => { - await connector.initialise(); - + it('sets and uses the fee currency', () => { expect(connector.feeCurrency).toEqual(CeloContract.StableTokenEUR); }); - it('sets the private key in locale storage', () => { - expect( - localStorage.getItem(localStorageKeys.lastUsedWalletArguments) - ).toEqual(JSON.stringify([TEST_KEY])); - }); - - it('sets the last network in local storage', () => { - expect(localStorage.getItem(localStorageKeys.lastUsedNetwork)).toEqual( - Alfajores.name - ); + it('emits CONNECTED event with needed params', () => { + expect(onConnect).toBeCalledWith({ + networkName: Alfajores.name, + walletType: WalletTypes.PrivateKey, + address: '0x6df18c5837718a83581ead5e26bfcdb8a548e409', + }); }); }); + // it.skip('does not need to support ADDRESS CHANGE'); + describe('close()', () => { - it('clears out localStorage', async () => { + const onDisconnect = jest.fn(); + beforeEach(() => { connector = new PrivateKeyConnector( Alfajores, TEST_KEY, CeloContract.StableTokenEUR ); - await connector.initialise(); + connector.on(ConnectorEvents.DISCONNECTED, onDisconnect); + }); + it('emits DISCONNECTED event', () => { connector.close(); - - expect( - localStorage.getItem(localStorageKeys.lastUsedFeeCurrency) - ).toEqual(null); - - expect( - localStorage.getItem(localStorageKeys.lastUsedWalletArguments) - ).toEqual(null); - - expect(localStorage.getItem(localStorageKeys.lastUsedNetwork)).toEqual( - null - ); + expect(onDisconnect).toBeCalled(); }); }); describe('updateFeeCurrency', () => { it('sets fee currency and in fact uses it', async () => { - await connector.updateFeeCurrency(CeloContract.StableToken); + connector = new PrivateKeyConnector( + Alfajores, + TEST_KEY, + CeloContract.StableTokenEUR + ); + await connector.initialise(); + expect(connector.kit.connection.defaultFeeCurrency).toEqual( + '0x10c892A6EC43a53E45D0B916B4b7D383B1b78C0F' + ); + await connector.updateFeeCurrency(CeloContract.StableTokenBRL); - expect(connector.feeCurrency).toEqual(CeloContract.StableToken); + expect(connector.feeCurrency).toEqual(CeloContract.StableTokenBRL); expect(connector.kit.connection.defaultFeeCurrency).toEqual( - '0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1' + '0xE4D517785D091D3c54818832dB6094bcc2744545' ); }); }); diff --git a/packages/react-celo/__tests__/connectors/walletconnect.test.ts b/packages/react-celo/__tests__/connectors/walletconnect.test.ts index efe7eb97..df267125 100644 --- a/packages/react-celo/__tests__/connectors/walletconnect.test.ts +++ b/packages/react-celo/__tests__/connectors/walletconnect.test.ts @@ -3,8 +3,11 @@ import { CeloContract } from '@celo/contractkit'; import { WalletConnectWallet } from '@celo/wallet-walletconnect-v1'; import { generateTestingUtils } from 'eth-testing'; -import { Alfajores } from '../../src'; -import { WalletConnectConnector } from '../../src/connectors'; +import { Alfajores, WalletIds } from '../../src'; +import { ConnectorEvents, WalletConnectConnector } from '../../src/connectors'; +import { buildOptions } from '../../src/connectors/wallet-connect'; +import { setApplicationLogger } from '../../src/utils/logger'; +import { mockLogger } from '../test-logger'; const ACCOUNT = '0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf'; @@ -16,11 +19,22 @@ jest.spyOn(wallet, 'init').mockImplementation(async function init() { return Promise.resolve(undefined); }); +jest.spyOn(wallet, 'close').mockImplementation(async function close() { + return Promise.resolve(undefined); +}); + jest.spyOn(wallet, 'getAccounts').mockImplementation(function getAccounts() { return [ACCOUNT]; }); +jest.spyOn(wallet, 'getUri').mockImplementation(function getUri() { + return Promise.resolve( + 'wc:8a5e5bdc-a0e4-4702-ba63-8f1a5655744f@1?bridge=https%3A%2F%2Fbridge.walletconnect.org&key=41791102999c339c844880b23950704cc43aa840f3739e365323cda4dfa89e7a' + ); +}); + describe('WalletConnectConnector', () => { + let connector: WalletConnectConnector; const testingUtils = generateTestingUtils({ providerType: 'WalletConnect', verbose: false, @@ -32,21 +46,30 @@ describe('WalletConnectConnector', () => { testingUtils.mockRequestAccounts([ACCOUNT]); testingUtils.lowLevel.mockRequest('wallet_switchEthereumChain', {}); jest.setTimeout(11_000); + setApplicationLogger(mockLogger); }); afterEach(() => { // Clear all mocks between tests testingUtils.clearAllMocks(); }); - - it('initialises', async () => { - const connector = new WalletConnectConnector( + const onConnect = jest.fn(); + const onInit = jest.fn(); + beforeEach(() => { + connector = new WalletConnectConnector( Alfajores, CeloContract.GoldToken, - { connect: { chainId: Alfajores.chainId } } + buildOptions(Alfajores), + false, + (x) => x, + 1, + WalletIds.Steakwallet ); - jest.spyOn(connector.kit, 'getWallet').mockImplementation(() => wallet); + connector.on(ConnectorEvents.CONNECTED, onConnect); + connector.on(ConnectorEvents.WC_INITIALISED, onInit); + }); + it('initialises', async () => { await connector.initialise(); expect(connector.account).toEqual(ACCOUNT); expect(connector.initialised).toBe(true); @@ -56,5 +79,32 @@ describe('WalletConnectConnector', () => { expect(wallet.getAccounts).toHaveBeenCalled(); // eslint-disable-next-line @typescript-eslint/unbound-method expect(connector.kit.getWallet).toHaveBeenCalled(); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(wallet.getUri).toHaveBeenCalled(); + + expect(onInit).toBeCalled(); + }); + + describe('when a connected wallet changes accounts', () => { + const onAddressChange = jest.fn(); + beforeEach(async () => { + await connector.initialise(); + connector.on(ConnectorEvents.ADDRESS_CHANGED, onAddressChange); + }); + it.skip('emits an ADDRESS_CHANGED event', () => { + expect(onAddressChange).toBeCalledWith('address'); + }); + }); + + describe('close()', () => { + const onDisconnected = jest.fn(); + beforeEach(() => { + connector.on(ConnectorEvents.DISCONNECTED, onDisconnected); + }); + + it('emits DISCONNECTED event', async () => { + await connector.close(); + expect(onDisconnected).toBeCalled(); + }); }); }); diff --git a/packages/react-celo/__tests__/modals/connect.test.tsx b/packages/react-celo/__tests__/modals/connect.test.tsx index fd601e0e..c81a8c02 100644 --- a/packages/react-celo/__tests__/modals/connect.test.tsx +++ b/packages/react-celo/__tests__/modals/connect.test.tsx @@ -1,6 +1,6 @@ import '@testing-library/jest-dom'; -import { RenderResult } from '@testing-library/react'; +import { act, RenderResult } from '@testing-library/react'; import React from 'react'; import { ConnectModal } from '../../src/modals'; @@ -22,15 +22,21 @@ describe('ConnectModal', () => { let dom: RenderResult; describe('style.overlay', () => { beforeEach(() => { - dom = renderComponentInCKProvider( - , - { providerProps: { theme } } - ); + act(() => { + dom = renderComponentInCKProvider( + , + { providerProps: { theme } } + ); + }); + }); + + afterEach(() => { + // dom.unmount(); }); it('applies those styles while keeping original', async () => { const modal = await dom.findByRole('dialog'); diff --git a/packages/react-celo/__tests__/react-celo-provider.test.tsx b/packages/react-celo/__tests__/react-celo-provider.test.tsx index e5575d39..6dfbe90c 100644 --- a/packages/react-celo/__tests__/react-celo-provider.test.tsx +++ b/packages/react-celo/__tests__/react-celo-provider.test.tsx @@ -4,16 +4,29 @@ import { CeloContract } from '@celo/contractkit'; import { act, fireEvent } from '@testing-library/react'; import React from 'react'; -import { Mainnet, SupportedProviders } from '../src/constants'; -import { CeloProviderProps } from '../src/react-celo-provider'; +import { + Alfajores, + Baklava, + Mainnet, + NetworkNames, + SupportedProviders, +} from '../src/constants'; +import CeloProviderProps from '../src/react-celo-provider-props'; +import defaultTheme from '../src/theme/default'; import { Maybe, Network, Theme } from '../src/types'; import { UseCelo, useCelo, useCeloInternal } from '../src/use-celo'; +import { clearPreviousConfig } from '../src/utils/local-storage'; import { renderComponentInCKProvider, renderHookInCKProvider, } from './render-in-provider'; describe('CeloProvider', () => { + beforeAll(() => { + jest.spyOn(console, 'log').mockImplementation(jest.fn()); + jest.spyOn(console, 'warn').mockImplementation(jest.fn()); + jest.spyOn(console, 'error').mockImplementation(jest.fn()); + }); describe('user interface', () => { const ConnectButton = () => { const { connect } = useCelo(); @@ -40,23 +53,29 @@ describe('CeloProvider', () => { const modal = await dom.findByText('Connect a wallet'); // eslint-disable-next-line @typescript-eslint/no-unsafe-call expect(modal).toBeVisible(); + dom.unmount(); }); it('shows default wallets', async () => { const dom = await stepsToOpenModal(); - Object.keys(SupportedProviders).map(async (key) => { - const walletName = { ...SupportedProviders }[ - key - ] as SupportedProviders; + const testPromises = Object.keys(SupportedProviders).map( + async (key) => { + const walletName = { ...SupportedProviders }[ + key + ] as SupportedProviders; - if (walletName === SupportedProviders.Injected) { - return; - } + if (walletName === SupportedProviders.Injected) { + return; + } - const walletEntry = await dom.findByText(walletName); + const walletEntry = await dom.findByText(walletName); - expect(walletEntry).toBeVisible(); - }); + expect(walletEntry).toBeVisible(); + } + ); + + await Promise.all(testPromises); + dom.unmount(); }); }); @@ -77,6 +96,7 @@ describe('CeloProvider', () => { expect(valora).toBeVisible(); expect(ledger).toBe(null); + dom.unmount(); }); }); describe('when hideFromModal option is given true', () => { @@ -96,6 +116,7 @@ describe('CeloProvider', () => { expect(ledger).toBe(null); expect(none).toBeVisible(); + dom.unmount(); }); }); }); @@ -155,10 +176,12 @@ describe('CeloProvider', () => { }); it('updates the Current network', async () => { - const { result, rerender, unmount } = renderUseCelo({ networks }); + const { result, rerender, unmount } = renderUseCelo({ + networks, + defaultNetwork: networks[0].name, + }); - // TODO Need to determine behavior when network is not in networks - expect(result.current.network).toEqual(Mainnet); + expect(result.current.network).toEqual(networks[0]); await act(async () => { await result.current.updateNetwork(networks[1]); @@ -169,6 +192,51 @@ describe('CeloProvider', () => { expect(result.current.network).toEqual(networks[1]); unmount(); }); + + it('still allows old network prop to be used ', () => { + const { result } = renderUseCelo({ + network: Baklava, + }); + + expect(result.current.network).toEqual(Baklava); + }); + + describe('when given defaultNetwork prop that exists in networks', () => { + it('starts with that network', () => { + const { result } = renderUseCelo({ + defaultNetwork: NetworkNames.Alfajores, + }); + + expect(result.current.network).toMatchObject(Alfajores); + }); + }); + describe('when given defaultNetwork prop does not exist in networks', () => { + it('throws an error', () => { + expect(() => { + renderUseCelo({ + defaultNetwork: 'Solana', + }); + }).toThrowError( + `[react-celo] Could not find 'defaultNetwork' (Solana) in 'networks'. 'defaultNetwork' must equal 'network.name' on one of the 'networks' passed to CeloProvider.` + ); + }); + }); + describe('when given defaultNetwork and networks array prop', () => { + it('starts with the network it found', () => { + const customRPCMainnet: Network = { + name: NetworkNames.Mainnet, + chainId: Mainnet.chainId, + rpcUrl: 'https://rpc.ankr.com/celo', + explorer: 'https://celoscan.xyz', + }; + const { result } = renderUseCelo({ + defaultNetwork: NetworkNames.Mainnet, + networks: [Alfajores, Baklava, customRPCMainnet], + }); + + expect(result.current.network).toEqual(customRPCMainnet); + }); + }); }); describe('regarding feeCurrency', () => { @@ -189,6 +257,9 @@ describe('CeloProvider', () => { }); describe('when feeCurrency WhitelistToken passed', () => { + beforeEach(() => { + clearPreviousConfig(); + }); it('sets that as the feeCurrency', () => { const { result } = renderUseCelo({ feeCurrency: CeloContract.StableTokenBRL, @@ -216,29 +287,20 @@ describe('CeloProvider', () => { expect(result.current.network).toEqual(Mainnet); act(() => { - result.current.updateTheme({ - background: '#000', - primary: '#000', - secondary: '#000', - muted: '#000', - error: '#000', - text: '#000', - textSecondary: '#000', - textTertiary: '#000', - }); + result.current.updateTheme(defaultTheme.light); }); rerender(); expect(result.current.theme).toEqual({ - background: '#000', - primary: '#000', - secondary: '#000', - muted: '#000', - error: '#000', - text: '#000', - textSecondary: '#000', - textTertiary: '#000', + background: '#ffffff', + primary: '#6366f1', + secondary: '#eef2ff', + muted: '#e2e8f0', + error: '#ef4444', + text: '#000000', + textSecondary: '#1f2937', + textTertiary: '#64748b', }); }); }); diff --git a/packages/react-celo/__tests__/render-in-provider.tsx b/packages/react-celo/__tests__/render-in-provider.tsx index 9b9ebe10..c012d3a6 100644 --- a/packages/react-celo/__tests__/render-in-provider.tsx +++ b/packages/react-celo/__tests__/render-in-provider.tsx @@ -1,7 +1,9 @@ import { render, renderHook } from '@testing-library/react'; import React, { ReactElement } from 'react'; -import { CeloProvider, CeloProviderProps } from '../src/react-celo-provider'; +import { CeloProvider } from '../src/react-celo-provider'; +import { CeloProviderProps } from '../src/react-celo-provider-props'; +import { mockLogger } from './test-logger'; interface RenderArgs { providerProps: Partial; @@ -15,6 +17,7 @@ const defaultProps: CeloProviderProps = { icon: '', }, children: null, + logger: mockLogger, }; export function renderComponentInCKProvider( diff --git a/packages/react-celo/__tests__/test-logger.ts b/packages/react-celo/__tests__/test-logger.ts new file mode 100644 index 00000000..1ecbe24f --- /dev/null +++ b/packages/react-celo/__tests__/test-logger.ts @@ -0,0 +1,6 @@ +export const mockLogger = { + log: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), +}; diff --git a/packages/react-celo/__tests__/utils/colors.test.ts b/packages/react-celo/__tests__/utils/colors.test.ts new file mode 100644 index 00000000..6fc47751 --- /dev/null +++ b/packages/react-celo/__tests__/utils/colors.test.ts @@ -0,0 +1,109 @@ +import { + Color, + contrast, + hexToRGB, + luminance, + RGBToHex, +} from '../../src/utils/colors'; + +beforeAll(() => { + jest.spyOn(console, 'log').mockImplementation(jest.fn()); + jest.spyOn(console, 'debug').mockImplementation(jest.fn()); + jest.spyOn(console, 'error').mockImplementation(jest.fn()); + jest.spyOn(console, 'warn').mockImplementation(jest.fn()); +}); + +describe('RGBToHex', () => { + it('converts rgb to hex', () => { + expect(RGBToHex('rgb(0, 0, 0)')).toEqual('#000000'); + expect(RGBToHex('rgb(255, 0, 0)')).toEqual('#ff0000'); + }); + it('converts rgba to hex, with a warning, and strip the alpha', () => { + const spy = jest.spyOn(console, 'warn').mockImplementation(jest.fn()); + expect(RGBToHex('rgba(255, 255, 255, 0.5)')).toEqual('#ffffff80'); + expect(spy).toBeCalled(); + }); +}); + +describe('hexToRGB', () => { + it('converts hex to rgb', () => { + expect(hexToRGB('#000000')).toEqual('rgb(0, 0, 0)'); + expect(hexToRGB('#ff0000')).toEqual('rgb(255, 0, 0)'); + }); + it('converts hex to rgba', () => { + expect(hexToRGB('#000000ff')).toEqual('rgba(0, 0, 0, 1)'); + expect(hexToRGB('#ff0000', 0.5)).toEqual('rgba(255, 0, 0, 0.5)'); + }); +}); + +describe('luminance', () => { + it('calculates the luminance of one color', () => { + expect(luminance(new Color('#fff'))).toBe(1); + expect(luminance(new Color('#000'))).toBe(0); + const randomColor = '#' + Math.floor(Math.random() * 0xffffff).toString(16); + expect(luminance(new Color(randomColor))).toBeGreaterThanOrEqual(0); + expect(luminance(new Color(randomColor))).toBeLessThanOrEqual(1); + }); +}); + +describe('contrast', () => { + it('calculates the constrats between two colors', () => { + expect(contrast(new Color('#fff'), new Color('#000'))).toBe(21); + expect(contrast(new Color('#000'), new Color('#000'))).toBe(1); + expect(contrast(new Color('#fff'), new Color('#fff'))).toBe(1); + expect(contrast(new Color('#fff'), new Color('#444'))).toBe(9.74); + }); +}); + +describe('Color', () => { + it('accepts hex', () => { + const color1 = new Color('#000000'); + expect(color1.r).toEqual(0x00); + expect(color1.g).toEqual(0x00); + expect(color1.b).toEqual(0x00); + expect(color1.a).toEqual(null); + const color2 = new Color('#f00'); + expect(color2.r).toEqual(0xff); + expect(color1.g).toEqual(0x00); + expect(color1.b).toEqual(0x00); + expect(color1.a).toEqual(null); + const color3 = new Color('#ffffff80'); + expect(color3.r).toEqual(0xff); + expect(color3.r).toEqual(0xff); + expect(color3.r).toEqual(0xff); + expect(color3.a).toEqual(0.5); + }); + it('accepts rgb(a)', () => { + const color1 = new Color('rgb(0, 0, 0)'); + expect(color1.r).toEqual(0x00); + expect(color1.g).toEqual(0x00); + expect(color1.b).toEqual(0x00); + expect(color1.a).toEqual(null); + const color2 = new Color('rgb(255, 0, 0)'); + expect(color2.r).toEqual(0xff); + expect(color1.g).toEqual(0x00); + expect(color1.b).toEqual(0x00); + expect(color1.a).toEqual(null); + const color3 = new Color('rgb(255, 255, 255, 0.2)'); + expect(color3.r).toEqual(0xff); + expect(color3.r).toEqual(0xff); + expect(color3.r).toEqual(0xff); + expect(color3.a).toEqual(0.2); + }); + it('accepts hsl(a)', () => { + const color1 = new Color('hsl(0, 0, 0)'); + expect(color1.r).toEqual(0x00); + expect(color1.g).toEqual(0x00); + expect(color1.b).toEqual(0x00); + expect(color1.a).toEqual(null); + const color2 = new Color('hsl(359, 94, 62)'); + expect(color2.r).toEqual(0xf9); + expect(color2.g).toEqual(0x43); + expect(color2.b).toEqual(0x46); + const color3 = new Color('hsl(359, 94, 62, 0.3)'); + expect(color3.r).toEqual(0xf9); + expect(color3.g).toEqual(0x43); + expect(color3.b).toEqual(0x46); + expect(color3.a).toEqual(0.3); + }); +}); diff --git a/packages/react-celo/__tests__/utils/connector-stub.ts b/packages/react-celo/__tests__/utils/connector-stub.ts new file mode 100644 index 00000000..042ce7be --- /dev/null +++ b/packages/react-celo/__tests__/utils/connector-stub.ts @@ -0,0 +1,34 @@ +import { CeloContract, CeloTokenContract } from '@celo/contractkit/lib/base'; +import { MiniContractKit, newKit } from '@celo/contractkit/lib/mini-kit'; + +import { + AbstractConnector, + ConnectorEvents, + EventsMap, +} from '../../src/connectors/common'; +import { WalletTypes } from '../../src/constants'; +import { Connector, Network } from '../../src/types'; + +export class ConnectorStub extends AbstractConnector implements Connector { + public initialised = true; + public type = WalletTypes.Unauthenticated; + public kit: MiniContractKit; + public feeCurrency: CeloTokenContract = CeloContract.GoldToken; + + constructor(n: Network) { + super(); + this.kit = newKit(n.rpcUrl); + } + + startNetworkChangeFromApp(_network: Network) { + // no op + } + + initialise = () => Promise.resolve(this); + + testEmit = (event: E, args?: EventsMap[E]) => { + this.emit(event, args); + }; + + close: () => void = () => undefined; +} diff --git a/packages/react-celo/__tests__/utils/fetchWCWallets.test.ts b/packages/react-celo/__tests__/utils/fetch-wallet-connect-wallets.test.ts similarity index 80% rename from packages/react-celo/__tests__/utils/fetchWCWallets.test.ts rename to packages/react-celo/__tests__/utils/fetch-wallet-connect-wallets.test.ts index 93a6d130..90acac7c 100644 --- a/packages/react-celo/__tests__/utils/fetchWCWallets.test.ts +++ b/packages/react-celo/__tests__/utils/fetch-wallet-connect-wallets.test.ts @@ -1,5 +1,5 @@ import { ChainId } from '../../src'; -import fetchWCWallets from '../../src/utils/fetchWCWallets'; +import fetchWCWallets from '../../src/utils/fetch-wallet-connect-wallets'; describe('fetchWCWallets', () => { it('gets only the CELO compatible wallets from the WC registry', async () => { diff --git a/packages/react-celo/__tests__/utils/local-storage.test.ts b/packages/react-celo/__tests__/utils/local-storage.test.ts new file mode 100644 index 00000000..58303a57 --- /dev/null +++ b/packages/react-celo/__tests__/utils/local-storage.test.ts @@ -0,0 +1,42 @@ +import { CeloContract } from '@celo/contractkit'; + +import { localStorageKeys, WalletTypes } from '../../src/constants'; +import { + getTypedStorageKey, + setTypedStorageKey, +} from '../../src/utils/local-storage'; + +describe('TypedLocalStorage', () => { + describe('get/setTypedStorageKey', () => { + describe(localStorageKeys.lastUsedAddress, () => { + it('sets Address', () => { + setTypedStorageKey(localStorageKeys.lastUsedAddress, 'address'); + expect(getTypedStorageKey(localStorageKeys.lastUsedAddress)).toEqual( + 'address' + ); + }); + }); + describe(localStorageKeys.lastUsedWalletType, () => { + it('sets Last used Wallet', () => { + setTypedStorageKey( + localStorageKeys.lastUsedWalletType, + WalletTypes.MetaMask + ); + expect(getTypedStorageKey(localStorageKeys.lastUsedWalletType)).toEqual( + WalletTypes.MetaMask + ); + }); + }); + describe(localStorageKeys.lastUsedFeeCurrency, () => { + it('sets Last used Wallet', () => { + setTypedStorageKey( + localStorageKeys.lastUsedFeeCurrency, + CeloContract.StableTokenBRL + ); + expect( + getTypedStorageKey(localStorageKeys.lastUsedFeeCurrency) + ).toEqual(CeloContract.StableTokenBRL); + }); + }); + }); +}); diff --git a/packages/react-celo/__tests__/utils/logger.test.ts b/packages/react-celo/__tests__/utils/logger.test.ts new file mode 100644 index 00000000..5924293d --- /dev/null +++ b/packages/react-celo/__tests__/utils/logger.test.ts @@ -0,0 +1,106 @@ +import { + getApplicationLogger, + Level, + Logger, + setApplicationLogger, +} from '../../src/utils/logger'; + +describe('set and get applicationLogger', () => { + it('replaces the default applicationLogger', () => { + const fakeLogger = Symbol('logger'); + + expect(getApplicationLogger()).toBeInstanceOf(Logger); + // @ts-expect-error fakeLogger isn't a logger, but this is a test. + expect(() => setApplicationLogger(fakeLogger)).not.toThrowError(); + expect(getApplicationLogger()).toEqual(fakeLogger); + }); +}); + +const spies = { + debug: jest.spyOn(console, 'info'), + log: jest.spyOn(console, 'log'), + warn: jest.spyOn(console, 'warn'), + error: jest.spyOn(console, 'error'), +}; +beforeEach(() => { + Object.values(spies).forEach((spy) => spy.mockReset()); +}); + +describe('default applicationLogger in dev', () => { + const logger = new Logger(); + it('debugs', () => { + expect(() => logger.debug(1)).not.toThrowError(); + expect(spies.debug).toBeCalledWith('[react-celo]', 1); + }); + it('logs', () => { + expect(() => logger.log(1)).not.toThrowError(); + expect(spies.log).toBeCalledWith('[react-celo]', 1); + }); + it('warns', () => { + expect(() => logger.warn(1)).not.toThrowError(); + expect(spies.warn).toBeCalledWith('[react-celo]', 1); + }); + it('errors', () => { + expect(() => logger.error(1)).not.toThrowError(); + expect(spies.error).toBeCalledWith('[react-celo]', 1); + }); +}); + +describe('default applicationLogger in production', () => { + let logger: Logger; + const previousEnv = process.env.NODE_ENV; + + beforeEach(() => { + process.env.NODE_ENV = 'production'; + logger = new Logger(); + }); + afterAll(() => { + process.env.NODE_ENV = previousEnv; + }); + + it('doesnt debug', () => { + expect(() => logger.debug(1)).not.toThrowError(); + expect(spies.debug).not.toBeCalled(); + }); + it('doesnt log', () => { + expect(() => logger.log(1)).not.toThrowError(); + expect(spies.log).not.toBeCalled(); + }); + it('doesnt warn', () => { + expect(() => logger.warn(1)).not.toThrowError(); + expect(spies.warn).not.toBeCalled(); + }); + it('errors', () => { + expect(() => logger.error(1)).not.toThrowError(); + expect(spies.error).toBeCalledWith('[react-celo]', 1); + }); +}); + +describe('custom applicationLogger in production', () => { + let logger: Logger; + const previousEnv = process.env.NODE_ENV; + + beforeEach(() => { + process.env.NODE_ENV = 'production'; + logger = new Logger(Level.Debug); + }); + afterAll(() => { + process.env.NODE_ENV = previousEnv; + }); + it('debugs', () => { + expect(() => logger.debug(1)).not.toThrowError(); + expect(spies.debug).toBeCalledWith('[react-celo]', 1); + }); + it('logs', () => { + expect(() => logger.log(1)).not.toThrowError(); + expect(spies.log).toBeCalledWith('[react-celo]', 1); + }); + it('warns', () => { + expect(() => logger.warn(1)).not.toThrowError(); + expect(spies.warn).toBeCalledWith('[react-celo]', 1); + }); + it('errors', () => { + expect(() => logger.error(1)).not.toThrowError(); + expect(spies.error).toBeCalledWith('[react-celo]', 1); + }); +}); diff --git a/packages/react-celo/__tests__/utils/metamask.test.ts b/packages/react-celo/__tests__/utils/metamask.test.ts index 498968fa..7c580508 100644 --- a/packages/react-celo/__tests__/utils/metamask.test.ts +++ b/packages/react-celo/__tests__/utils/metamask.test.ts @@ -1,7 +1,7 @@ import { MiniContractKit, newKit } from '@celo/contractkit/lib/mini-kit'; import { GoldTokenWrapper } from '@celo/contractkit/lib/wrappers/GoldTokenWrapper'; -import { Alfajores } from '../../src'; +import { Alfajores, Baklava, Mainnet } from '../../src'; import { AddEthereumEventListener, Ethereum, @@ -16,7 +16,7 @@ import { makeNetworkParams, MetamaskRPCErrorCode, StableTokens, - switchToCeloNetwork, + switchToNetwork, tokenToParam, } from '../../src/utils/metamask'; @@ -88,7 +88,7 @@ describe('tokenToParam', () => { name: 'Celo Euro', symbol: 'cEUR', decimals: 18, - image: 'https://celoreserve.org/assets/tokens/cEUR.svg', + image: 'https://reserve.mento.org/assets/tokens/cEUR.svg', }, }); }); @@ -161,18 +161,16 @@ describe('addNetworksToMetamask', () => { await addNetworksToMetamask(jestEthereum); }); }); -describe('switchToCeloNetwork', () => { - it('todo', async () => { - jestEthereumRequest.mockImplementation((...args) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (args[0].method === 'eth_chainId') { - // mock the web.eth.getChainId call - return Promise.resolve(-1); - } - }); - - await switchToCeloNetwork(kit, Alfajores, jestEthereum); - expect(jestEthereumRequest.mock.calls[1]).toEqual([ +describe('switchToNetwork', () => { + it('sends request to switch chain', async () => { + const mockedGetChainIdFunction = jest.fn(); + mockedGetChainIdFunction + .mockReturnValueOnce(Promise.resolve(Mainnet.chainId)) + .mockReturnValueOnce(Promise.resolve(Alfajores.chainId)) + .mockReturnValueOnce(Promise.resolve(Alfajores.chainId)); + + await switchToNetwork(Alfajores, jestEthereum, mockedGetChainIdFunction); + expect(jestEthereumRequest.mock.calls[0]).toEqual([ { method: 'wallet_switchEthereumChain', params: [ @@ -183,48 +181,69 @@ describe('switchToCeloNetwork', () => { }, ]); }); - it('handles known errors in a specific way', async () => { - let calls = 0; - jestEthereumRequest.mockImplementation((...args) => { - calls += 1; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (args[0].method === 'eth_chainId') { - // mock the web.eth.getChainId call - return Promise.resolve(-1); - } - if (calls === 3) throw { code: MetamaskRPCErrorCode.UnknownNetwork }; + describe('when ethereum.chainId already matches', () => { + const mockedGetChainIdFunction = jest.fn(); + mockedGetChainIdFunction.mockReturnValue( + Promise.resolve(Alfajores.chainId) + ); + jestEthereum.chainId = Alfajores.chainId.toString(16); + it('does not request to switch', async () => { + await switchToNetwork(Alfajores, jestEthereum, mockedGetChainIdFunction); + expect(jestEthereumRequest).toBeCalledTimes(0); + }); + }); + + describe('when ethereum.chainId does not match', () => { + const mockedGetChainIdFunction = jest.fn(); + mockedGetChainIdFunction.mockReturnValue( + Promise.resolve(Alfajores.chainId) + ); + it('requests wallet to switch chains', async () => { + jestEthereum.chainId = Baklava.chainId.toString(16); + await switchToNetwork(Alfajores, jestEthereum, mockedGetChainIdFunction); + expect(jestEthereumRequest).toHaveBeenCalledWith({ + method: 'wallet_switchEthereumChain', + params: [ + { + chainId: `0x${Alfajores.chainId.toString(16)}`, + }, + ], + }); }); + }); + + it('handles UnknownNetwork error in a specific way', async () => { + const mockedGetChainIdFunction = jest.fn(); + mockedGetChainIdFunction + .mockReturnValueOnce(Promise.resolve(Mainnet.chainId)) + .mockRejectedValueOnce({ code: MetamaskRPCErrorCode.UnknownNetwork }) + .mockReturnValueOnce(Promise.resolve(Alfajores.chainId)) + .mockReturnValueOnce(Promise.resolve(Alfajores.chainId)); await expect( - switchToCeloNetwork(kit, Alfajores, jestEthereum) + switchToNetwork(Alfajores, jestEthereum, mockedGetChainIdFunction) ).resolves.toBe(undefined); }); - it('handles known errors in a specific way', async () => { - jestEthereumRequest.mockImplementation((...args) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (args[0].method === 'eth_chainId') { - // mock the web.eth.getChainId call - return Promise.resolve(-1); - } - throw { code: MetamaskRPCErrorCode.AwaitingUserConfirmation }; - }); + it('handles AwaitingUserConfirmation error in a specific way', async () => { + const mockedGetChainIdFunction = jest.fn(); + mockedGetChainIdFunction + .mockReturnValueOnce(Promise.resolve(Mainnet.chainId)) + .mockRejectedValueOnce({ + code: MetamaskRPCErrorCode.AwaitingUserConfirmation, + }); await expect( - switchToCeloNetwork(kit, Alfajores, jestEthereum) + switchToNetwork(Alfajores, jestEthereum, mockedGetChainIdFunction) ).resolves.toBe(undefined); }); it('doesnt yet handle unknown errors', async () => { - jestEthereumRequest.mockImplementation((...args) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (args[0].method === 'eth_chainId') { - // mock the web.eth.getChainId call - return Promise.resolve(-1); - } - throw new Error('test-error'); - }); + const mockedGetChainIdFunction = jest.fn(); + mockedGetChainIdFunction + .mockReturnValueOnce(Promise.resolve(Mainnet.chainId)) + .mockRejectedValueOnce(new Error('test-error')); await expect( - switchToCeloNetwork(kit, Alfajores, jestEthereum) + switchToNetwork(Alfajores, jestEthereum, mockedGetChainIdFunction) ).rejects.toThrow('test-error'); }); }); diff --git a/packages/react-celo/__tests__/utils/persistor.test.ts b/packages/react-celo/__tests__/utils/persistor.test.ts new file mode 100644 index 00000000..7d2d2f1c --- /dev/null +++ b/packages/react-celo/__tests__/utils/persistor.test.ts @@ -0,0 +1,82 @@ +import { ConnectorEvents, ConnectorParams } from '../../src/connectors/common'; +import { + Alfajores, + localStorageKeys, + PROVIDERS, + WalletIds, + WalletTypes, +} from '../../src/constants'; +import { getRecent } from '../../src/hooks/use-providers'; +import { getTypedStorageKey } from '../../src/utils/local-storage'; +import { setApplicationLogger } from '../../src/utils/logger'; +import persistor from '../../src/utils/persistor'; +import { mockLogger } from '../test-logger'; +import { ConnectorStub } from './connector-stub'; + +describe('Persistor', () => { + beforeAll(() => setApplicationLogger(mockLogger)); + let connector: ConnectorStub; + beforeEach(() => { + connector = new ConnectorStub(Alfajores); + persistor(connector); + }); + describe(`when connector emits ${ConnectorEvents.ADDRESS_CHANGED}`, () => { + it('dispatches the new address to Reducer', () => { + connector.testEmit(ConnectorEvents.ADDRESS_CHANGED, '0x12312823y471'); + expect(getTypedStorageKey(localStorageKeys.lastUsedAddress)).toEqual( + '0x12312823y471' + ); + }); + }); + describe(`when connector emits ${ConnectorEvents.NETWORK_CHANGED}`, () => { + it('dispatches the new network to Reducer', () => { + connector.testEmit(ConnectorEvents.NETWORK_CHANGED, 'Polygon'); + expect(getTypedStorageKey(localStorageKeys.lastUsedNetwork)).toEqual( + 'Polygon' + ); + }); + }); + describe(`when connector emits ${ConnectorEvents.CONNECTED}`, () => { + const params: ConnectorParams = { + walletType: WalletTypes.WalletConnect, + address: '0x9e81622', + networkName: 'Celo', + index: 2, + walletId: WalletIds.Steakwallet, + }; + beforeEach(() => { + connector.testEmit(ConnectorEvents.CONNECTED, params); + }); + it('stores walletType', () => { + expect(getTypedStorageKey(localStorageKeys.lastUsedWalletType)).toEqual( + WalletTypes.WalletConnect + ); + }); + it('stores networkName', () => { + expect(getTypedStorageKey(localStorageKeys.lastUsedNetwork)).toEqual( + 'Celo' + ); + }); + it('stores address', () => { + expect(getTypedStorageKey(localStorageKeys.lastUsedAddress)).toEqual( + '0x9e81622' + ); + }); + + it('stores index', () => { + expect(getTypedStorageKey(localStorageKeys.lastUsedIndex)).toEqual(2); + }); + + it('remembers walletID so it can be used to find recently used wallet', () => { + expect(getTypedStorageKey(localStorageKeys.lastUsedWalletId)).toEqual( + WalletIds.Steakwallet + ); + expect(getRecent()).toEqual(PROVIDERS.Steakwallet); + }); + }); + describe(`when connector emits ${ConnectorEvents.DISCONNECTED}`, () => { + it('removes data from local storage', () => { + connector.testEmit(ConnectorEvents.DISCONNECTED); + }); + }); +}); diff --git a/packages/react-celo/__tests__/utils/react-celo-reducer.test.ts b/packages/react-celo/__tests__/utils/react-celo-reducer.test.ts index 876a5a60..d0391bd9 100644 --- a/packages/react-celo/__tests__/utils/react-celo-reducer.test.ts +++ b/packages/react-celo/__tests__/utils/react-celo-reducer.test.ts @@ -1,7 +1,7 @@ import { CeloContract } from '@celo/contractkit'; import { UnauthenticatedConnector } from '../../src/connectors'; -import { Alfajores, Baklava, localStorageKeys } from '../../src/constants'; +import { Alfajores, Baklava } from '../../src/constants'; import { celoReactReducer, ReducerState } from '../../src/react-celo-reducer'; const initialState: ReducerState = { @@ -33,19 +33,14 @@ describe('setAddress', () => { it('adds new address', () => { expect(newState).toEqual({ ...initialState, address: 'test-address' }); }); - it('saves the address in localStorage', () => { - expect(localStorage.getItem(localStorageKeys.lastUsedAddress)).toEqual( - 'test-address' - ); - }); }); -describe('destroy', () => { +describe('disconnect', () => { let newState: ReducerState; beforeEach(() => { newState = celoReactReducer( { ...initialState, address: '0x0123456789abcdf' }, - { type: 'destroy', payload: undefined } + { type: 'disconnect', payload: undefined } ); }); it('removes the address from state', () => { @@ -55,9 +50,32 @@ describe('destroy', () => { it('removes the address from localStorage', () => { newState = celoReactReducer( { ...initialState, address: '0x0123456789abcdf' }, - { type: 'destroy', payload: undefined } + { type: 'disconnect', payload: undefined } ); expect(newState.address).toEqual(null); }); }); + +describe('connect', () => { + it('sets the address and network', () => { + const state = celoReactReducer(initialState, { + type: 'connect', + payload: { address: '0x1234567890', networkName: Baklava.name }, + }); + + expect(state).toHaveProperty('address', '0x1234567890'); + expect(state).toHaveProperty('network', Baklava); + }); +}); + +describe('setNetworkByName', () => { + it('sets the address and network', () => { + const state = celoReactReducer(initialState, { + type: 'setNetworkByName', + payload: Baklava.name, + }); + + expect(state).toHaveProperty('network', Baklava); + }); +}); diff --git a/packages/react-celo/__tests__/utils/resurector.test.ts b/packages/react-celo/__tests__/utils/resurector.test.ts new file mode 100644 index 00000000..7f48463e --- /dev/null +++ b/packages/react-celo/__tests__/utils/resurector.test.ts @@ -0,0 +1,83 @@ +import crypto from 'crypto'; + +import { CONNECTOR_TYPES } from '../../src/connectors/connectors-by-name'; +import { + Baklava, + DEFAULT_NETWORKS, + localStorageKeys, + NetworkNames, + WalletTypes, +} from '../../src/constants'; +import { Dapp } from '../../src/types'; +import { + clearPreviousConfig, + setTypedStorageKey, +} from '../../src/utils/local-storage'; +import { setApplicationLogger } from '../../src/utils/logger'; +import { resurrector } from '../../src/utils/resurrector'; +import { mockLogger } from '../test-logger'; + +const PRIVATE_TEST_KEY = + '04f9d516be49bb44346ca040bdd2736d486bca868693c74d51d274ad92f61976'; + +const dapp: Dapp = { + name: 'Rise Wallet', + description: 'Ascend', + url: 'example.com', + icon: '', +}; + +describe('resurrector', () => { + beforeAll(() => { + // @ts-expect-error global override + global.crypto = crypto; + Object.defineProperty(global.self, 'crypto', { + value: { + getRandomValues: (arr: number[]) => crypto.randomBytes(arr.length), + }, + }); + setApplicationLogger(mockLogger); + }); + afterEach(() => { + clearPreviousConfig(); + }); + + describe('when no walletType in Local Storage', () => { + it('returns null', () => { + expect(resurrector(DEFAULT_NETWORKS, dapp)).toEqual(null); + }); + }); + Object.keys(WalletTypes) + .filter((wt) => wt !== WalletTypes.Unauthenticated) + .forEach((wt) => { + describe(`when LocalStorage has ${localStorageKeys.lastUsedWalletType} of ${wt}`, () => { + beforeEach(() => { + setTypedStorageKey( + localStorageKeys.lastUsedNetwork, + NetworkNames.Alfajores + ); + setTypedStorageKey( + localStorageKeys.lastUsedWalletType, + wt as WalletTypes + ); + setTypedStorageKey( + localStorageKeys.lastUsedPrivateKey, + PRIVATE_TEST_KEY + ); + setTypedStorageKey(localStorageKeys.lastUsedIndex, 1); + }); + it('creates the Connector for that type', () => { + const resurrected = resurrector(DEFAULT_NETWORKS, dapp); + expect(resurrected).toBeInstanceOf( + CONNECTOR_TYPES[wt as WalletTypes] + ); + }); + + describe('when network in local Storage cant be found', () => { + it('does not resurrect', () => { + expect(resurrector([Baklava], dapp)).toBe(null); + }); + }); + }); + }); +}); diff --git a/packages/react-celo/__tests__/utils/updater.test.ts b/packages/react-celo/__tests__/utils/updater.test.ts new file mode 100644 index 00000000..d3f97989 --- /dev/null +++ b/packages/react-celo/__tests__/utils/updater.test.ts @@ -0,0 +1,53 @@ +import { Alfajores, WalletTypes } from '../../src'; +import { ConnectorEvents } from '../../src/connectors/common'; +import { setApplicationLogger } from '../../src/utils/logger'; +import { updater } from '../../src/utils/updater'; +import { mockLogger } from '../test-logger'; +import { ConnectorStub } from './connector-stub'; + +describe('Updater', () => { + const dispatchStub = jest.fn(); + let connector: ConnectorStub; + + beforeAll(() => setApplicationLogger(mockLogger)); + beforeEach(() => { + connector = new ConnectorStub(Alfajores); + updater(connector, dispatchStub); + }); + afterEach(() => { + dispatchStub.mockReset(); + }); + describe(`when connector emits ${ConnectorEvents.ADDRESS_CHANGED}`, () => { + it('dispatches the new address to Reducer', () => { + connector.testEmit(ConnectorEvents.ADDRESS_CHANGED, '0x12312823y471'); + expect(dispatchStub).toHaveBeenCalledWith('setAddress', '0x12312823y471'); + }); + }); + describe(`when connector emits ${ConnectorEvents.NETWORK_CHANGED}`, () => { + it('dispatches the new network to Reducer', () => { + connector.testEmit(ConnectorEvents.NETWORK_CHANGED, 'Polygon'); + expect(dispatchStub).toHaveBeenCalledWith('setNetworkByName', 'Polygon'); + }); + }); + describe(`when connector emits ${ConnectorEvents.CONNECTED}`, () => { + const params = { + address: '0x9e81622', + networkName: 'Celo', + index: 2, + privateKey: 'PRIVATE', + walletType: WalletTypes.PrivateKey, + }; + beforeEach(() => { + connector.testEmit(ConnectorEvents.CONNECTED, params); + }); + it('dispatches connect with appropriate params', () => { + expect(dispatchStub).toHaveBeenCalledWith('connect', params); + }); + }); + describe(`when connector emits ${ConnectorEvents.DISCONNECTED}`, () => { + it('dispatches disconnect action', () => { + connector.testEmit(ConnectorEvents.DISCONNECTED); + expect(dispatchStub).toHaveBeenCalledWith('disconnect'); + }); + }); +}); diff --git a/packages/react-celo/package.json b/packages/react-celo/package.json index 6beb62aa..83564b77 100644 --- a/packages/react-celo/package.json +++ b/packages/react-celo/package.json @@ -1,6 +1,6 @@ { "name": "@celo/react-celo", - "version": "4.0.1-dev", + "version": "4.1.0", "private": false, "scripts": { "prebuild": "mkdir -p lib && node ./scripts/json-to-ts.js package.json lib", @@ -24,14 +24,15 @@ "readme": "../../readme.md", "license": "MIT", "dependencies": { - "@celo/utils": "^2.0.0", "@celo/wallet-base": "^2.0.0", "@celo/wallet-ledger": "^2.0.0", "@celo/wallet-local": "^2.0.0", "@celo/wallet-remote": "^2.0.0", - "@celo/wallet-walletconnect-v1": "4.0.1-dev", + "@celo/wallet-walletconnect-v1": "4.1.0", + "@coinbase/wallet-sdk": "^3.2.0", "@ethersproject/providers": "^5.5.2", "@ledgerhq/hw-transport-webusb": "^5.43.0", + "eventemitter3": "^4.0.7", "isomorphic-fetch": "^3.0.0", "qrcode": "^1.5.0", "react-device-detect": "^2.1.2", diff --git a/packages/react-celo/src/components/button.tsx b/packages/react-celo/src/components/button.tsx index 8746794e..c2230360 100644 --- a/packages/react-celo/src/components/button.tsx +++ b/packages/react-celo/src/components/button.tsx @@ -1,7 +1,8 @@ import React from 'react'; +import useTheme from '../hooks/use-theme'; +import { getApplicationLogger } from '../utils/logger'; import cls from '../utils/tailwind'; -import useTheme from '../utils/useTheme'; const styles = cls({ button: ` @@ -28,7 +29,8 @@ export default function Button({ as, className, style, ...props }: Props) { if (process.env.NODE_ENV !== 'production') { if (as !== 'a' && props.href) { - console.warn( + getApplicationLogger().warn( + '[a11y]', "Potential accessibility error. Got an href on an element which isn't an " ); } diff --git a/packages/react-celo/src/components/connector-screen.tsx b/packages/react-celo/src/components/connector-screen.tsx index 3f780af7..36a46974 100644 --- a/packages/react-celo/src/components/connector-screen.tsx +++ b/packages/react-celo/src/components/connector-screen.tsx @@ -1,7 +1,7 @@ import React, { ReactElement } from 'react'; +import useTheme from '../hooks/use-theme'; import cls from '../utils/tailwind'; -import useTheme from '../utils/useTheme'; import Button from './button'; interface Footer { diff --git a/packages/react-celo/src/components/copy.tsx b/packages/react-celo/src/components/copy.tsx index 871af0ad..7e390c39 100644 --- a/packages/react-celo/src/components/copy.tsx +++ b/packages/react-celo/src/components/copy.tsx @@ -1,8 +1,8 @@ import React, { useCallback, useState } from 'react'; +import { useIsMounted } from '../hooks/use-is-mounted'; +import useTheme from '../hooks/use-theme'; import cls from '../utils/tailwind'; -import { useIsMounted } from '../utils/useIsMounted'; -import useTheme from '../utils/useTheme'; const styles = cls({ button: ` @@ -48,37 +48,24 @@ export const CopyText: React.FC = ({ text, payload }: Props) => { }} > {text} - {copied ? ( - - - - ) : ( - - - - )} + + + ); }; diff --git a/packages/react-celo/src/components/icons/celo-dance.tsx b/packages/react-celo/src/components/icons/celo-dance.tsx new file mode 100644 index 00000000..38f08d46 --- /dev/null +++ b/packages/react-celo/src/components/icons/celo-dance.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +const CeloDance: React.FC> = (props) => ( + + + + + + + + + + +); + +export default CeloDance; diff --git a/packages/react-celo/src/components/icons/celo-terminal.tsx b/packages/react-celo/src/components/icons/celo-terminal.tsx new file mode 100644 index 00000000..05ad0482 --- /dev/null +++ b/packages/react-celo/src/components/icons/celo-terminal.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +const CeloTerminal: React.FC> = (props) => ( + + + + + + +); + +export default CeloTerminal; diff --git a/packages/react-celo/src/components/icons/celo.tsx b/packages/react-celo/src/components/icons/celo.tsx new file mode 100644 index 00000000..aa48ae2e --- /dev/null +++ b/packages/react-celo/src/components/icons/celo.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +const Celo: React.FC> = (props) => ( + + + + + +); +export default Celo; diff --git a/packages/react-celo/src/components/icons/chrome-extension-store.tsx b/packages/react-celo/src/components/icons/chrome-extension-store.tsx new file mode 100644 index 00000000..730ef20c --- /dev/null +++ b/packages/react-celo/src/components/icons/chrome-extension-store.tsx @@ -0,0 +1,48 @@ +import React from 'react'; + +const ChromeExtensionStore: React.FC> = ( + props +) => ( + + + + + + + + + + + +); +export default ChromeExtensionStore; diff --git a/packages/react-celo/src/components/icons/coinbase-wallet.tsx b/packages/react-celo/src/components/icons/coinbase-wallet.tsx new file mode 100644 index 00000000..2327ed7f --- /dev/null +++ b/packages/react-celo/src/components/icons/coinbase-wallet.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const COINBASE_WALLET: React.FC> = (props) => ( + + + + +); + +export default COINBASE_WALLET; diff --git a/packages/react-celo/src/components/icons/ethereum.tsx b/packages/react-celo/src/components/icons/ethereum.tsx new file mode 100644 index 00000000..f153828b --- /dev/null +++ b/packages/react-celo/src/components/icons/ethereum.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +const Ethereum: React.FC> = (props) => ( + + + + + + + + + + +); +export default Ethereum; diff --git a/packages/react-celo/src/components/icons/index.tsx b/packages/react-celo/src/components/icons/index.tsx new file mode 100644 index 00000000..cce3656a --- /dev/null +++ b/packages/react-celo/src/components/icons/index.tsx @@ -0,0 +1,12 @@ +export { default as Celo } from './celo'; +export { default as CeloDance } from './celo-dance'; +export { default as CeloTerminal } from './celo-terminal'; +export { default as ChromeExtensionStore } from './chrome-extension-store'; +export { default as CoinbaseWallet } from './coinbase-wallet'; +export { default as Ethereum } from './ethereum'; +export { default as Ledger } from './ledger'; +export { default as MetaMask } from './metamask'; +export { default as PrivateKey } from './private-key'; +export { default as SteakWallet } from './steakwallet'; +export { default as Valora } from './valora'; +export { default as WalletConnect } from './wallet-connect'; diff --git a/packages/react-celo/src/components/icons/ledger.tsx b/packages/react-celo/src/components/icons/ledger.tsx new file mode 100644 index 00000000..c63ad228 --- /dev/null +++ b/packages/react-celo/src/components/icons/ledger.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const Ledger: React.FC> = (props) => ( + + + + + + + + +); +export default Ledger; diff --git a/packages/react-celo/src/components/icons/metamask.tsx b/packages/react-celo/src/components/icons/metamask.tsx new file mode 100644 index 00000000..664e3fe7 --- /dev/null +++ b/packages/react-celo/src/components/icons/metamask.tsx @@ -0,0 +1,66 @@ +import React from 'react'; + +const MetaMask: React.FC> = (props) => ( + + + + + + + + + + + + + + + + +); +export default MetaMask; diff --git a/packages/react-celo/src/components/icons/private-key.tsx b/packages/react-celo/src/components/icons/private-key.tsx new file mode 100644 index 00000000..9040179f --- /dev/null +++ b/packages/react-celo/src/components/icons/private-key.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const PrivateKey: React.FC> = (props) => ( + +); + +export default PrivateKey; diff --git a/packages/react-celo/src/components/icons/steakwallet.tsx b/packages/react-celo/src/components/icons/steakwallet.tsx new file mode 100644 index 00000000..c566c9d3 --- /dev/null +++ b/packages/react-celo/src/components/icons/steakwallet.tsx @@ -0,0 +1,4 @@ +const SteakWallet = + 'https://res.cloudinary.com/helpkit/image/upload/v1637141229/steakwallet_logo_dark_6703a6b026.png'; + +export default SteakWallet; diff --git a/packages/react-celo/src/components/icons/valora.tsx b/packages/react-celo/src/components/icons/valora.tsx new file mode 100644 index 00000000..f9b364c9 --- /dev/null +++ b/packages/react-celo/src/components/icons/valora.tsx @@ -0,0 +1,168 @@ +import React from 'react'; + +const Valora: React.FC> = (props) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); +export default Valora; diff --git a/packages/react-celo/src/components/icons/wallet-connect.tsx b/packages/react-celo/src/components/icons/wallet-connect.tsx new file mode 100644 index 00000000..b692015a --- /dev/null +++ b/packages/react-celo/src/components/icons/wallet-connect.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +const WalletConnect: React.FC> = (props) => ( + + + +); +export default WalletConnect; diff --git a/packages/react-celo/src/components/modal-container.tsx b/packages/react-celo/src/components/modal-container.tsx index cae8f344..808edd84 100644 --- a/packages/react-celo/src/components/modal-container.tsx +++ b/packages/react-celo/src/components/modal-container.tsx @@ -2,9 +2,9 @@ import React from 'react'; import { isMobile } from 'react-device-detect'; import { SupportedProviders } from '../constants'; +import useTheme from '../hooks/use-theme'; import { Maybe } from '../types'; import cls from '../utils/tailwind'; -import useTheme from '../utils/useTheme'; const styles = cls({ container: ``, diff --git a/packages/react-celo/src/components/provider-select.tsx b/packages/react-celo/src/components/provider-select.tsx index 6d62f480..e4965712 100644 --- a/packages/react-celo/src/components/provider-select.tsx +++ b/packages/react-celo/src/components/provider-select.tsx @@ -1,9 +1,9 @@ import React, { useState } from 'react'; import { isMobile } from 'react-device-detect'; +import useTheme from '../hooks/use-theme'; import { Provider } from '../types'; import cls from '../utils/tailwind'; -import useTheme from '../utils/useTheme'; interface Props { provider: Provider; diff --git a/packages/react-celo/src/components/qrcode.tsx b/packages/react-celo/src/components/qrcode.tsx index 44e7a76f..44119f2e 100644 --- a/packages/react-celo/src/components/qrcode.tsx +++ b/packages/react-celo/src/components/qrcode.tsx @@ -8,8 +8,9 @@ import { create, QRCodeErrorCorrectionLevel } from 'qrcode'; import React, { ReactElement, useMemo } from 'react'; import { QRCodeClass } from '../global'; +import useTheme from '../hooks/use-theme'; +import { getApplicationLogger } from '../utils/logger'; import cls from '../utils/tailwind'; -import useTheme from '../utils/useTheme'; // From https://github.com/soldair/node-qrcode#qr-code-capacity const qrCodeCapacity: [QRCodeErrorCorrectionLevel, number][] = [ @@ -143,7 +144,11 @@ type Props = { const PrettyQrCode = ({ size = 200, value }: Props) => { const theme = useTheme(); - const matrix = useMemo(() => generateMatrix(value), [value]); + const matrix = useMemo(() => { + const _matrix = generateMatrix(value); + getApplicationLogger().debug('[PrettyQrCode]', 'Generated matrix'); + return _matrix; + }, [value]); const corners = useMemo(() => matrixToCorners(matrix, size), [size, matrix]); const dots = useMemo(() => matrixToDots(matrix, size), [size, matrix]); diff --git a/packages/react-celo/src/components/spinner.tsx b/packages/react-celo/src/components/spinner.tsx index db68878c..22ae5bfe 100644 --- a/packages/react-celo/src/components/spinner.tsx +++ b/packages/react-celo/src/components/spinner.tsx @@ -1,7 +1,7 @@ import React from 'react'; +import useTheme from '../hooks/use-theme'; import cls from '../utils/tailwind'; -import useTheme from '../utils/useTheme'; const styles = cls({ // see styles.css diff --git a/packages/react-celo/src/components/tray.tsx b/packages/react-celo/src/components/tray.tsx index c43a2992..64973c69 100644 --- a/packages/react-celo/src/components/tray.tsx +++ b/packages/react-celo/src/components/tray.tsx @@ -5,10 +5,10 @@ import { isMobile } from 'react-device-detect'; // @ts-expect-error import { version } from '../../package'; import { Priorities, SupportedProviders } from '../constants'; +import useProviders from '../hooks/use-providers'; +import useTheme from '../hooks/use-theme'; import { Maybe } from '../types'; import cls from '../utils/tailwind'; -import useProviders from '../utils/useProviders'; -import useTheme from '../utils/useTheme'; import { ProviderSelect } from './provider-select'; function priorityToText(priority: Priorities) { @@ -24,6 +24,7 @@ function priorityToText(priority: Priorities) { } const styles = cls({ + spacer: `tw-h-10`, title: ` tw-pb-2 tw-text-md @@ -46,7 +47,6 @@ const styles = cls({ tw-overflow-y-auto tw-overflow-x-hidden tw-overscroll-contain - tw-min-h-full tw-pr-1`, subtitle: ` tw-font-medium @@ -106,7 +106,7 @@ interface Props { } export default function Tray({ - providers, + providers: providersByPriority, title, onClickProvider, selectedProvider, @@ -115,7 +115,7 @@ export default function Tray({ }: Props) { const theme = useTheme(); - const nPriorities = providers.reduce((acc, [prio]) => { + const nPriorities = providersByPriority.reduce((acc, [prio]) => { if (!acc.includes(prio)) acc.push(prio); return acc; }, [] as Priorities[]); @@ -155,7 +155,7 @@ export default function Tray({ return ( <>
-
+
{title} {isMobile && searchElem}
- {!providers.length && ( + {!providersByPriority.length && (
)} - {providers.map(([priority, providers]) => ( -
+ {providersByPriority.map(([priority, providers], i) => ( +
0 ? styles.container : '' + } + > {!isMobile && nPriorities.length !== 1 && ( ); })} + {/* add spacer to ensure there is room below to push up the last wallet from being hidden by ios tool bars etc. this is better than decreasing height as it just adds whitespace instead of taking away screen space*/} + {isMobile && }
))} diff --git a/packages/react-celo/src/connectors/celo-extension-wallet.ts b/packages/react-celo/src/connectors/celo-extension-wallet.ts new file mode 100644 index 00000000..4f5cf913 --- /dev/null +++ b/packages/react-celo/src/connectors/celo-extension-wallet.ts @@ -0,0 +1,91 @@ +import { CeloTokenContract } from '@celo/contractkit/lib/base'; +import { + MiniContractKit, + newKit, + newKitFromWeb3, +} from '@celo/contractkit/lib/mini-kit'; + +import { WalletTypes } from '../constants'; +import { Connector, Network } from '../types'; +import { AbstractConnector, ConnectorEvents, Web3Type } from './common'; + +export default class CeloExtensionWalletConnector + extends AbstractConnector + implements Connector +{ + public initialised = false; + public type = WalletTypes.CeloExtensionWallet; + public kit: MiniContractKit; + + constructor(private network: Network, public feeCurrency: CeloTokenContract) { + super(); + this.kit = newKit(network.rpcUrl); + } + + async initialise(): Promise { + const { default: Web3 } = await import('web3'); + const celo = window.celo; + if (!celo) { + throw new Error('Celo Extension Wallet not installed'); + } + const web3 = new Web3(celo); + await celo.enable(); + ( + web3.currentProvider as unknown as { + publicConfigStore: { + on: ( + event: string, + cb: (args: { networkVersion: number }) => void + ) => void; + }; + } + ).publicConfigStore.on('update', ({ networkVersion }) => { + if (networkVersion !== this.network.chainId) { + this.emit(ConnectorEvents.WALLET_CHAIN_CHANGED, networkVersion); + } + }); + + this.kit = newKitFromWeb3(web3 as unknown as Web3Type); + const [defaultAccount] = await this.kit.connection.web3.eth.getAccounts(); + this.kit.connection.defaultAccount = defaultAccount; + + this.initialised = true; + + this.emit(ConnectorEvents.CONNECTED, { + walletType: WalletTypes.CeloExtensionWallet, + address: defaultAccount, + networkName: this.network.name, + }); + return this; + } + + continueNetworkUpdateFromWallet(network: Network): void { + this.network = network; // must set to prevent loop + const web3 = this.kit.connection.web3; + this.newKit(web3, this.account as string); // kit caches things so it need to be recreated + this.emit(ConnectorEvents.NETWORK_CHANGED, network.name); + } + + startNetworkChangeFromApp() { + throw new Error( + 'Celo Extension wallet does not support changing network from app' + ); + } + + supportsFeeCurrency() { + return false; + } + + private newKit(web3: Web3Type, defaultAccount: string) { + this.kit = newKitFromWeb3(web3 as unknown as Web3Type); + this.kit.connection.defaultAccount = defaultAccount; + } + + close(): void { + try { + this.kit.connection.stop(); + } finally { + this.disconnect(); + } + } +} diff --git a/packages/react-celo/src/connectors/coinbase-wallet.ts b/packages/react-celo/src/connectors/coinbase-wallet.ts new file mode 100644 index 00000000..af3b9578 --- /dev/null +++ b/packages/react-celo/src/connectors/coinbase-wallet.ts @@ -0,0 +1,159 @@ +import { CeloContract, CeloTokenContract } from '@celo/contractkit/lib/base'; +import { + MiniContractKit, + newKit, + newKitFromWeb3, +} from '@celo/contractkit/lib/mini-kit'; +import { + CoinbaseWalletProvider, + CoinbaseWalletSDK, +} from '@coinbase/wallet-sdk'; + +import { WalletTypes } from '../constants'; +import { Ethereum } from '../global'; +import { Connector, Dapp, Network } from '../types'; +import { getApplicationLogger } from '../utils/logger'; +import { switchToNetwork } from '../utils/metamask'; +import { AbstractConnector, ConnectorEvents, Web3Type } from './common'; + +export default class CoinbaseWalletConnector + extends AbstractConnector + implements Connector +{ + public initialised = false; + public type = WalletTypes.CoinbaseWallet; + public kit: MiniContractKit; + public feeCurrency: CeloTokenContract = CeloContract.GoldToken; + + private provider: CoinbaseWalletProvider | null = null; + + constructor(private network: Network, dapp: Pick) { + super(); + this.kit = newKit(network.rpcUrl); + + const sdk = new CoinbaseWalletSDK({ + appName: dapp?.name ?? '', + appLogoUrl: dapp?.icon ?? '', + reloadOnDisconnect: false, + diagnosticLogger: { + log: (e, p) => { + // this fixes the app trying to resurrect the cb connector after the wallet has initiated a disconnection as the sdk then reloads the page + if ( + 'walletlink_sdk_metadata_destroyed' === e && + p?.alreadyDestroyed === false + ) { + this.close(); + } + getApplicationLogger().debug( + '[coinbase-wallet] sdk event', + e, + 'properties', + p + ); + }, + }, + }); + this.provider = sdk.makeWeb3Provider(network.rpcUrl, network.chainId); + } + + async initialise(): Promise { + if (!this.provider) { + throw new Error('Coinbase wallet provider not instantiated'); + } + if (this.initialised) { + return this; + } + const { default: Web3 } = await import('web3'); + const web3 = new Web3(this.provider); + + const [defaultAccount]: string[] = await this.provider.request({ + method: 'eth_requestAccounts', + }); + + this.removeListeners(); + + await switchToNetwork( + this.network, + this.provider as unknown as Ethereum, + () => web3.eth.getChainId() + ); + + this.provider.on('chainChanged', this.onChainChanged); + this.provider.on('accountsChanged', this.onAccountsChanged); + + this.newKit(web3 as unknown as Web3Type, defaultAccount); + this.initialised = true; + + this.emit(ConnectorEvents.CONNECTED, { + walletType: this.type, + networkName: this.network.name, + address: defaultAccount, + }); + + return this; + } + + private onChainChanged = (chainIdHex: string) => { + const chainId = parseInt(chainIdHex, 16); + if (this.network.chainId !== chainId) { + this.emit(ConnectorEvents.WALLET_CHAIN_CHANGED, chainId); + } + }; + + private removeListeners() { + if (this.provider) { + this.provider.removeListener('chainChanged', this.onChainChanged); + this.provider.removeListener('accountsChanged', this.onAccountsChanged); + } + } + + private newKit(web3: Web3Type, defaultAccount: string) { + this.kit = newKitFromWeb3(web3 as unknown as Web3Type); + this.kit.connection.defaultAccount = defaultAccount; + return this.kit; + } + + private onAccountsChanged = (accounts: string[]) => { + if (accounts[0]) { + this.kit.connection.defaultAccount = accounts[0]; + this.emit(ConnectorEvents.ADDRESS_CHANGED, accounts[0]); + } + }; + + supportsFeeCurrency() { + return false; + } + async startNetworkChangeFromApp(network: Network) { + const web3 = this.kit.connection.web3; + await switchToNetwork(network, this.provider! as unknown as Ethereum, () => + web3.eth.getChainId() + ); + this.continueNetworkUpdateFromWallet(network); + } + + // for when the wallet is already on the desired network and the kit / dapp need to catch up. + continueNetworkUpdateFromWallet(network: Network): void { + this.network = network; // must set to prevent loop + const web3 = this.kit.connection.web3; + this.newKit(web3, this.account as string); // kit caches things so it need to be recreated + this.emit(ConnectorEvents.NETWORK_CHANGED, network.name); + } + + close(): void { + this.removeListeners(); + try { + this.kit.connection.stop(); + } catch (e) { + getApplicationLogger().error( + '[methods.close] could not stop a already stopped CeloConnection', + e + ); + } + this.disconnect(); + if (this.provider?.connected) { + // must be called last as it refreshes page which then starts the resurector if disconnect has not been called + void this.provider?.close(); + } + return; + } +} diff --git a/packages/react-celo/src/connectors/common.ts b/packages/react-celo/src/connectors/common.ts new file mode 100644 index 00000000..c8a69162 --- /dev/null +++ b/packages/react-celo/src/connectors/common.ts @@ -0,0 +1,133 @@ +import { CeloContract, CeloTokenContract } from '@celo/contractkit/lib/base'; +import { + MiniContractKit, + newKitFromWeb3, +} from '@celo/contractkit/lib/mini-kit'; +import EventEmitter from 'eventemitter3'; + +import { WalletTypes } from '../constants'; +import { Connector } from '../types'; +import { getApplicationLogger } from '../utils/logger'; + +export type Web3Type = Parameters[0]; + +export class UnsupportedChainIdError extends Error { + public static readonly NAME: string = 'UnsupportedChainIdError'; + constructor(public readonly chainID: number) { + super(`Unsupported chain ID: ${chainID}`); + this.name = UnsupportedChainIdError.NAME; + } +} + +export async function updateFeeCurrency( + this: Connector, + feeContract: CeloTokenContract +): Promise { + if (!this.supportsFeeCurrency()) { + return; + } + this.feeCurrency = feeContract; + const address = + feeContract === CeloContract.GoldToken + ? undefined + : await this.kit.registry.addressFor(feeContract); + + this.kit.connection.defaultFeeCurrency = address; +} + +export enum ConnectorEvents { + 'CONNECTED' = 'CONNECTED', + 'DISCONNECTED' = 'DISCONNECTED', + 'ADDRESS_CHANGED' = 'ADDRESS_CHANGED', + 'NETWORK_CHANGED' = 'NETWORK_CHANGED', + 'NETWORK_CHANGE_FAILED' = 'NETWORK_CHANGE_FAILED', + 'WALLET_CHAIN_CHANGED' = 'WALLET_CHAIN_CHANGED', + 'WC_URI_RECEIVED' = 'WC_URI_RECEIVED', + 'WC_INITIALISED' = 'WC_INITIALISED', + 'WC_ERROR' = 'WC_ERROR', +} + +interface ConnectorParamsCommon { + networkName: string; + walletType: WalletTypes; + address: string; + walletId?: string; + index?: number; +} + +interface PrivateKeyParams extends ConnectorParamsCommon { + walletType: WalletTypes.PrivateKey; + privateKey: string; +} + +interface LedgerParams extends ConnectorParamsCommon { + walletType: WalletTypes.Ledger; + index: number; +} + +interface WalletConnectParams extends ConnectorParamsCommon { + walletType: + | WalletTypes.CeloWallet + | WalletTypes.Valora + | WalletTypes.WalletConnect + | WalletTypes.CeloTerminal + | WalletTypes.CeloDance; + + walletId: string; +} + +export type ConnectorParams = + | ConnectorParamsCommon + | PrivateKeyParams + | LedgerParams + | WalletConnectParams; + +export type EventsMap = { + [ConnectorEvents.ADDRESS_CHANGED]: string; // address/account changed + [ConnectorEvents.NETWORK_CHANGED]: string; // network has been changed, when this is issued post it being reflected on kit and wallet + [ConnectorEvents.NETWORK_CHANGE_FAILED]: unknown; // an attempt to change chain id failed (likely either the wallet or dapp does not support the requested chain) + [ConnectorEvents.WALLET_CHAIN_CHANGED]: number; // wallet changed network, dapp and connector should respond + [ConnectorEvents.CONNECTED]: ConnectorParams; // wallet is now connected + [ConnectorEvents.WC_URI_RECEIVED]: string; // wc uri is available + [ConnectorEvents.WC_INITIALISED]: void; // called when the initialise function is complete. at this point CONNECTED should have already been emited and uri should be available + [ConnectorEvents.WC_ERROR]: Error; // generic errors from wallet connect operations + [ConnectorEvents.DISCONNECTED]: void; // wallet is no longer connected +}; + +export class AbstractConnector { + public kit?: MiniContractKit; + type: WalletTypes | undefined; + protected emitter = new EventEmitter(); + + get account() { + return this.kit?.connection?.defaultAccount; + } + + protected set account(address) { + this.kit!.connection.defaultAccount = address; + } + + on = ( + event: E, + fn: (arg: EventsMap[E]) => void + ) => { + this.emitter.on(event, fn); + }; + + supportsFeeCurrency() { + return false; + } + + protected emit = ( + event: E, + data?: EventsMap[E] + ) => { + getApplicationLogger().debug('[CONNECTOR EMIT]', this.type, event, data); + this.emitter.emit(event, data); + }; + + protected disconnect() { + this.emit(ConnectorEvents.DISCONNECTED); + this.emitter.removeAllListeners(); + } +} diff --git a/packages/react-celo/src/connectors/connectors-by-name.ts b/packages/react-celo/src/connectors/connectors-by-name.ts index 8f7580e2..f44523d1 100644 --- a/packages/react-celo/src/connectors/connectors-by-name.ts +++ b/packages/react-celo/src/connectors/connectors-by-name.ts @@ -1,14 +1,13 @@ import { WalletTypes } from '../constants'; import { Connector, Network } from '../types'; -import { - CeloExtensionWalletConnector, - InjectedConnector, - LedgerConnector, - MetaMaskConnector, - PrivateKeyConnector, - UnauthenticatedConnector, - WalletConnectConnector, -} from './connectors'; +import CeloExtensionWalletConnector from './celo-extension-wallet'; +import CoinbaseWalletConnector from './coinbase-wallet'; +import InjectedConnector from './injected'; +import LedgerConnector from './ledger'; +import MetaMaskConnector from './metamask'; +import PrivateKeyConnector from './private-key'; +import UnauthenticatedConnector from './unauthenticated'; +import WalletConnectConnector from './wallet-connect'; /** * Connectors for each wallet. @@ -29,4 +28,5 @@ export const CONNECTOR_TYPES: { [WalletTypes.CeloDance]: WalletConnectConnector, [WalletTypes.CeloTerminal]: WalletConnectConnector, [WalletTypes.CeloWallet]: WalletConnectConnector, + [WalletTypes.CoinbaseWallet]: CoinbaseWalletConnector, }; diff --git a/packages/react-celo/src/connectors/connectors.ts b/packages/react-celo/src/connectors/connectors.ts deleted file mode 100644 index 1f1e54ad..00000000 --- a/packages/react-celo/src/connectors/connectors.ts +++ /dev/null @@ -1,533 +0,0 @@ -import { ReadOnlyWallet } from '@celo/connect/lib'; -import { CeloContract, CeloTokenContract } from '@celo/contractkit/lib/base'; -import { - MiniContractKit, - newKit, - newKitFromWeb3, -} from '@celo/contractkit/lib/mini-kit'; -import { LocalWallet } from '@celo/wallet-local'; -// Uncomment with WCV2 support -// import { -// WalletConnectWallet, -// WalletConnectWalletOptions, -// } from '@celo/wallet-walletconnect'; -import { - WalletConnectWallet as WalletConnectWalletV1, - WalletConnectWalletOptions as WalletConnectWalletOptionsV1, -} from '@celo/wallet-walletconnect-v1'; -import { BigNumber } from 'bignumber.js'; - -import { localStorageKeys, WalletTypes } from '../constants'; -import { Connector, Maybe, Network } from '../types'; -import { getEthereum, getInjectedEthereum } from '../utils/ethereum'; -import { clearPreviousConfig } from '../utils/helpers'; -import localStorage from '../utils/localStorage'; -import { switchToCeloNetwork } from '../utils/metamask'; - -type Web3Type = Parameters[0]; - -/** - * Connectors are our link between a DApp and the users wallet. Each - * wallet has different semantics and these classes attempt to unify - * them and present a workable API. - */ - -export class UnauthenticatedConnector implements Connector { - public initialised = true; - public type = WalletTypes.Unauthenticated; - public kit: MiniContractKit; - public account: Maybe = null; - public feeCurrency: CeloTokenContract = CeloContract.GoldToken; - constructor(n: Network) { - this.kit = newKit(n.rpcUrl); - } - - persist() { - localStorage.removeItem(localStorageKeys.lastUsedWalletType); - localStorage.removeItem(localStorageKeys.lastUsedWalletArguments); - localStorage.removeItem(localStorageKeys.lastUsedNetwork); - } - - initialise(): this { - this.initialised = true; - this.persist(); - return this; - } - - supportsFeeCurrency() { - return false; - } - - close(): void { - clearPreviousConfig(); - return; - } -} - -export class PrivateKeyConnector implements Connector { - public initialised = true; - public type = WalletTypes.PrivateKey; - public kit: MiniContractKit; - public account: Maybe = null; - - constructor( - private network: Network, - private privateKey: string, - public feeCurrency: CeloTokenContract - ) { - const wallet = new LocalWallet(); - wallet.addAccount(privateKey); - - this.kit = newKit(network.rpcUrl, wallet); - this.kit.connection.defaultAccount = wallet.getAccounts()[0]; - this.account = this.kit.connection.defaultAccount ?? null; - } - - persist() { - persist({ - walletType: WalletTypes.PrivateKey, - network: this.network, - options: [this.privateKey], - }); - } - - async initialise(): Promise { - await this.updateFeeCurrency(this.feeCurrency); - this.initialised = true; - - this.persist(); - - return this; - } - - supportsFeeCurrency() { - return true; - } - - updateFeeCurrency: typeof updateFeeCurrency = updateFeeCurrency.bind(this); - - close(): void { - clearPreviousConfig(); - return; - } -} - -export class LedgerConnector implements Connector { - public initialised = false; - public type = WalletTypes.Ledger; - public kit: MiniContractKit; - public account: Maybe = null; - - constructor( - private network: Network, - private index: number, - public feeCurrency: CeloTokenContract - ) { - localStorage.setItem( - localStorageKeys.lastUsedWalletType, - WalletTypes.Ledger - ); - localStorage.setItem( - localStorageKeys.lastUsedWalletArguments, - JSON.stringify([index]) - ); - localStorage.setItem(localStorageKeys.lastUsedNetwork, network.name); - this.kit = newKit(network.rpcUrl); - } - - persist() { - persist({ - walletType: WalletTypes.Ledger, - network: this.network, - options: [this.index], - }); - } - - async initialise(): Promise { - const { default: TransportUSB } = await import( - '@ledgerhq/hw-transport-webusb' - ); - const { newLedgerWalletWithSetup } = await import('@celo/wallet-ledger'); - const transport = await TransportUSB.create(); - const wallet = await newLedgerWalletWithSetup(transport, [this.index]); - this.kit = newKit(this.network.rpcUrl, wallet); - this.kit.connection.defaultAccount = wallet.getAccounts()[0]; - - this.initialised = true; - this.account = this.kit.connection.defaultAccount ?? null; - - if (this.feeCurrency) { - await this.updateFeeCurrency(this.feeCurrency); - } - - this.persist(); - - return this; - } - - supportsFeeCurrency() { - return true; - } - - updateFeeCurrency: typeof updateFeeCurrency = updateFeeCurrency.bind(this); - - close(): void { - clearPreviousConfig(); - return; - } -} - -export class UnsupportedChainIdError extends Error { - public static readonly NAME: string = 'UnsupportedChainIdError'; - constructor(public readonly chainID: number) { - super(`Unsupported chain ID: ${chainID}`); - this.name = UnsupportedChainIdError.NAME; - } -} - -export class InjectedConnector implements Connector { - public initialised = false; - public type = WalletTypes.Injected; - public kit: MiniContractKit; - public account: Maybe = null; - private onNetworkChangeCallback?: (chainId: number) => void; - private onAddressChangeCallback?: (address: Maybe) => void; - private network: Network; - - constructor( - network: Network, - public feeCurrency: CeloTokenContract, - defaultType: WalletTypes = WalletTypes.Injected - ) { - this.type = defaultType; - this.kit = newKit(network.rpcUrl); - this.network = network; - } - - persist() { - persist({ - walletType: this.type, - network: this.network, - }); - } - - async initialise(): Promise { - const injected = await getInjectedEthereum(); - if (!injected) { - throw new Error('Ethereum wallet not installed'); - } - const { web3, ethereum, isMetaMask } = injected; - - this.type = isMetaMask ? WalletTypes.MetaMask : WalletTypes.Injected; - - const [defaultAccount] = await ethereum.request({ - method: 'eth_requestAccounts', - }); - - ethereum.removeListener('chainChanged', this.onChainChanged); - ethereum.removeListener('accountsChanged', this.onAccountsChanged); - await switchToCeloNetwork(this.kit, this.network, ethereum); - ethereum.on('chainChanged', this.onChainChanged); - ethereum.on('accountsChanged', this.onAccountsChanged); - - this.kit = newKitFromWeb3(web3 as unknown as Web3Type); - - this.kit.connection.defaultAccount = defaultAccount; - this.account = defaultAccount ?? null; - this.initialised = true; - - this.persist(); - - return this; - } - - private onChainChanged = (chainIdHex: string) => { - const chainId = parseInt(chainIdHex, 16); - if (this.onNetworkChangeCallback && this.network.chainId !== chainId) { - this.onNetworkChangeCallback(chainId); - } - }; - - private onAccountsChanged = (accounts: string[]) => { - if (this.onAddressChangeCallback) { - this.kit.connection.defaultAccount = accounts[0]; - this.onAddressChangeCallback(accounts[0] ?? null); - } - }; - - supportsFeeCurrency() { - return false; - } - - async updateKitWithNetwork(network: Network): Promise { - localStorage.setItem(localStorageKeys.lastUsedNetwork, network.name); - this.network = network; - await this.initialise(); - } - - onNetworkChange(callback: (chainId: number) => void): void { - this.onNetworkChangeCallback = callback; - } - - onAddressChange(callback: (address: Maybe) => void): void { - this.onAddressChangeCallback = callback; - } - - close(): void { - clearPreviousConfig(); - const ethereum = getEthereum(); - if (ethereum) { - ethereum.removeListener('chainChanged', this.onChainChanged); - ethereum.removeListener('accountsChanged', this.onAccountsChanged); - } - this.onNetworkChangeCallback = undefined; - this.onAddressChangeCallback = undefined; - return; - } -} - -export class MetaMaskConnector extends InjectedConnector { - constructor(network: Network, feeCurrency: CeloTokenContract) { - super(network, feeCurrency, WalletTypes.MetaMask); - } -} - -export class CeloExtensionWalletConnector implements Connector { - public initialised = false; - public type = WalletTypes.CeloExtensionWallet; - public kit: MiniContractKit; - public account: Maybe = null; - private onNetworkChangeCallback?: (chainId: number) => void; - - constructor(private network: Network, public feeCurrency: CeloTokenContract) { - this.kit = newKit(network.rpcUrl); - } - - persist() { - persist({ - walletType: WalletTypes.CeloExtensionWallet, - network: this.network, - options: [this.feeCurrency], - }); - } - - async initialise(): Promise { - const { default: Web3 } = await import('web3'); - - const celo = window.celo; - if (!celo) { - throw new Error('Celo Extension Wallet not installed'); - } - const web3 = new Web3(celo); - await celo.enable(); - - ( - web3.currentProvider as unknown as { - publicConfigStore: { - on: ( - event: string, - cb: (args: { networkVersion: number }) => void - ) => void; - }; - } - ).publicConfigStore.on('update', ({ networkVersion }) => { - if (this.onNetworkChangeCallback) { - this.onNetworkChangeCallback(networkVersion); - } - }); - - this.kit = newKitFromWeb3(web3 as unknown as Web3Type); - const [defaultAccount] = await this.kit.connection.web3.eth.getAccounts(); - this.kit.connection.defaultAccount = defaultAccount; - this.account = defaultAccount ?? null; - - this.initialised = true; - - this.persist(); - - return this; - } - - supportsFeeCurrency() { - return false; - } - - onNetworkChange(callback: (chainId: number) => void): void { - this.onNetworkChangeCallback = callback; - } - - close(): void { - clearPreviousConfig(); - return; - } -} - -export class WalletConnectConnector implements Connector { - public initialised = false; - public type = WalletTypes.WalletConnect; - public kit: MiniContractKit; - public account: Maybe = null; - - private onUriCallback?: (uri: string) => void; - private onConnectCallback?: (account: string) => void; - private onCloseCallback?: () => void; - - constructor( - readonly network: Network, - public feeCurrency: CeloTokenContract, - // options: WalletConnectWalletOptions | WalletConnectWalletOptionsV1, - readonly options: WalletConnectWalletOptionsV1, - readonly autoOpen = false, - public getDeeplinkUrl?: (uri: string) => string | false, - readonly version?: number, - readonly walletId?: string - ) { - const wallet = new WalletConnectWalletV1(options); - // Uncomment with WCV2 support - // version == 1 - // ? new WalletConnectWalletV1(options as WalletConnectWalletOptionsV1) - // : new WalletConnectWallet(options as WalletConnectWalletOptions); - this.kit = newKit(network.rpcUrl, wallet as ReadOnlyWallet); - this.version = version; - } - - persist() { - persist({ - walletType: WalletTypes.WalletConnect, - walletId: this.walletId, - network: this.network, - options: [this.options], - }); - } - - onUri(callback: (uri: string) => void): void { - this.onUriCallback = callback; - } - - onConnect(callback: (account: string) => void): void { - this.onConnectCallback = callback; - } - - onClose(callback: () => void): void { - this.onCloseCallback = callback; - } - - async initialise(): Promise { - const wallet = this.kit.getWallet() as WalletConnectWalletV1; - - if (this.onCloseCallback) { - // Uncomment with WCV2 support - // wallet.onPairingDeleted = () => this.onCloseCallback?.(); - wallet.onSessionDeleted = () => this.onCloseCallback?.(); - wallet.onWcSessionUpdate = (_error, session) => { - if (session.params[0].chainId == null) { - this.onCloseCallback?.(); - } - }; - wallet.onSessionUpdated = (_error, session) => { - if (session.params[0].chainId == null) { - this.onCloseCallback?.(); - } - }; - } - - if (this.onConnectCallback) { - wallet.onSessionCreated = (error, session) => { - this.onConnectCallback?.(session.params as string); - }; - } - - const uri = await wallet.getUri(); - if (uri && this.onUriCallback) { - this.onUriCallback(uri); - } - - if (uri && this.autoOpen) { - const deepLink = this.getDeeplinkUrl ? this.getDeeplinkUrl(uri) : uri; - if (deepLink) { - location.href = deepLink; - } - } - - await wallet.init(); - const [address] = wallet.getAccounts(); - const defaultAccount = await this.fetchWalletAddressForAccount(address); - this.kit.connection.defaultAccount = defaultAccount; - this.account = defaultAccount ?? null; - - await this.updateFeeCurrency(this.feeCurrency); - this.initialised = true; - - this.persist(); - - return this; - } - - supportsFeeCurrency() { - // If on WC 1 it will not work due to fields being dropped - if (!this.version || this.version === 1) { - return false; - } - // TODO when V2 is used again check based on wallet? - return true; - } - - private async fetchWalletAddressForAccount(address?: string) { - if (!address) { - return undefined; - } - const accounts = await this.kit.contracts.getAccounts(); - const walletAddress = await accounts.getWalletAddress(address); - return new BigNumber(walletAddress).isZero() ? address : walletAddress; - } - - updateFeeCurrency: typeof updateFeeCurrency = updateFeeCurrency.bind(this); - - close(message?: string): Promise { - clearPreviousConfig(); - const wallet = this.kit.getWallet() as WalletConnectWalletV1; - return wallet.close(message); - } -} - -async function updateFeeCurrency( - this: Connector, - feeContract: CeloTokenContract -): Promise { - if (!this.supportsFeeCurrency()) { - return; - } - this.feeCurrency = feeContract; - const address = - feeContract === CeloContract.GoldToken - ? undefined - : await this.kit.registry.addressFor(feeContract); - - this.kit.connection.defaultFeeCurrency = address; -} - -function persist({ - walletType, - walletId, - options = [], - network, -}: { - walletType?: WalletTypes; - walletId?: string; - options?: unknown[]; - network?: Network; -}): void { - if (walletType) { - localStorage.setItem(localStorageKeys.lastUsedWalletType, walletType); - } - if (walletId) { - localStorage.setItem(localStorageKeys.lastUsedWalletId, walletId); - } - if (network) { - localStorage.setItem(localStorageKeys.lastUsedNetwork, network.name); - } - localStorage.setItem( - localStorageKeys.lastUsedWalletArguments, - JSON.stringify(options) - ); -} diff --git a/packages/react-celo/src/connectors/index.ts b/packages/react-celo/src/connectors/index.ts index bcfb3bc7..5bc90afb 100644 --- a/packages/react-celo/src/connectors/index.ts +++ b/packages/react-celo/src/connectors/index.ts @@ -1,2 +1,9 @@ -export * from './connectors'; -export * from './connectors-by-name'; +export { default as CeloExtensionWalletConnector } from './celo-extension-wallet'; +export { default as CoinbaseWalletConnector } from './coinbase-wallet'; +export * from './common'; +export { default as InjectedConnector } from './injected'; +export { default as LedgerConnector } from './ledger'; +export { default as MetaMaskConnector } from './metamask'; +export { default as PrivateKeyConnector } from './private-key'; +export { default as UnauthenticatedConnector } from './unauthenticated'; +export { default as WalletConnectConnector } from './wallet-connect'; diff --git a/packages/react-celo/src/connectors/injected.ts b/packages/react-celo/src/connectors/injected.ts new file mode 100644 index 00000000..d0fa1dd2 --- /dev/null +++ b/packages/react-celo/src/connectors/injected.ts @@ -0,0 +1,136 @@ +import { CeloTokenContract } from '@celo/contractkit/lib/base'; +import { + MiniContractKit, + newKit, + newKitFromWeb3, +} from '@celo/contractkit/lib/mini-kit'; + +import { WalletTypes } from '../constants'; +import { Connector, Network } from '../types'; +import { getEthereum, getInjectedEthereum } from '../utils/ethereum'; +import { getApplicationLogger } from '../utils/logger'; +import { switchToNetwork } from '../utils/metamask'; +import { AbstractConnector, ConnectorEvents, Web3Type } from './common'; + +export default class InjectedConnector + extends AbstractConnector + implements Connector +{ + public initialised = false; + public type = WalletTypes.Injected; + public kit: MiniContractKit; + private network: Network; + + constructor( + network: Network, + public feeCurrency: CeloTokenContract, + defaultType: WalletTypes = WalletTypes.Injected + ) { + super(); + this.type = defaultType; + this.kit = newKit(network.rpcUrl); + this.network = network; + } + + async initialise(): Promise { + if (this.initialised) { + return this; + } + + const injected = await getInjectedEthereum(); + if (!injected) { + throw new Error('Ethereum wallet not installed'); + } + const { web3, ethereum, isMetaMask } = injected; + + this.type = isMetaMask ? WalletTypes.MetaMask : WalletTypes.Injected; + + const [defaultAccount] = await ethereum.request({ + method: 'eth_requestAccounts', + }); + + ethereum.removeListener('chainChanged', this.onChainChanged); + ethereum.removeListener('accountsChanged', this.onAccountsChanged); + await switchToNetwork(this.network, ethereum, () => + this.kit.connection.chainId() + ); + ethereum.on('chainChanged', this.onChainChanged); + ethereum.on('accountsChanged', this.onAccountsChanged); + + this.newKit(web3 as unknown as Web3Type, defaultAccount); + + this.initialised = true; + + this.emit(ConnectorEvents.CONNECTED, { + walletType: this.type, + address: defaultAccount, + networkName: this.network.name, + }); + + return this; + } + + private newKit(web3: Web3Type, defaultAccount: string) { + this.kit = newKitFromWeb3(web3 as unknown as Web3Type); + this.kit.connection.defaultAccount = defaultAccount; + } + + async startNetworkChangeFromApp(network: Network) { + const ethereum = getEthereum(); + await switchToNetwork(network, ethereum!, this.kit.connection.chainId); + this.continueNetworkUpdateFromWallet(network); + } + + // + continueNetworkUpdateFromWallet(network: Network): void { + this.network = network; // must set to prevent loop + const web3 = this.kit.connection.web3; + this.newKit(web3, this.account as string); // kit caches things so it need to be recreated + this.emit(ConnectorEvents.NETWORK_CHANGED, network.name); + } + + // wallet changes net chain + // emits 'chainChanged' + // onChainChanged called and emits WALLET_CHAIN_CHANGED if chain ids dont match + // networkWatcher sees that and if a suitable network can be found calls continueNetworkUpdateFromWallet() + // else it dies. + private onChainChanged = (chainIdHex: string) => { + const chainId = parseInt(chainIdHex, 16); + // if this change was initiated by app the chainIds will already match and we can abort + getApplicationLogger().log('onChainChanged', chainId); + if (this.network.chainId !== chainId) { + this.emit(ConnectorEvents.WALLET_CHAIN_CHANGED, chainId); + } + }; + + private onAccountsChanged = (accounts: string[]) => { + if (accounts.length === 0) { + // wallet is locked properly close the connection. + this.close(); + } else { + this.kit.connection.defaultAccount = accounts[0]; + this.emit(ConnectorEvents.ADDRESS_CHANGED, accounts[0]); + } + }; + + supportsFeeCurrency() { + return false; + } + + private removeListenersFromEth() { + const ethereum = getEthereum(); + if (ethereum) { + ethereum.removeListener('chainChanged', this.onChainChanged); + ethereum.removeListener('accountsChanged', this.onAccountsChanged); + } + } + + close(): void { + this.removeListenersFromEth(); + try { + this.kit.connection.stop(); + } finally { + this.disconnect(); + } + } +} diff --git a/packages/react-celo/src/connectors/ledger.ts b/packages/react-celo/src/connectors/ledger.ts new file mode 100644 index 00000000..e977dcca --- /dev/null +++ b/packages/react-celo/src/connectors/ledger.ts @@ -0,0 +1,86 @@ +import { CeloTokenContract } from '@celo/contractkit/lib/base'; +import { MiniContractKit, newKit } from '@celo/contractkit/lib/mini-kit'; +import { LedgerWallet, newLedgerWalletWithSetup } from '@celo/wallet-ledger'; +import TransportWebUSB from '@ledgerhq/hw-transport-webusb'; + +import { WalletTypes } from '../constants'; +import { Connector, Network } from '../types'; +import { + AbstractConnector, + ConnectorEvents, + updateFeeCurrency, +} from './common'; + +export default class LedgerConnector + extends AbstractConnector + implements Connector +{ + public initialised = false; + public type = WalletTypes.Ledger; + public kit: MiniContractKit; + private wallet: LedgerWallet | undefined; + constructor( + private network: Network, + private index: number, + public feeCurrency: CeloTokenContract + ) { + super(); + this.kit = newKit(network.rpcUrl); + } + + private getWallet() { + return this.wallet; + } + + private async createWallet(index: number) { + const transport = await TransportWebUSB.create(); + this.wallet = await newLedgerWalletWithSetup(transport, [index]); + return this.wallet; + } + + private async createKit(wallet: LedgerWallet, network: Network) { + this.kit = newKit(network.rpcUrl, wallet); + this.kit.connection.defaultAccount = wallet.getAccounts()[0]; + if (this.feeCurrency) { + await this.updateFeeCurrency(this.feeCurrency); + } + } + + async initialise(): Promise { + if (this.initialised) { + return this; + } + + const wallet = await this.createWallet(this.index); + await this.createKit(wallet, this.network); + + this.initialised = true; + + this.emit(ConnectorEvents.CONNECTED, { + walletType: this.type, + address: this.kit.connection.defaultAccount as string, + index: this.index, + networkName: this.network.name, + }); + return this; + } + + supportsFeeCurrency() { + return true; + } + + async startNetworkChangeFromApp(network: Network) { + await this.createKit(this.getWallet() as LedgerWallet, network); + this.emit(ConnectorEvents.NETWORK_CHANGED, network.name); + } + + updateFeeCurrency: typeof updateFeeCurrency = updateFeeCurrency.bind(this); + + close(): void { + try { + this.kit.connection.stop(); + } finally { + this.disconnect(); + } + } +} diff --git a/packages/react-celo/src/connectors/metamask.ts b/packages/react-celo/src/connectors/metamask.ts new file mode 100644 index 00000000..0baf3475 --- /dev/null +++ b/packages/react-celo/src/connectors/metamask.ts @@ -0,0 +1,11 @@ +import { CeloTokenContract } from '@celo/contractkit/lib/base'; + +import { WalletTypes } from '../constants'; +import { Network } from '../types'; +import InjectedConnector from './injected'; + +export default class MetaMaskConnector extends InjectedConnector { + constructor(network: Network, feeCurrency: CeloTokenContract) { + super(network, feeCurrency, WalletTypes.MetaMask); + } +} diff --git a/packages/react-celo/src/connectors/private-key.ts b/packages/react-celo/src/connectors/private-key.ts new file mode 100644 index 00000000..4174874e --- /dev/null +++ b/packages/react-celo/src/connectors/private-key.ts @@ -0,0 +1,74 @@ +import { CeloTokenContract } from '@celo/contractkit/lib/base'; +import { MiniContractKit, newKit } from '@celo/contractkit/lib/mini-kit'; +import { LocalWallet } from '@celo/wallet-local'; + +import { localStorageKeys, WalletTypes } from '../constants'; +import { Connector, Network } from '../types'; +import { setTypedStorageKey } from '../utils/local-storage'; +import { + AbstractConnector, + ConnectorEvents, + updateFeeCurrency, +} from './common'; + +export default class PrivateKeyConnector + extends AbstractConnector + implements Connector +{ + public initialised = false; + public type = WalletTypes.PrivateKey; + public kit: MiniContractKit; + private wallet: LocalWallet; + constructor( + private network: Network, + privateKey: string, + public feeCurrency: CeloTokenContract + ) { + super(); + this.wallet = new LocalWallet(); + this.wallet.addAccount(privateKey); + this.kit = this.newKit(network); + setTypedStorageKey(localStorageKeys.lastUsedPrivateKey, privateKey); + } + + async initialise(): Promise { + if (this.initialised) return this; + + await this.updateFeeCurrency(this.feeCurrency); + this.initialised = true; + + this.emit(ConnectorEvents.CONNECTED, { + networkName: this.network.name, + walletType: this.type, + address: this.kit.connection.defaultAccount as string, + }); + return this; + } + + async startNetworkChangeFromApp(network: Network) { + this.kit = this.newKit(network); + await this.updateFeeCurrency(this.feeCurrency); // new kit so we must set the feeCurrency again + this.emit(ConnectorEvents.NETWORK_CHANGED, network.name); + } + + private newKit(network: Network) { + const kit = newKit(network.rpcUrl, this.wallet); + kit.connection.defaultAccount = this.wallet.getAccounts()[0]; + return kit; + } + + supportsFeeCurrency() { + return true; + } + + updateFeeCurrency: typeof updateFeeCurrency = updateFeeCurrency.bind(this); + + close(): void { + setTypedStorageKey(localStorageKeys.lastUsedPrivateKey, ''); + try { + this.kit.connection.stop(); + } finally { + this.disconnect(); + } + } +} diff --git a/packages/react-celo/src/connectors/unauthenticated.ts b/packages/react-celo/src/connectors/unauthenticated.ts new file mode 100644 index 00000000..79b2e1ee --- /dev/null +++ b/packages/react-celo/src/connectors/unauthenticated.ts @@ -0,0 +1,48 @@ +import { CeloContract, CeloTokenContract } from '@celo/contractkit/lib/base'; +import { MiniContractKit, newKit } from '@celo/contractkit/lib/mini-kit'; + +import { WalletTypes } from '../constants'; +import { Connector, Network } from '../types'; +import { AbstractConnector, ConnectorEvents } from './common'; + +/** + * Connectors are our link between a DApp and the users wallet. Each + * wallet has different semantics and these classes attempt to unify + * them and present a workable API. + */ + +export default class UnauthenticatedConnector + extends AbstractConnector + implements Connector +{ + public initialised = true; + public type = WalletTypes.Unauthenticated; + public kit: MiniContractKit; + public feeCurrency: CeloTokenContract = CeloContract.GoldToken; + constructor(n: Network) { + super(); + this.kit = newKit(n.rpcUrl); + } + + initialise(): this { + this.initialised = true; + return this; + } + + supportsFeeCurrency() { + return false; + } + + startNetworkChangeFromApp(network: Network) { + this.kit = newKit(network.rpcUrl); + this.emit(ConnectorEvents.NETWORK_CHANGED, network.name); + } + + close(): void { + try { + this.kit.connection.stop(); + } finally { + this.disconnect(); + } + } +} diff --git a/packages/react-celo/src/connectors/wallet-connect.ts b/packages/react-celo/src/connectors/wallet-connect.ts new file mode 100644 index 00000000..f1368051 --- /dev/null +++ b/packages/react-celo/src/connectors/wallet-connect.ts @@ -0,0 +1,276 @@ +import { CeloTokenContract } from '@celo/contractkit/lib/base'; +import { MiniContractKit, newKit } from '@celo/contractkit/lib/mini-kit'; +import { + EthProposal, + SessionConnect, + SessionDisconnect, + SessionProposal, + SessionUpdate, + WalletConnectWallet as WalletConnectWalletV1, + WalletConnectWalletOptions as WalletConnectWalletOptionsV1, +} from '@celo/wallet-walletconnect-v1'; +import { BigNumber } from 'bignumber.js'; + +import { WalletTypes } from '../constants'; +import { Connector, Network } from '../types'; +import { getApplicationLogger } from '../utils/logger'; +import { + AbstractConnector, + ConnectorEvents, + updateFeeCurrency, +} from './common'; + +export function buildOptions(network: Network): WalletConnectWalletOptionsV1 { + return { + connect: { + chainId: network.chainId, + }, + }; +} + +export default class WalletConnectConnector + extends AbstractConnector + implements Connector +{ + public initialised = false; + public type: WalletTypes.WalletConnect = WalletTypes.WalletConnect; + public kit: MiniContractKit; + + constructor( + private network: Network, + public feeCurrency: CeloTokenContract, + // options: WalletConnectWalletOptions | WalletConnectWalletOptionsV1, + readonly options: WalletConnectWalletOptionsV1, + readonly autoOpen = false, + public getDeeplinkUrl?: (uri: string) => string | false, + readonly version?: number, + readonly walletId?: string + ) { + super(); + const wallet = new WalletConnectWalletV1(options); + + this.kit = newKit(network.rpcUrl, wallet); + } + + // this is called automatically and is what gives us the uri for the qr code to be scanned + async initialise(): Promise { + const wallet = this.kit.getWallet() as WalletConnectWalletV1; + + wallet.onSessionCreated = this.onSessionCreated.bind(this); + + wallet.onSessionUpdated = this.onSessionUpdated.bind(this); + + wallet.onCallRequest = this.onCallRequest.bind(this); + + wallet.onWcSessionUpdate = this.onWcSessionUpdate.bind(this); + + wallet.onSessionDeleted = this.onSessionDeleted.bind(this); + + // must be called after all callbacks are set + await wallet.setupClient(); + + await this.handleUri(wallet); + + await wallet.init(); + + const [address] = wallet.getAccounts(); + const defaultAccount = await this.fetchWalletAddressForAccount(address); + + this.kit.connection.defaultAccount = defaultAccount; + + this.initialised = true; + this.emit(ConnectorEvents.WC_INITIALISED); + return this; + } + + private async handleUri(wallet: WalletConnectWalletV1) { + const uri = await wallet.getUri(); + if (uri) { + this.emit(ConnectorEvents.WC_URI_RECEIVED, uri); + } + + if (uri && this.autoOpen) { + const deepLink = this.getDeeplinkUrl ? this.getDeeplinkUrl(uri) : uri; + if (deepLink) { + location.href = deepLink; + } + } + } + + async startNetworkChangeFromApp(network: Network) { + try { + const wallet = this.kit.getWallet() as WalletConnectWalletV1; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const resp = await wallet.switchToChain({ + ...network, + networkId: network.chainId, + }); + getApplicationLogger().debug( + '[startNetworkChangeFromApp] response', + resp + ); + this.restartKit(network); + this.emit(ConnectorEvents.NETWORK_CHANGED, network.name); + } catch (e) { + this.emit(ConnectorEvents.NETWORK_CHANGE_FAILED, e); + } + } + + private restartKit(network: Network) { + const wallet = this.kit.getWallet() as WalletConnectWalletV1; + this.network = network; // must set to prevent loop + try { + this.kit.connection.stop(); // this blows up if its already stopped + } finally { + this.kit = newKit(network.rpcUrl, wallet); + } + } + + private async onSessionCreated( + _error: Error | null, + session: SessionConnect + ) { + // TODO HANDLE FAILED TO CONNECT STATE + const connectSession = session; + await this.onConnected(connectSession); + } + + private onCallRequest(error: Error | null, payload: EthProposal) { + getApplicationLogger().debug( + '[wallet-connect] onCallRequest => Payload:', + payload, + error ? `Error ${error.name} ${error.message}` : '' + ); + if (error) { + this.emit(ConnectorEvents.WC_ERROR, error); + } + } + + private async onWcSessionUpdate( + _error: Error | null, + session: SessionProposal + ) { + getApplicationLogger().debug( + 'wallet-connect', + 'on-wc-session-update', + session, + _error + ); + const params = session.params[0]; + await this.combinedSessionUpdater(params); + } + + private async onSessionUpdated(_error: Error | null, session: SessionUpdate) { + getApplicationLogger().debug( + 'wallet-connect', + 'on-session-update', + session, + _error + ); + const params = session.params[0]; + + // TODO emit event when there is an error + await this.combinedSessionUpdater(params); + } + + private onSessionDeleted(_error: Error | null, session: SessionDisconnect) { + getApplicationLogger().debug( + 'wallet-connect', + 'on-session-delete', + session, + _error + ); + // since dapps send the both when they initiate disconnection and when responding to disconnection requests + // check if dapp initiated the closure to avoid closing twice. + if (session.params[0]?.message?.startsWith(END_MESSAGE)) { + return; + } + void this.close(); + } + + private combinedSessionUpdater(params: { + accounts?: string[]; + chainId: number | null; + }) { + if (params.chainId == null) { + return this.close(); + } + + if (params.chainId !== this.network.chainId) { + return this.emit(ConnectorEvents.WALLET_CHAIN_CHANGED, params.chainId); + } + + const accounts = params.accounts as string[]; + const addressFromSessionUpdate = accounts[0]; + if ( + typeof addressFromSessionUpdate === 'string' && + addressFromSessionUpdate !== this.kit.connection.defaultAccount + ) { + return this.onAddressChange(addressFromSessionUpdate); + } + } + + // for when the wallet is already on the desired network and the kit / dapp need to catch up. + continueNetworkUpdateFromWallet(network: Network): void { + this.restartKit(network); + this.emit(ConnectorEvents.NETWORK_CHANGED, network.name); + } + + supportsFeeCurrency() { + // If on WC 1 it will not work due to fields being dropped + if (!this.version || this.version === 1) { + return false; + } + // TODO when V2 is used again check based on wallet? + return true; + } + + private async onAddressChange(address: string) { + this.kit.connection.defaultAccount = + await this.fetchWalletAddressForAccount(address); + this.emit(ConnectorEvents.ADDRESS_CHANGED, address); + } + + private async onConnected(session: SessionConnect) { + const sessionAccount = session.params[0].accounts[0]; + const walletAddress = await this.fetchWalletAddressForAccount( + sessionAccount + ); + if (this.kit.connection.defaultAccount !== walletAddress) { + this.kit.connection.defaultAccount = walletAddress; + } + + this.emit(ConnectorEvents.CONNECTED, { + walletType: this.type, + walletId: this.walletId as string, + networkName: this.network.name, + address: sessionAccount, + }); + } + + private async fetchWalletAddressForAccount(address?: string) { + if (!address) { + return undefined; + } + const accounts = await this.kit.contracts.getAccounts(); + const walletAddress = await accounts.getWalletAddress(address); + return new BigNumber(walletAddress).isZero() ? address : walletAddress; + } + + updateFeeCurrency: typeof updateFeeCurrency = updateFeeCurrency.bind(this); + + async close(message?: string): Promise { + getApplicationLogger().log('wallet-connect', 'close', message); + try { + const wallet = this.kit.getWallet() as WalletConnectWalletV1; + await wallet.close(`${END_MESSAGE} : ${message || ''}`); + this.kit.connection.stop(); + } finally { + this.disconnect(); + } + } +} + +const END_MESSAGE = '[react-celo] WC SESSION ENDED BY DAPP'; diff --git a/packages/react-celo/src/constants.tsx b/packages/react-celo/src/constants.tsx index c1e3b09a..0d4c4cda 100644 --- a/packages/react-celo/src/constants.tsx +++ b/packages/react-celo/src/constants.tsx @@ -1,5 +1,6 @@ import { isMobile } from 'react-device-detect'; +import * as Icons from './components/icons'; import { ChainId, Maybe, @@ -8,28 +9,19 @@ import { WalletConnectProvider, } from './types'; import { isEthereumFromMetamask, isEthereumPresent } from './utils/ethereum'; -import { - CELO, - CELO_DANCE, - CELO_TERMINAL, - CHROME_EXTENSION_STORE, - ETHEREUM, - LEDGER, - METAMASK, - PRIVATE_KEY, - STEAKWALLET, - VALORA, - WALLETCONNECT, -} from './walletIcons'; -export const localStorageKeys = { - lastUsedAddress: 'react-celo/last-used-address', - lastUsedNetwork: 'react-celo/last-used-network', - lastUsedWalletType: 'react-celo/last-used-wallet', - lastUsedWalletId: 'react-celo/last-used-wallet-id', - lastUsedWalletArguments: 'react-celo/last-used-wallet-arguments', - lastUsedFeeCurrency: 'react-celo/last-used-fee-currency', -}; +export enum localStorageKeys { + lastUsedAddress = 'react-celo/last-used-address', + lastUsedNetwork = 'react-celo/last-used-network', + lastUsedWalletType = 'react-celo/last-used-wallet', + lastUsedWalletId = 'react-celo/last-used-wallet-id', + + lastUsedIndex = 'react-celo/last-used-index', + lastUsedPrivateKey = 'react-celo/last-used-private-key', + + lastUsedWalletArguments = 'react-celo/last-used-wallet-arguments', + lastUsedFeeCurrency = 'react-celo/last-used-fee-currency', +} export enum SupportedProviders { CeloExtensionWallet = 'Celo Extension Wallet', @@ -43,6 +35,7 @@ export enum SupportedProviders { Valora = 'Valora', WalletConnect = 'WalletConnect', Steakwallet = 'Steakwallet', + CoinbaseWallet = 'Coinbase Wallet', } export enum WalletTypes { Valora = 'Valora', @@ -56,6 +49,7 @@ export enum WalletTypes { Injected = 'Injected', PrivateKey = 'PrivateKey', Unauthenticated = 'Unauthenticated', + CoinbaseWallet = 'CoinbaseWallet', } export enum Priorities { @@ -71,6 +65,7 @@ export enum Platform { } export const WalletIds = { + WalletConnect: '_', Valora: 'd01c7758d741b363e637a817a09bcf579feae4db9f5bb16f599fdd1f66e2f974', CeloWallet: '36d854b702817e228d5c853c528d7bdb46f4bb041d255f67b82eb47111e5676b', @@ -82,14 +77,14 @@ export const WalletIds = { }; export const PROVIDERS: { - [K in SupportedProviders]: Provider | WalletConnectProvider; + [K in SupportedProviders]: Provider; } = { [SupportedProviders.Valora]: { name: SupportedProviders.Valora, type: WalletTypes.WalletConnect, description: 'Connect to Valora, a mobile payments app that works worldwide', - icon: VALORA, + icon: Icons.Valora, canConnect: () => true, showInList: () => true, listPriority: () => Priorities.Popular, @@ -104,22 +99,22 @@ export const PROVIDERS: { return false; } }, - }, + } as WalletConnectProvider, [SupportedProviders.WalletConnect]: { name: SupportedProviders.WalletConnect, type: WalletTypes.WalletConnect, description: 'Scan a QR code to connect your wallet', - icon: WALLETCONNECT, + icon: Icons.WalletConnect, canConnect: () => true, showInList: () => true, - listPriority: () => Priorities.Popular, + listPriority: () => Priorities.Default, supportedPlatforms: [Platform.Mobile], - }, + } as WalletConnectProvider, [SupportedProviders.Ledger]: { name: SupportedProviders.Ledger, type: WalletTypes.Ledger, description: 'Sync with your Ledger hardware wallet', - icon: LEDGER, + icon: Icons.Ledger, canConnect: () => true, showInList: () => !isMobile, listPriority: () => Priorities.Popular, @@ -128,10 +123,10 @@ export const PROVIDERS: { name: SupportedProviders.CeloWallet, type: WalletTypes.WalletConnect, description: 'Connect to Celo Wallet for web or desktop', - icon: CELO, + icon: Icons.Celo, canConnect: () => true, showInList: () => true, - listPriority: () => (!isMobile ? 0 : 1), + listPriority: () => Priorities.Default, walletConnectId: WalletIds.CeloWallet, installURL: 'https://celowallet.app/', supportedPlatforms: [Platform.Desktop, Platform.Web], @@ -145,19 +140,19 @@ export const PROVIDERS: { return false; } }, - }, + } as WalletConnectProvider, [SupportedProviders.CeloTerminal]: { name: SupportedProviders.CeloTerminal, type: WalletTypes.WalletConnect, description: 'Connect to the Celo Terminal desktop app', - icon: CELO_TERMINAL, + icon: Icons.CeloTerminal, canConnect: () => true, showInList: () => !isMobile, listPriority: () => Priorities.Default, installURL: 'https://celoterminal.com/', walletConnectId: WalletIds.CeloTerminal, supportedPlatforms: [], - }, + } as WalletConnectProvider, [SupportedProviders.MetaMask]: { name: SupportedProviders.MetaMask, type: WalletTypes.MetaMask, @@ -166,7 +161,7 @@ export const PROVIDERS: { ? 'Connect with MetaMask Mobile App' : 'Open MetaMask Mobile App' : 'Use the Metamask browser extension. Celo support is limited.', - icon: METAMASK, + icon: Icons.MetaMask, canConnect: () => isMobile || isEthereumFromMetamask(), showInList: () => true, listPriority: () => Priorities.Popular, @@ -179,7 +174,7 @@ export const PROVIDERS: { name: SupportedProviders.CeloExtensionWallet, type: WalletTypes.CeloExtensionWallet, description: 'Use a wallet from the the Celo chrome extension', - icon: CHROME_EXTENSION_STORE, + icon: Icons.ChromeExtensionStore, canConnect: () => !!window.celo, showInList: () => !isMobile, listPriority: () => Priorities.Default, @@ -190,7 +185,7 @@ export const PROVIDERS: { name: SupportedProviders.Injected, type: WalletTypes.Injected, description: 'Connect any Ethereum wallet to Celo', - icon: ETHEREUM, + icon: Icons.Ethereum, canConnect: () => isEthereumPresent(), showInList: () => isEthereumFromMetamask(), listPriority: () => Priorities.Default, @@ -200,7 +195,7 @@ export const PROVIDERS: { type: WalletTypes.PrivateKey, description: 'Enter a plaintext private key to load your account (testing only)', - icon: PRIVATE_KEY, + icon: Icons.PrivateKey, canConnect: () => true, showInList: () => process.env.NODE_ENV !== 'production', listPriority: () => Priorities.Default, @@ -209,7 +204,7 @@ export const PROVIDERS: { name: SupportedProviders.CeloDance, type: WalletTypes.WalletConnect, description: 'Send, vote, and earn rewards within one wallet', - icon: CELO_DANCE, + icon: Icons.CeloDance, canConnect: () => true, showInList: () => true, listPriority: () => Priorities.Default, @@ -224,15 +219,15 @@ export const PROVIDERS: { return false; } }, - }, + } as WalletConnectProvider, [SupportedProviders.Steakwallet]: { name: SupportedProviders.Steakwallet, description: 'Scan a QR code to connect your wallet', type: WalletTypes.WalletConnect, - icon: STEAKWALLET, + icon: Icons.SteakWallet, canConnect: () => true, showInList: () => true, - listPriority: () => Priorities.Popular, + listPriority: () => Priorities.Default, installURL: 'https://steakwallet.fi/', walletConnectId: WalletIds.Steakwallet, supportedPlatforms: [Platform.Mobile], @@ -244,22 +239,18 @@ export const PROVIDERS: { return false; } }, + } as WalletConnectProvider, + [SupportedProviders.CoinbaseWallet]: { + name: SupportedProviders.CoinbaseWallet, + type: WalletTypes.CoinbaseWallet, + description: 'Scan a QR code to connect your wallet', + icon: Icons.CoinbaseWallet, + canConnect: () => true, + showInList: () => true, + listPriority: () => Priorities.Default, }, }; -export const images = { - [SupportedProviders.Valora]: VALORA, - [SupportedProviders.MetaMask]: METAMASK, - [SupportedProviders.WalletConnect]: WALLETCONNECT, - [SupportedProviders.Ledger]: LEDGER, - [SupportedProviders.CeloWallet]: CELO, - [SupportedProviders.CeloDance]: CELO_DANCE, - [SupportedProviders.CeloTerminal]: CELO_TERMINAL, - [SupportedProviders.CeloExtensionWallet]: CHROME_EXTENSION_STORE, - [SupportedProviders.PrivateKey]: PRIVATE_KEY, - [SupportedProviders.Steakwallet]: STEAKWALLET, -} as const; - export const NetworkNames = { Alfajores: 'Alfajores' as const, Baklava: 'Baklava' as const, @@ -318,7 +309,7 @@ export const getProviderForWallet = ( /** * Default networks to connect to. */ -export const DEFAULT_NETWORKS = [ +export const DEFAULT_NETWORKS: Network[] = [ Mainnet, Alfajores, Baklava, diff --git a/packages/react-celo/src/ethers.ts b/packages/react-celo/src/ethers.ts index 1885b6ba..aebb4f06 100644 --- a/packages/react-celo/src/ethers.ts +++ b/packages/react-celo/src/ethers.ts @@ -2,9 +2,9 @@ import { JsonRpcSigner, Web3Provider } from '@ethersproject/providers'; import { ExternalProvider } from '@ethersproject/providers/lib/web3-provider'; import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useIsMounted } from './hooks/use-is-mounted'; import { Maybe } from './types'; import { useCelo } from './use-celo'; -import { useIsMounted } from './utils/useIsMounted'; export const useProvider = (): Web3Provider => { const { kit, network } = useCelo(); diff --git a/packages/react-celo/src/global.d.ts b/packages/react-celo/src/global.d.ts index 288896bb..c0cea5c1 100644 --- a/packages/react-celo/src/global.d.ts +++ b/packages/react-celo/src/global.d.ts @@ -9,6 +9,12 @@ declare global { removeListener?: (...args: unknown[]) => void; autoRefreshOnNetworkChange?: boolean; enable: () => Promise; + publicConfigStore: { + on: ( + event: string, + cb: (args: { networkVersion: number }) => void + ) => void; + }; }; web3?: unknown; } @@ -18,8 +24,11 @@ interface Ethereum extends Exclude { on: AddEthereumEventListener; removeListener: RemoveEthereumEventListener; isMetaMask?: boolean; + isConnected: () => boolean; + selectedAddress: string | undefined; request: EthereumRequest; enable: () => Promise; + chainId?: string; } type AddEthereumEventListener = ( @@ -47,6 +56,7 @@ interface EthereumRequestReturns { wallet_addEthereumChain: null; wallet_watchAsset: boolean; wallet_switchEthereumChain: null; + eth_chainId: string; } interface BitMatrix { diff --git a/packages/react-celo/src/hooks/use-coinbase-wallet-connector.ts b/packages/react-celo/src/hooks/use-coinbase-wallet-connector.ts new file mode 100644 index 00000000..afd0d656 --- /dev/null +++ b/packages/react-celo/src/hooks/use-coinbase-wallet-connector.ts @@ -0,0 +1,45 @@ +import { useEffect } from 'react'; + +import { CoinbaseWalletConnector } from '../connectors'; +import { Connector, Dapp, Maybe, Network } from '../types'; +import { useCeloInternal } from '../use-celo'; +import { getApplicationLogger } from '../utils/logger'; + +export function useCoinbaseWalletConnector( + onSubmit: (connector: Connector) => void +): UseCoinbaseWalletConnector { + const { + network, + feeCurrency, + initConnector, + initError: error, + dapp, + } = useCeloInternal(); + + useEffect(() => { + let stale; + void (async () => { + const connector = new CoinbaseWalletConnector(network, dapp); + try { + await initConnector(connector); + if (!stale) { + onSubmit(connector); + } + } catch (e) { + getApplicationLogger().error('[use-coinbase-wallet-connector]', e); + } + })(); + + return () => { + stale = true; + }; + }, [initConnector, network, dapp, onSubmit, feeCurrency]); + + return { error, dapp, network }; +} + +export interface UseCoinbaseWalletConnector { + error: Maybe; + network: Network; + dapp: Dapp; +} diff --git a/packages/react-celo/src/ContractCacheBuilder.tsx b/packages/react-celo/src/hooks/use-contracts-cache.tsx similarity index 94% rename from packages/react-celo/src/ContractCacheBuilder.tsx rename to packages/react-celo/src/hooks/use-contracts-cache.tsx index c6c39604..28db85f6 100644 --- a/packages/react-celo/src/ContractCacheBuilder.tsx +++ b/packages/react-celo/src/hooks/use-contracts-cache.tsx @@ -2,7 +2,7 @@ import { AddressRegistry } from '@celo/contractkit/lib/address-registry'; import { MiniContractKit } from '@celo/contractkit/lib/mini-kit'; import { useMemo } from 'react'; -import { Connector } from './types'; +import { Connector } from '../types'; export type ContractCacheBuilder = ( connection: MiniContractKit['connection'], diff --git a/packages/react-celo/src/connectors/useMetaMaskConnector.ts b/packages/react-celo/src/hooks/use-injected-connector.ts similarity index 86% rename from packages/react-celo/src/connectors/useMetaMaskConnector.ts rename to packages/react-celo/src/hooks/use-injected-connector.ts index 76e47108..3f3c5aee 100644 --- a/packages/react-celo/src/connectors/useMetaMaskConnector.ts +++ b/packages/react-celo/src/hooks/use-injected-connector.ts @@ -1,9 +1,10 @@ import { useCallback, useEffect } from 'react'; +import { InjectedConnector, MetaMaskConnector } from '../connectors'; import { Connector, Dapp, Maybe, Network } from '../types'; import { useCeloInternal } from '../use-celo'; +import { getApplicationLogger } from '../utils/logger'; import { CeloTokens } from '../utils/metamask'; -import { InjectedConnector, MetaMaskConnector } from './connectors'; export function useInjectedConnector( onSubmit: (connector: Connector) => void, @@ -31,7 +32,7 @@ export function useInjectedConnector( onSubmit(connector); } } catch (e) { - console.error(e); + getApplicationLogger().error('[useMetaMaskConnector]', e); } })(); diff --git a/packages/react-celo/src/utils/useIsMounted.ts b/packages/react-celo/src/hooks/use-is-mounted.ts similarity index 100% rename from packages/react-celo/src/utils/useIsMounted.ts rename to packages/react-celo/src/hooks/use-is-mounted.ts diff --git a/packages/react-celo/src/utils/useProviders.ts b/packages/react-celo/src/hooks/use-providers.ts similarity index 67% rename from packages/react-celo/src/utils/useProviders.ts rename to packages/react-celo/src/hooks/use-providers.ts index be3874d0..bc2cad95 100644 --- a/packages/react-celo/src/utils/useProviders.ts +++ b/packages/react-celo/src/hooks/use-providers.ts @@ -3,33 +3,36 @@ import { isMobile } from 'react-device-detect'; import { localStorageKeys, + Platform, Priorities, PROVIDERS, SupportedProviders, WalletTypes, } from '../constants'; import { Maybe, Provider, WalletConnectProvider, WalletEntry } from '../types'; -import localStorage from './localStorage'; -import { defaultProviderSort } from './sort'; +import { getTypedStorageKey } from '../utils/local-storage'; +import { defaultProviderSort } from '../utils/sort'; export function walletToProvider(wallet: WalletEntry): WalletConnectProvider { return { name: wallet.name, + walletConnectId: wallet.id, type: WalletTypes.WalletConnect, description: wallet.description || 'Missing description in registry', icon: wallet.logos.md, canConnect: () => true, showInList: () => isMobile ? Object.values(wallet.mobile).some(Boolean) : true, - listPriority: () => 0, + listPriority: () => Priorities.Default, installURL: wallet.homepage, + supportedPlatforms: [Platform.Mobile], }; } export function getRecent(): Maybe { - const type = localStorage.getItem(localStorageKeys.lastUsedWalletType); - const id = localStorage.getItem(localStorageKeys.lastUsedWalletId); - let provider; + const type = getTypedStorageKey(localStorageKeys.lastUsedWalletType); + const id = getTypedStorageKey(localStorageKeys.lastUsedWalletId); + let provider: Maybe; if (id && WalletTypes.WalletConnect === type) { provider = Object.values(PROVIDERS).find( @@ -48,13 +51,17 @@ export default function useProviders( includedDefaultProviders: SupportedProviders[], sort = defaultProviderSort, search?: string -) { +): [ + priority: Priorities, + entry: [providerKey: string, provider: Provider][] +][] { const record: Record = useMemo( () => ({ ...includedDefaultProviders.reduce((all, current) => { all[current] = PROVIDERS[current]; return all; }, {} as Record), + ...wallets.reduce((acc, wallet) => { acc[wallet.id] = walletToProvider(wallet); return acc; @@ -73,24 +80,26 @@ export default function useProviders( .sort(([, a], [, b]) => sort(a, b)); }, [record, sort, search]); - const recentlyUsedProvider = getRecent(); - const prioritizedProviders = useMemo(() => { - const map = providers.reduce((acc, [providerKey, provider]) => { - const priority = - recentlyUsedProvider && recentlyUsedProvider.name === provider.name - ? Priorities.Recent - : Priorities.Default; - - if (!acc.has(priority)) { - acc.set(priority, []); - } + if (!providers.length) { + return []; + } - acc.get(priority)?.push([providerKey, provider]); - return acc; - }, new Map()); + const recentlyUsedProvider = getRecent(); + if (recentlyUsedProvider) { + const index = providers.findIndex( + ([providerKey]) => providerKey === recentlyUsedProvider.name + ); - return [...map.entries()].sort(([prioA], [prioB]) => prioB - prioA); - }, [recentlyUsedProvider, providers]); + if (index > -1) { + return [ + [ + Priorities.Recent, + [[recentlyUsedProvider.name, recentlyUsedProvider]], + ], + [Priorities.Default, providers.filter((_, i) => index !== i)], + ]; + } + } - return prioritizedProviders; + return [[Priorities.Default, providers]]; } diff --git a/packages/react-celo/src/utils/useTheme.ts b/packages/react-celo/src/hooks/use-theme.ts similarity index 100% rename from packages/react-celo/src/utils/useTheme.ts rename to packages/react-celo/src/hooks/use-theme.ts diff --git a/packages/react-celo/src/connectors/useWalletConnectConnector.ts b/packages/react-celo/src/hooks/use-wallet-connect-connector.ts similarity index 54% rename from packages/react-celo/src/connectors/useWalletConnectConnector.ts rename to packages/react-celo/src/hooks/use-wallet-connect-connector.ts index cdf1e93a..33923d7d 100644 --- a/packages/react-celo/src/connectors/useWalletConnectConnector.ts +++ b/packages/react-celo/src/hooks/use-wallet-connect-connector.ts @@ -3,10 +3,13 @@ import { CANCELED } from '@celo/wallet-walletconnect-v1'; import { useCallback, useEffect, useState } from 'react'; +import { WalletConnectConnector } from '../connectors'; +import { ConnectorEvents } from '../connectors/common'; +import { buildOptions } from '../connectors/wallet-connect'; import { Connector, Maybe } from '../types'; import { useCeloInternal } from '../use-celo'; -import { useWalletVersion } from '../utils/useWalletVersion'; -import { WalletConnectConnector } from './connectors'; +import { getApplicationLogger } from '../utils/logger'; +import { useWalletVersion } from './use-wallet-version'; interface UseWalletConnectConnector { error: Maybe; @@ -15,13 +18,13 @@ interface UseWalletConnectConnector { retry: () => void; } -export function useWalletConnectConnector( +export default function useWalletConnectConnector( onSubmit: (connector: Connector) => void, autoOpen: boolean, - getDeeplinkUrl?: (uri: string) => string | false, - walletId?: string + walletId: string, + getDeeplinkUrl?: (uri: string) => string | false ): UseWalletConnectConnector { - const { network, feeCurrency, initConnector, destroy } = useCeloInternal(); + const { network, feeCurrency, initConnector, disconnect } = useCeloInternal(); const [uri, setUri] = useState>(null); const [loading, setLoading] = useState(false); const [error, setError] = useState>(null); @@ -41,8 +44,9 @@ export function useWalletConnectConnector( void (async () => { if (version == null) { - console.warn( - 'WalletconnectConnector initialization awaiting for registry' + getApplicationLogger().debug( + '[useWalletConnectConnector]', + 'Initialization awaiting for registry' ); return; } @@ -50,52 +54,49 @@ export function useWalletConnectConnector( connector = new WalletConnectConnector( network, feeCurrency, - { - connect: { - chainId: network.chainId, - // Uncomment with WCV2 support - // metadata: { - // name: dapp.name, - // description: dapp.description, - // url: dapp.url, - // icons: [dapp.icon], - // }, - // permissions: { - // blockchain: { - // chains: [`eip155:${}`], - // }, - // jsonrpc: { - // methods: Object.values(SupportedMethods), - // }, - // }, - }, - }, + buildOptions(network), autoOpen, getDeeplinkUrl, - version + version, + walletId ); - connector.onUri((newUri) => { + connector.on(ConnectorEvents.WC_URI_RECEIVED, (nextURI) => { + getApplicationLogger().debug( + '[useWalletConnectConnector]', + 'Generated WC URI', + nextURI + ); if (mounted) { - setUri(newUri); + setUri(nextURI); } }); - connector.onConnect(() => { - setLoading(true); - }); - connector.onClose(() => { - void destroy().then(() => { - setError('Connection with wallet was closed.'); - setUri(null); - }); + connector.on(ConnectorEvents.DISCONNECTED, () => { + getApplicationLogger().debug( + '[useWalletConnectConnector]', + 'Lost connection to WC servers' + ); + setError('Connection with wallet was closed.'); + setUri(null); }); + try { await initConnector(connector); onSubmit(connector); } catch (reason) { if (reason === CANCELED) { - return; + getApplicationLogger().debug( + '[useWalletConnectConnector]', + 'User canceled connection' + ); + // disconnect so we dont have open connectors all over the place + return disconnect(); } + getApplicationLogger().debug( + '[useWalletConnectConnector]', + 'WC error', + reason + ); setError((reason as Error).message); } })(); @@ -105,7 +106,8 @@ export function useWalletConnectConnector( // if initialised is false, it means the connection was canceled or errored. // We should cleanup the state if (!connector?.initialised) { - void connector?.close('Connection canceled'); + // disconnect so we dont have open connectors all over the place + void disconnect(); } setUri(null); diff --git a/packages/react-celo/src/utils/useWalletVersion.ts b/packages/react-celo/src/hooks/use-wallet-version.ts similarity index 100% rename from packages/react-celo/src/utils/useWalletVersion.ts rename to packages/react-celo/src/hooks/use-wallet-version.ts diff --git a/packages/react-celo/src/index.ts b/packages/react-celo/src/index.ts index 4f0d3b4e..981b6d87 100644 --- a/packages/react-celo/src/index.ts +++ b/packages/react-celo/src/index.ts @@ -4,3 +4,4 @@ export * from './react-celo-provider'; export { defaultScreens as Screens } from './screens'; export * from './types'; export { UseCelo, useCelo, useContractKit } from './use-celo'; +export { ILogger } from './utils/logger'; diff --git a/packages/react-celo/src/modals/action.tsx b/packages/react-celo/src/modals/action.tsx index 289b6cc5..e42e0fe7 100644 --- a/packages/react-celo/src/modals/action.tsx +++ b/packages/react-celo/src/modals/action.tsx @@ -3,11 +3,12 @@ import { isMobile } from 'react-device-detect'; import ReactModal from 'react-modal'; import Spinner from '../components/spinner'; +import useTheme from '../hooks/use-theme'; import { Theme } from '../types'; import { useCeloInternal } from '../use-celo'; -import { hexToRGB } from '../utils/helpers'; +import { hexToRGB } from '../utils/colors'; +import { useFixedBody } from '../utils/helpers'; import cls from '../utils/tailwind'; -import useTheme from '../utils/useTheme'; import { styles as modalStyles } from './connect'; const styles = cls({ @@ -90,6 +91,7 @@ export const ActionModal: React.FC = ({ }: Props) => { const theme = useTheme(); const { pendingActionCount, dapp } = useCeloInternal(); + useFixedBody(pendingActionCount > 0); return ( void; - provider?: WalletConnectProvider; - }>; + [x in SupportedProviders]?: React.FC; }; RenderProvider?: React.FC<{ provider: Provider; diff --git a/packages/react-celo/src/react-celo-provider-props.tsx b/packages/react-celo/src/react-celo-provider-props.tsx new file mode 100644 index 00000000..b79a6e82 --- /dev/null +++ b/packages/react-celo/src/react-celo-provider-props.tsx @@ -0,0 +1,30 @@ +import { CeloTokenContract } from '@celo/contractkit/lib/base'; +import { ReactNode } from 'react'; + +import { ContractCacheBuilder } from './hooks/use-contracts-cache'; +import { ActionModalProps, ConnectModalProps } from './modals'; +import { Dapp, Network, Theme } from './types'; +import { ILogger } from './utils/logger'; + +export interface CeloProviderProps { + children: ReactNode; + dapp: Dapp; + /** + * `network` has been deprecated and replaced with defaultNetwork + * since passing a full object could lead to bugs + */ + network?: Network; + defaultNetwork?: string; // must match the name of a network in networks Array + networks?: Network[]; + theme?: Theme; + feeCurrency?: CeloTokenContract; + buildContractsCache?: ContractCacheBuilder; + connectModal?: ConnectModalProps; + actionModal?: { + reactModalProps?: Partial; + render?: (props: ActionModalProps) => ReactNode; + }; + logger?: ILogger; +} + +export default CeloProviderProps; diff --git a/packages/react-celo/src/react-celo-provider-state.tsx b/packages/react-celo/src/react-celo-provider-state.tsx new file mode 100644 index 00000000..500b365c --- /dev/null +++ b/packages/react-celo/src/react-celo-provider-state.tsx @@ -0,0 +1,117 @@ +import { CeloContract } from '@celo/contractkit/lib/base'; +import React, { useCallback, useMemo, useReducer } from 'react'; + +import { UnauthenticatedConnector } from './connectors'; +import { DEFAULT_NETWORKS, Mainnet } from './constants'; +import { useIsMounted } from './hooks/use-is-mounted'; +import { CeloProviderProps } from './react-celo-provider-props'; +import { + Actions, + ActionsMap, + celoReactReducer, + ReducerState, +} from './react-celo-reducer'; +import { Network } from './types'; +import { getInitialNetwork } from './utils/get-initial-network'; +import { loadPreviousState } from './utils/helpers'; +import { resurrector } from './utils/resurrector'; + +const initialState: ReducerState = { + connector: new UnauthenticatedConnector(Mainnet), + connectorInitError: null, + dapp: { + name: 'Celo dApp', + description: 'Celo dApp', + url: 'https://celo.org', + icon: 'https://celo.org/favicon.ico', + }, + network: Mainnet, + networks: DEFAULT_NETWORKS, + pendingActionCount: 0, + address: null, + connectionCallback: null, + feeCurrency: CeloContract.GoldToken, + theme: null, +}; + +// This type lets you call dispatch with one or two arguments: +// First a type, and second an optional payload that matches an +// action's payload with that type. + +export type Dispatcher = < + Type extends Actions['type'], + Payload extends ActionsMap[Type] +>( + type: Type, + // This line makes it so if there shouldn't be a payload then + // you only need to call the function with the type, but if + // there should be a payload then you need the second argument. + ...payload: Payload extends undefined ? [undefined?] : [Payload] +) => void; + +function useDispatch(dispatch: React.Dispatch): Dispatcher { + const isMountedRef = useIsMounted(); + return useCallback( + (type, ...payload) => { + if (isMountedRef.current) { + dispatch({ type, payload: payload[0] } as Actions); + } + }, + [dispatch, isMountedRef] + ); +} + +type CeloStateProps = Pick< + CeloProviderProps, + 'dapp' | 'theme' | 'network' | 'defaultNetwork' | 'networks' | 'feeCurrency' +> & { networks: Network[] }; + +export function useCeloState({ + dapp, + network, + defaultNetwork, + theme, + networks, + feeCurrency, +}: CeloStateProps): [ReducerState, Dispatcher] { + const stateFromLocalStorage = useMemo( + () => loadPreviousState(), + // We only want this to run on mount so the deps array is empty. + // This is OK because the stateFromLocalStorage is only used to create the initial reducer state + /* eslint-disable-next-line */ + [] + ); + + const connector = useMemo(() => { + return resurrector(networks, dapp); + /* eslint-disable-next-line */ + }, []); + + const initialNetwork = getInitialNetwork( + networks, + defaultNetwork, + network, + stateFromLocalStorage.networkName + ); + + const [state, _dispatch] = useReducer(celoReactReducer, { + ...initialState, + address: stateFromLocalStorage.address, + connector: connector || initialState.connector, + network: initialNetwork, + feeCurrency: + stateFromLocalStorage.feeCurrency || + feeCurrency || + CeloContract.GoldToken, + networks, + theme, + dapp: { + ...dapp, + icon: dapp.icon ?? `${dapp.url}/favicon.ico`, + }, + }); + + const dispatch: Dispatcher = useDispatch(_dispatch); + + return [state, dispatch]; +} diff --git a/packages/react-celo/src/react-celo-provider.tsx b/packages/react-celo/src/react-celo-provider.tsx index fe248051..7072971e 100644 --- a/packages/react-celo/src/react-celo-provider.tsx +++ b/packages/react-celo/src/react-celo-provider.tsx @@ -1,46 +1,14 @@ -import { CeloContract, CeloTokenContract } from '@celo/contractkit/lib/base'; -import React, { - ReactNode, - useCallback, - useEffect, - useMemo, - useReducer, -} from 'react'; +import { CeloContract } from '@celo/contractkit/lib/base'; +import React, { useEffect } from 'react'; import IOSViewportFix from './components/ios-viewport-fix'; -import { CONNECTOR_TYPES, UnauthenticatedConnector } from './connectors'; import { DEFAULT_NETWORKS, Mainnet } from './constants'; -import { ContractCacheBuilder } from './ContractCacheBuilder'; -import { - ActionModal, - ActionModalProps, - ConnectModal, - ConnectModalProps, -} from './modals'; -import { - Actions, - ActionsMap, - celoReactReducer, - ReducerState, -} from './react-celo-reducer'; -import { Dapp, Network, Theme } from './types'; +import { ActionModal, ConnectModal } from './modals'; +import { CeloProviderProps } from './react-celo-provider-props'; +import { Dispatcher, useCeloState } from './react-celo-provider-state'; +import { ReducerState } from './react-celo-reducer'; import { CeloMethods, useCeloMethods } from './use-celo-methods'; -import { loadPreviousConfig } from './utils/helpers'; -import { useIsMounted } from './utils/useIsMounted'; - -// This type lets you call dispatch with one or two arguments: -// First a type, and second an optional payload that matches an -// action's payload with that type. -export type Dispatcher = < - Type extends Actions['type'], - Payload extends ActionsMap[Type] ->( - type: Type, - // This line makes it so if there shouldn't be a payload then - // you only need to call the function with the type, but if - // there should be a payload then you need the second argument. - ...payload: Payload extends undefined ? [undefined?] : [Payload] -) => void; +import { getApplicationLogger, setApplicationLogger } from './utils/logger'; type ReactCeloContextInterface = readonly [ ReducerState, @@ -48,24 +16,6 @@ type ReactCeloContextInterface = readonly [ CeloMethods ]; -const initialState = { - connector: new UnauthenticatedConnector(Mainnet), - connectorInitError: null, - dapp: { - name: 'Celo dApp', - description: 'Celo dApp', - url: 'https://celo.org', - icon: 'https://celo.org/favicon.ico', - }, - network: Mainnet, - networks: DEFAULT_NETWORKS, - pendingActionCount: 0, - address: null, - connectionCallback: null, - feeCurrency: CeloContract.GoldToken, - theme: null, -}; - export const [useReactCeloContext, ContextProvider] = createReactCeloContext(); // This makes it so we don't have to provide defaults for our context @@ -91,55 +41,51 @@ export const CeloProvider: React.FC = ({ connectModal, actionModal, dapp, - network = Mainnet, + network, // TODO:#246 remove when network prop is removed + defaultNetwork = Mainnet.name, theme, networks = DEFAULT_NETWORKS, feeCurrency = CeloContract.GoldToken, buildContractsCache, + logger, }: CeloProviderProps) => { - const isMountedRef = useIsMounted(); - const previousConfig = useMemo( - () => loadPreviousConfig(network, feeCurrency, networks), - // We only want this to run on mount so the deps array is empty. - // This is OK because the previousConfig is only used to create the initial reducer state - /* eslint-disable-next-line */ - [] - ); - const [state, _dispatch] = useReducer(celoReactReducer, { - ...initialState, - ...previousConfig, - network: previousConfig.network || network, - feeCurrency: previousConfig.feeCurrency || feeCurrency, - networks, + if (logger) { + setApplicationLogger(logger); + } + + const [state, dispatch] = useCeloState({ + dapp, + network, + defaultNetwork, theme, - dapp: { - ...dapp, - icon: dapp.icon ?? `${dapp.url}/favicon.ico`, - }, + networks, + feeCurrency, }); - const dispatch: Dispatcher = useCallback( - (type, ...payload) => { - if (isMountedRef.current) { - _dispatch({ type, payload: payload[0] } as Actions); - } - }, - [isMountedRef] - ); - const methods = useCeloMethods(state, dispatch, buildContractsCache); + // what happens when i disconnect, need to be able to switch chains still. + // need to init Unauthenticated connnector both at startup and when last chain was disconnected or Replace the null object pattern + // benefit is that you might still want to just passively watch a chain + // downside is there are some sementics that get weird useEffect(() => { - if (CONNECTOR_TYPES[state.connector.type] !== UnauthenticatedConnector) { - methods.initConnector(state.connector).catch(() => { - // If the connector fails to initialise on mount then we reset. - dispatch('destroy'); - }); - } + getApplicationLogger().debug( + 'onLoad Initialisation of', + state.connector.type, + 'Connector' + ); + methods.initConnector(state.connector).catch(async (e) => { + getApplicationLogger().error( + 'onLoad Initialisation Failed', + state.connector.type, + e + ); + // If the connector fails to initialise on mount then we reset. + await methods.disconnect(); + }); // We only want this to run on mount so the deps array is empty. /* eslint-disable-next-line */ }, []); - return ( @@ -155,18 +101,3 @@ export const CeloProvider: React.FC = ({ * @deprecated Use the alias {@link CeloProvider} Component instead. */ export const ContractKitProvider = CeloProvider; - -export interface CeloProviderProps { - children: ReactNode; - dapp: Dapp; - network?: Network; - networks?: Network[]; - theme?: Theme; - feeCurrency?: CeloTokenContract; - buildContractsCache?: ContractCacheBuilder; - connectModal?: ConnectModalProps; - actionModal?: { - reactModalProps?: Partial; - render?: (props: ActionModalProps) => ReactNode; - }; -} diff --git a/packages/react-celo/src/react-celo-reducer.ts b/packages/react-celo/src/react-celo-reducer.ts index 6d4f249f..54a012c7 100644 --- a/packages/react-celo/src/react-celo-reducer.ts +++ b/packages/react-celo/src/react-celo-reducer.ts @@ -1,10 +1,7 @@ import { CeloTokenContract } from '@celo/contractkit/lib/base'; -import { UnauthenticatedConnector } from './connectors'; -import { localStorageKeys } from './constants'; import { Connector, Dapp, Maybe, Network, Theme } from './types'; -import { clearPreviousConfig } from './utils/helpers'; -import localStorage from './utils/localStorage'; +import { getApplicationLogger } from './utils/logger'; export function celoReactReducer( state: ReducerState, @@ -21,11 +18,6 @@ export function celoReactReducer( if (action.payload === state.address) { return state; } - if (action.payload) { - localStorage.setItem(localStorageKeys.lastUsedAddress, action.payload); - } else { - localStorage.removeItem(localStorageKeys.lastUsedAddress); - } return { ...state, address: action.payload, @@ -34,51 +26,39 @@ export function celoReactReducer( if (action.payload === state.network) { return state; } - localStorage.setItem( - localStorageKeys.lastUsedNetwork, - action.payload.name - ); return { ...state, network: action.payload, }; - - case 'setConnector': - localStorage.removeItem(localStorageKeys.lastUsedAddress); - return { - ...state, - connector: action.payload, - connectorInitError: null, - address: null, - }; + case 'setNetworkByName': { + const network = state.networks.find((net) => net.name === action.payload); + if (network) { + return { ...state, network }; + } + return state; + } case 'setFeeCurrency': if (action.payload === state.feeCurrency) { return state; } - localStorage.setItem( - localStorageKeys.lastUsedFeeCurrency, - action.payload - ); return { ...state, feeCurrency: action.payload }; case 'initialisedConnector': { - const newConnector = action.payload; - const address = newConnector.kit.connection.defaultAccount ?? null; - if (address) { - localStorage.setItem(localStorageKeys.lastUsedAddress, address); - } return { ...state, connector: action.payload, - address, }; } - - case 'destroy': - clearPreviousConfig(); + case 'connect': { + const network = state.networks.find( + (net) => net.name === action.payload.networkName + ); + return { ...state, address: action.payload.address, network: network! }; + } + case 'disconnect': return { ...state, address: null, - connector: new UnauthenticatedConnector(state.network), + // connector is overwritten by the disconnect method init of a new Unauthenticated Connector, so no need to do here }; default: @@ -94,10 +74,9 @@ export function celoReactReducer( [key]: action.payload, }; } else { - console.error( - new Error( - `Unrecognized action type ${action.type} in celoReactReducer` - ) + getApplicationLogger().error( + '[reducer]', + new Error(`Unrecognized action type ${action.type}`) ); } return state; @@ -106,6 +85,9 @@ export function celoReactReducer( export interface ReducerState { connector: Connector; + /** + * Initialisation error, if applicable. + */ connectorInitError: Maybe; dapp: Dapp; network: Network; @@ -114,7 +96,6 @@ export interface ReducerState { address: Maybe; feeCurrency: CeloTokenContract; theme: Maybe; - connectionCallback: Maybe<(connector: Connector | false) => void>; } @@ -127,7 +108,9 @@ type SetActions = { export interface ActionsMap extends SetActions { decrementPendingActionCount: undefined; initialisedConnector: Connector; - destroy: undefined; + disconnect: undefined; + connect: { address: string; networkName: string }; + setNetworkByName: string; } // This converts the `ActionsMap` into a union of possible actions diff --git a/packages/react-celo/src/screens/cew.tsx b/packages/react-celo/src/screens/cew.tsx index b00192ec..2ae68ca6 100644 --- a/packages/react-celo/src/screens/cew.tsx +++ b/packages/react-celo/src/screens/cew.tsx @@ -4,9 +4,9 @@ import ConnectorScreen from '../components/connector-screen'; import Spinner from '../components/spinner'; import { CeloExtensionWalletConnector } from '../connectors'; import { PROVIDERS } from '../constants'; +import useTheme from '../hooks/use-theme'; import { useCeloInternal } from '../use-celo'; import cls from '../utils/tailwind'; -import useTheme from '../utils/useTheme'; import { ConnectorProps } from '.'; const styles = cls({ diff --git a/packages/react-celo/src/screens/coinbase.tsx b/packages/react-celo/src/screens/coinbase.tsx new file mode 100644 index 00000000..5a5318d5 --- /dev/null +++ b/packages/react-celo/src/screens/coinbase.tsx @@ -0,0 +1,80 @@ +import React from 'react'; + +import ConnectorScreen from '../components/connector-screen'; +import Spinner from '../components/spinner'; +import { PROVIDERS } from '../constants'; +import { useCoinbaseWalletConnector } from '../hooks/use-coinbase-wallet-connector'; +import useTheme from '../hooks/use-theme'; +import cls from '../utils/tailwind'; +import { ConnectorProps } from '.'; + +const styles = cls({ + container: ` + tw-my-8 + tw-flex + tw-flex-col + tw-items-center + tw-justify-center + grid + tw-gap-8 + tw-flex-grow`, + error: ` + tw-text-md + tw-pb-4`, + spinnerContainer: ` + tw-relative + tw-gap-2 + tw-items-center + tw-flex + tw-flex-col`, + disclaimer: ` + tw-text-center + tw-text-sm`, + button: ` + tw-mt-6 + tw-px-4 + tw-py-2`, +}); + +const provider = PROVIDERS['Coinbase Wallet']; +export const CoinbaseWallet = ({ onSubmit }: ConnectorProps) => { + const theme = useTheme(); + const { error } = useCoinbaseWalletConnector(onSubmit); + + let content: React.ReactElement; + + if (error) { + content = ( +

+ {error.message} +

+ ); + } else if (provider.canConnect()) { + content = ( +
+ +

+ No pop-up? Check your if your Coinbase Wallet extension is unlocked. +

+
+ ); + } else { + content = ( +
+

+ {provider.name} not detected. +
+ Are you sure it is installed in this browser? +

+
+ ); + } + + return ( + {content}
} + footer={{ name: 'Coinbase Wallet', url: '' }} + /> + ); +}; diff --git a/packages/react-celo/src/screens/index.ts b/packages/react-celo/src/screens/index.ts index 714ea2d2..bcb45e1c 100644 --- a/packages/react-celo/src/screens/index.ts +++ b/packages/react-celo/src/screens/index.ts @@ -1,12 +1,15 @@ import { SupportedProviders } from '../constants'; import { Connector, WalletConnectProvider } from '../types'; import { CeloExtensionWallet } from './cew'; +import { CoinbaseWallet } from './coinbase'; import { Ledger } from './ledger'; import { MetaMaskOrInjectedWallet } from './metamask'; import { PrivateKey } from './private-key'; import { WalletConnect } from './wallet-connect'; -export const defaultScreens = { +export const defaultScreens: { + [K in SupportedProviders]: React.FC; +} = { [SupportedProviders.Valora]: WalletConnect, [SupportedProviders.MetaMask]: MetaMaskOrInjectedWallet, [SupportedProviders.WalletConnect]: WalletConnect, @@ -18,9 +21,10 @@ export const defaultScreens = { [SupportedProviders.Steakwallet]: WalletConnect, [SupportedProviders.Injected]: MetaMaskOrInjectedWallet, [SupportedProviders.PrivateKey]: PrivateKey, + [SupportedProviders.CoinbaseWallet]: CoinbaseWallet, }; export type ConnectorProps = { onSubmit: (connector: Connector) => void; - provider?: WalletConnectProvider; + provider: WalletConnectProvider; }; diff --git a/packages/react-celo/src/screens/ledger.tsx b/packages/react-celo/src/screens/ledger.tsx index f4a00f55..1ec5eff3 100644 --- a/packages/react-celo/src/screens/ledger.tsx +++ b/packages/react-celo/src/screens/ledger.tsx @@ -4,10 +4,10 @@ import Button from '../components/button'; import ConnectorScreen from '../components/connector-screen'; import Spinner from '../components/spinner'; import { LedgerConnector } from '../connectors'; +import { useIsMounted } from '../hooks/use-is-mounted'; +import useTheme from '../hooks/use-theme'; import { useCeloInternal } from '../use-celo'; import cls from '../utils/tailwind'; -import { useIsMounted } from '../utils/useIsMounted'; -import useTheme from '../utils/useTheme'; import { ConnectorProps } from '.'; const styles = cls({ diff --git a/packages/react-celo/src/screens/metamask.tsx b/packages/react-celo/src/screens/metamask.tsx index c59df9b3..96a2ecfe 100644 --- a/packages/react-celo/src/screens/metamask.tsx +++ b/packages/react-celo/src/screens/metamask.tsx @@ -4,11 +4,11 @@ import { isMobile } from 'react-device-detect'; import Button from '../components/button'; import ConnectorScreen from '../components/connector-screen'; import Spinner from '../components/spinner'; -import { useInjectedConnector } from '../connectors/useMetaMaskConnector'; import { PROVIDERS } from '../constants'; +import { useInjectedConnector } from '../hooks/use-injected-connector'; +import useTheme from '../hooks/use-theme'; import { isEthereumFromMetamask } from '../utils/ethereum'; import cls from '../utils/tailwind'; -import useTheme from '../utils/useTheme'; import { ConnectorProps } from '.'; const styles = cls({ diff --git a/packages/react-celo/src/screens/placeholder.tsx b/packages/react-celo/src/screens/placeholder.tsx index e0fc5223..b99f966e 100644 --- a/packages/react-celo/src/screens/placeholder.tsx +++ b/packages/react-celo/src/screens/placeholder.tsx @@ -1,9 +1,9 @@ import React from 'react'; import ConnectorScreen from '../components/connector-screen'; +import useTheme from '../hooks/use-theme'; import { useCeloInternal } from '../use-celo'; import cls from '../utils/tailwind'; -import useTheme from '../utils/useTheme'; const styles = cls({ list: ` diff --git a/packages/react-celo/src/screens/private-key.tsx b/packages/react-celo/src/screens/private-key.tsx index 4ed20aa4..88901e66 100644 --- a/packages/react-celo/src/screens/private-key.tsx +++ b/packages/react-celo/src/screens/private-key.tsx @@ -3,9 +3,9 @@ import React, { useCallback, useState } from 'react'; import Button from '../components/button'; import ConnectorScreen from '../components/connector-screen'; import { PrivateKeyConnector } from '../connectors'; -import { useCelo } from '../use-celo'; +import useTheme from '../hooks/use-theme'; +import { useCeloInternal } from '../use-celo'; import cls from '../utils/tailwind'; -import useTheme from '../utils/useTheme'; import { ConnectorProps } from '.'; const styles = cls({ @@ -23,24 +23,25 @@ const styles = cls({ tw-py-2 tw-font-mono`, button: ` - tw-mt-3 - tw-px-4 + tw-mt-3 + tw-px-4 tw-py-2`, }); export const PrivateKey = ({ onSubmit }: ConnectorProps) => { const theme = useTheme(); const [value, setValue] = useState(''); - const { network, feeCurrency } = useCelo(); + const { network, feeCurrency, initConnector } = useCeloInternal(); - const handleSubmit = useCallback(() => { + const handleSubmit = useCallback(async () => { if (!value) { return; } const connector = new PrivateKeyConnector(network, value, feeCurrency); + await initConnector(connector); void onSubmit(connector); - }, [feeCurrency, network, value, onSubmit]); + }, [feeCurrency, network, value, onSubmit, initConnector]); return ( void; - provider?: WalletConnectProvider; + provider: WalletConnectProvider; } export const WalletConnect = ({ onSubmit, provider }: Props) => { @@ -65,10 +65,10 @@ export const WalletConnect = ({ onSubmit, provider }: Props) => { const { uri, error, loading, retry } = useWalletConnectConnector( onSubmit, isMobile, - provider?.getLink && - ((uri: string) => - provider.getLink!(uri, isMobile ? Platform.Mobile : Platform.Desktop)), - provider?.walletConnectId + provider.walletConnectId, + provider.getLink && + ((URI: string) => + provider.getLink!(URI, isMobile ? Platform.Mobile : Platform.Desktop)) ); const onClickPlatform = useCallback( diff --git a/packages/react-celo/src/styles.css b/packages/react-celo/src/styles.css index dd67c954..3fb4b69c 100644 --- a/packages/react-celo/src/styles.css +++ b/packages/react-celo/src/styles.css @@ -433,9 +433,7 @@ Spinner @tailwind utilities; .react-celo-modal-open-body { - min-height: 100vh; - /* mobile viewport bug fix */ - min-height: -webkit-fill-available; + height: -webkit-fill-available; overflow: hidden; } @@ -443,6 +441,15 @@ Spinner .react-celo-modal-open-html { height: -webkit-fill-available; } + .rc-tray-ios-fix { + height: -webkit-fill-available; + } +} +@media (min-width: 900px) { + .react-celo-modal-open-body { + min-height: 100vh; + max-height: 100vh; + } } .tw-drop-shadow { diff --git a/packages/react-celo/src/types.ts b/packages/react-celo/src/types.ts index 482e4398..511a316f 100644 --- a/packages/react-celo/src/types.ts +++ b/packages/react-celo/src/types.ts @@ -2,6 +2,7 @@ import { CeloTokenContract } from '@celo/contractkit/lib/base'; import { MiniContractKit } from '@celo/contractkit/lib/mini-kit'; import React from 'react'; +import { ConnectorEvents, EventsMap } from './connectors/common'; import { Platform, Priorities, WalletTypes } from './constants'; export type Maybe = T | null | undefined; @@ -39,7 +40,7 @@ export interface CeloNetwork extends Network { */ export interface Provider { name: string; - type: WalletTypes; + type: Omit; description: string | JSX.Element; icon: string | React.FC>; canConnect: () => boolean; @@ -49,8 +50,9 @@ export interface Provider { } export interface WalletConnectProvider extends Provider { - walletConnectId?: string; - supportedPlatforms?: Platform[]; + type: WalletTypes.WalletConnect; + walletConnectId: string; + supportedPlatforms: Platform[]; getLink?: (uri: string, platform: Platform) => string | false; } @@ -60,18 +62,34 @@ export interface WalletConnectProvider extends Provider { export interface Connector { kit: MiniContractKit; type: WalletTypes; - account: Maybe; feeCurrency: CeloTokenContract; + /** + * `initialised` indicates if the connector + * has been fully loaded. + */ initialised: boolean; + /** + * `initialise` loads the connector + * and saves it to local storage. + */ initialise: () => Promise | this; close: () => Promise | void; + on(e: E, fn: (arg: EventsMap[E]) => void): void; updateFeeCurrency?: (token: CeloTokenContract) => Promise; supportsFeeCurrency: () => boolean; getDeeplinkUrl?: (uri: string) => string | false; - updateKitWithNetwork?: (network: Network) => Promise; - onNetworkChange?: (callback: (chainId: number) => void) => void; - onAddressChange?: (callback: (address: Maybe) => void) => void; - persist: () => void; + /** + * This isn't the method you want + * used when wallet changes chain. To change chain from app use startNetworkChangeFromApp() + */ + continueNetworkUpdateFromWallet?: (network: Network) => void; + + /** + * when network is changed from dapp side + * connectors / wallets that dont support this should do X + * connectors that do support MUST emit a NETWORK_CHANGED event + */ + startNetworkChangeFromApp: (network: Network) => Promise | void; } /** diff --git a/packages/react-celo/src/use-celo-methods.ts b/packages/react-celo/src/use-celo-methods.ts index 8d92273f..e12ccf01 100644 --- a/packages/react-celo/src/use-celo-methods.ts +++ b/packages/react-celo/src/use-celo-methods.ts @@ -3,102 +3,53 @@ import { MiniContractKit } from '@celo/contractkit/lib/mini-kit'; import { useCallback } from 'react'; import { isMobile } from 'react-device-detect'; -import { CONNECTOR_TYPES } from './connectors'; -import { - localStorageKeys, - STATIC_NETWORK_WALLETS, - WalletTypes, -} from './constants'; +import { UnauthenticatedConnector } from './connectors'; +import { STATIC_NETWORK_WALLETS, WalletTypes } from './constants'; import { ContractCacheBuilder, useContractsCache, -} from './ContractCacheBuilder'; -import { Dispatcher } from './react-celo-provider'; -import defaultTheme from './theme/default'; +} from './hooks/use-contracts-cache'; +import { Dispatcher } from './react-celo-provider-state'; import { Connector, Network, Theme } from './types'; -import { RGBToHex } from './utils/helpers'; +import { contrastCheck, fixTheme } from './utils/colors'; +import { getApplicationLogger } from './utils/logger'; +import networkWatcher from './utils/network-watcher'; +import persistor from './utils/persistor'; +import { updater } from './utils/updater'; + +interface CeloMethodsInput { + connector: Connector; + networks: Network[]; + network: Network; +} export function useCeloMethods( - { - connector, - networks, - network, - }: { - connector: Connector; - networks: Network[]; - network: Network; - }, + { connector, networks, network }: CeloMethodsInput, dispatch: Dispatcher, buildContractsCache?: ContractCacheBuilder ): CeloMethods { - const destroy = useCallback(async () => { - await connector.close(); - dispatch('destroy'); - }, [dispatch, connector]); - const initConnector = useCallback( async (nextConnector: Connector) => { try { + // need to set the event listeners here before initialise() + updater(nextConnector, dispatch); + persistor(nextConnector); + networkWatcher(nextConnector, networks); const initialisedConnector = await nextConnector.initialise(); dispatch('initialisedConnector', initialisedConnector); - - // If the new wallet already has a specific network it's - // using then we should go with that one. - const netId = - await initialisedConnector.kit.connection.web3.eth.net.getId(); - const newNetwork = networks.find((n) => netId === n.chainId); - if (newNetwork !== network) { - dispatch('setNetwork', network); - } - - // This happens if the network changes on the wallet side - // and we need to update what network we're storing - // accordingly. - initialisedConnector.onNetworkChange?.((chainId) => { - const network = networks.find((n) => n.chainId === chainId); - if (netId === chainId || !network) return; - - // TODO: We should probably throw an error if we can't find the new chainId - - if (network) { - dispatch('setNetwork', network); - initialisedConnector.updateKitWithNetwork && - initialisedConnector - .updateKitWithNetwork(network) - .then(() => { - dispatch('initialisedConnector', initialisedConnector); - }) - .catch((e) => { - console.error( - '[react-celo] Error switching network', - nextConnector.type, - e - ); - const error = - e instanceof Error - ? e - : new Error( - `Failed to initialise connector with ${network.name}` - ); - dispatch('setConnectorInitError', error); - throw e; - }); - } - }); - initialisedConnector.onAddressChange?.((address) => { - dispatch('setAddress', address); - }); } catch (e) { if (typeof e === 'symbol') { - console.info( - '[react-celo] Ignoring error initializing connector with reason', + getApplicationLogger().debug( + '[initConnector]', + 'Ignoring error initializing connector with reason', e.description ); throw e; } - console.error( - '[react-celo] Error initializing connector', + getApplicationLogger().error( + '[initConnector]', + 'Error initializing connector', nextConnector.type, e ); @@ -109,35 +60,31 @@ export function useCeloMethods( throw e; } }, - [dispatch, network, networks] + [dispatch, networks] ); + const disconnect = useCallback(async () => { + await connector.close(); + const passiveConnector = new UnauthenticatedConnector(network); + await initConnector(passiveConnector); + }, [connector, network, initConnector]); // This is just to be used to for users to explicitly change // the network. It doesn't work for all wallets. const updateNetwork = useCallback( async (newNetwork: Network) => { + getApplicationLogger().debug( + '[updateNetwork]', + newNetwork, + connector.type + ); if (STATIC_NETWORK_WALLETS.includes(connector.type)) { throw new Error( "The connected wallet's network must be changed from the wallet." ); } - if (network === newNetwork) return; - if (connector.initialised) { - const connectorArgs = JSON.parse( - localStorage.getItem(localStorageKeys.lastUsedWalletArguments) || '[]' - ) as unknown[]; - await connector.close(); - const ConnectorConstructor = CONNECTOR_TYPES[connector.type]; - const newConnector = new ConnectorConstructor( - newNetwork, - ...connectorArgs - ); - await initConnector(newConnector); - } - - dispatch('setNetwork', newNetwork); + await connector.startNetworkChangeFromApp(newNetwork); }, - [dispatch, connector, network, initConnector] + [connector] ); const connect = useCallback(async (): Promise => { @@ -173,7 +120,8 @@ export function useCeloMethods( dispatch('setFeeCurrency', newFeeCurrency); } } catch (error) { - console.warn( + getApplicationLogger().warn( + '[updateFeeCurrency]', 'updating Fee Currency not supported by this wallet or network', error ); @@ -185,23 +133,12 @@ export function useCeloMethods( const updateTheme = useCallback( (theme: Theme | null) => { if (!theme) return dispatch('setTheme', null); - Object.entries(theme).forEach(([key, value]: [string, string]) => { - if (!(key in defaultTheme.light)) { - console.warn(`Theme key ${key} is not valid.`); - } - const _key = key as keyof Theme; - if (value.startsWith('rgb')) { - theme[_key] = RGBToHex(value); - console.warn( - `RGB values not officially supported, but were translated to hex (${value} -> ${theme[_key]})` - ); - } else if (!value.startsWith('#')) { - theme[_key] = `#${value}`; - console.warn( - `Malformed hex value was missing # (${value} -> ${theme[_key]})` - ); - } - }); + + if (process.env.NODE_ENV !== 'production') { + fixTheme(theme); + contrastCheck(theme); + } + dispatch('setTheme', theme); }, [dispatch] @@ -242,7 +179,8 @@ export function useCeloMethods( }, [dispatch]); return { - destroy, + destroy: disconnect, + disconnect, initConnector, resetInitError, updateNetwork, @@ -256,16 +194,67 @@ export function useCeloMethods( } export interface CeloMethods { - resetInitError: () => void; + /** + * @deprecated use `disconnect` (same behavior better name) + * + */ destroy: () => Promise; - initConnector: (connector: Connector) => Promise; - updateNetwork: (network: Network) => Promise; + /** + * `disconnect` closes the connection to the wallet and reses state + */ + disconnect: () => Promise; + /** + * `updateNetwork` changes the network used in the wallet. + * + * Note: _not compatible with all wallets_ + */ + updateNetwork: (network: Network, forceUpdate?: boolean) => Promise; + /** + * `connect` initiates the connection to a wallet and + * opens a modal from which the user can choose a + * wallet to connect to. + */ connect: () => Promise; + /** + * `getConnectedKit` gets the connected instance of MiniContractKit. + * If the user is not connected, this opens up the connection modal. + */ getConnectedKit: () => Promise; + /** + * `performActions` is a helper function for handling any interaction with a Celo wallet. + * Perform action will: + * - open the action modal + * - handle multiple transactions in order + */ performActions: ( ...operations: ((kit: MiniContractKit) => unknown | Promise)[] ) => Promise; + /** + * `updateFeeCurrency` updates the currency that will be used + * in future transactions. + * + * Note: _not compatible with all wallets_ + */ updateFeeCurrency: (newFeeCurrency: CeloTokenContract) => Promise; - contractsCache?: undefined | unknown; + + contractsCache?: unknown; + /** + * `updateTheme` programmaticaly updates the theme used in the + * wallet connection modal. This is useful if you want to give + * the user the option to change the theme. + */ updateTheme: (theme: Theme | null) => void; + /** + * @internal + * resetInitError cleans up the error that occurred + * when trying to initialize a wallet connector. + */ + resetInitError: () => void; + /** + * @internal + * + * `initConnector` is used to initialize a connector + * for the wallet chosen by the user. + */ + initConnector: (connector: Connector) => Promise; } diff --git a/packages/react-celo/src/use-celo.tsx b/packages/react-celo/src/use-celo.tsx index 934b1efd..dbe70a4f 100644 --- a/packages/react-celo/src/use-celo.tsx +++ b/packages/react-celo/src/use-celo.tsx @@ -1,119 +1,101 @@ -import { CeloTokenContract } from '@celo/contractkit/lib/base'; -import { MiniContractKit } from '@celo/contractkit/lib/mini-kit'; - -import { WalletTypes } from './constants'; import { useReactCeloContext } from './react-celo-provider'; -import { Connector, Dapp, Maybe, Network, Theme } from './types'; - -export interface UseCelo { - dapp: Dapp; - kit: MiniContractKit; - walletType: WalletTypes; - feeCurrency: CeloTokenContract; +import { ReducerState } from './react-celo-reducer'; +import { Maybe } from './types'; +import { CeloMethods } from './use-celo-methods'; - /** - * Name of the account. - */ - account: Maybe; +type SomeReducerStateProps = Pick< + ReducerState, + 'dapp' | 'address' | 'network' | 'feeCurrency' +>; - address: Maybe; - connect: () => Promise; - destroy: () => Promise; - network: Network; - networks: readonly Network[]; - updateNetwork: (network: Network) => Promise; - updateFeeCurrency: (newFeeCurrency: CeloTokenContract) => Promise; - updateTheme: (theme: Theme | null) => void; - supportsFeeCurrency: boolean; - /** - * Helper function for handling any interaction with a Celo wallet. Perform action will - * - open the action modal - * - handle multiple transactions in order - */ - performActions: ( - ...operations: ((kit: MiniContractKit) => unknown | Promise)[] - ) => Promise; +type DerivedFromReducerStateProps = { + networks: readonly ReducerState['network'][]; + initError: ReducerState['connectorInitError']; +}; - /** - * Whether or not the connector has been fully loaded. - */ - initialised: boolean; - /** - * Initialisation error, if applicable. - */ - initError: Maybe; +type SomeReducerConnectorProps = Pick< + ReducerState['connector'], + 'kit' | 'initialised' +>; - /** - * Gets the connected instance of MiniContractKit. - * If the user is not connected, this opens up the connection modal. - */ - getConnectedKit: () => Promise; +type DerivedFromConnectorProps = { + supportsFeeCurrency: ReturnType< + ReducerState['connector']['supportsFeeCurrency'] + >; + walletType: ReducerState['connector']['type']; +}; - contractsCache?: unknown; -} +type SomeCeloMethods = Omit; +export type UseCelo = SomeReducerStateProps & + DerivedFromReducerStateProps & + SomeReducerConnectorProps & + DerivedFromConnectorProps & + SomeCeloMethods & { account: Maybe }; export function useCelo(): UseCelo { - const [ - { - dapp, - connector, - connectorInitError, - address, - network, - feeCurrency, - networks, - }, - _dispatch, - { - destroy, - updateNetwork, - connect, - getConnectedKit, - performActions, - updateFeeCurrency, - contractsCache, - updateTheme, - }, - ] = useReactCeloContext(); + const [reducerState, _dispatch, celoMethods] = useReactCeloContext(); - return { + const { + dapp, address, + network, + feeCurrency, + connectorInitError, + networks, + connector, + } = reducerState; + + const { + destroy, + disconnect, + updateNetwork, + connect, + getConnectedKit, + performActions, + updateFeeCurrency, + contractsCache, + updateTheme, + } = celoMethods; + + return { dapp, + address, // The account address network, + feeCurrency, + initError: connectorInitError, // Copy to ensure any accidental mutations dont affect global state networks: networks.map((net) => ({ ...net })), - updateNetwork, + kit: connector.kit, - contractsCache: contractsCache as CC, - walletType: connector.type, - account: connector.account, + // the wallet address from Account.getWalletAddress => The address at which the account expects to receive transfers. + // If it's empty/0x0, the account indicates that an address exchange should be initiated with the dataEncryptionKey + /* + * @deprecated this will likely be removed in favor of just address + */ + account: connector.kit.connection.defaultAccount, initialised: connector.initialised, - feeCurrency, - updateFeeCurrency, + walletType: connector.type, supportsFeeCurrency: connector.supportsFeeCurrency(), - performActions, - getConnectedKit, - connect, destroy, + disconnect, + updateNetwork, + connect, + getConnectedKit, + performActions, + updateFeeCurrency, + contractsCache: contractsCache as CC, updateTheme, - - initError: connectorInitError, }; } /** - * * @deprecated Use the alias {@link useCelo} hook instead. */ export const useContractKit = useCelo; -interface UseCeloInternal extends UseCelo { - connectionCallback: Maybe<(connector: Connector | false) => void>; - initConnector: (connector: Connector) => Promise; - pendingActionCount: number; - theme: Maybe; - resetInitError: () => void; -} +type UseCeloInternal = UseCelo & + Pick & + Pick; /** * @internal useCelo with internal methods exposed. Package use only. diff --git a/packages/react-celo/src/utils/colors.ts b/packages/react-celo/src/utils/colors.ts new file mode 100644 index 00000000..5072ace4 --- /dev/null +++ b/packages/react-celo/src/utils/colors.ts @@ -0,0 +1,210 @@ +import defaultTheme from '../theme/default'; +import { Theme } from '../types'; +import { getApplicationLogger } from './logger'; + +const minmax = (value: number, lowerBound = 0, higherBound = 1) => + Math.max(lowerBound, Math.min(higherBound, value)); + +const round2 = (num: number) => Math.round((num + Number.EPSILON) * 100) / 100; + +enum Format { + Hex, + Rgb, +} +export class Color { + r: number; + b: number; + g: number; + a: number | null = null; + + constructor(color: string) { + if (color.startsWith('#')) { + if (color.length === 4) { + // eg: #fff, #000 + this.r = parseInt(color.slice(1, 2) + color.slice(1, 2), 16); + this.g = parseInt(color.slice(2, 3) + color.slice(2, 3), 16); + this.b = parseInt(color.slice(3, 4) + color.slice(3, 4), 16); + } else { + // eg: #ffffff, #000000 + this.r = parseInt(color.slice(1, 3), 16); + this.g = parseInt(color.slice(3, 5), 16); + this.b = parseInt(color.slice(5, 7), 16); + + if (color.length === 9) { + // eg: #ffffff80, #000000ff + this.a = round2(minmax(parseInt(color.slice(7, 9), 16) / 255)); + } + } + } else if (color.startsWith('rgb')) { + // eg: rgb(0, 0, 0) + const values = color.split('(')[1].split(')')[0].split(','); + this.r = parseInt(values[0].trim(), 10); + this.g = parseInt(values[1].trim(), 10); + this.b = parseInt(values[2].trim(), 10); + + if (values[3]) { + // eg: rgba(0, 0, 0, 1) + this.a = round2(minmax(parseFloat(values[3]))); + } + + getApplicationLogger().warn( + '[colors]', + `RGB(A) values not officially supported, but were translated to hex (${color} -> ${this.toHex()})` + ); + } else if (color.startsWith('hsl')) { + // eg: hsl(100, 50%, 75%) + const values = color.split('(')[1].split(')')[0].split(','); + const h = parseInt(values[0].trim().replace('deg', ''), 10); + const s = parseInt(values[1].trim().replace('%', ''), 10); + let l = parseFloat(values[2].trim().replace('%', '')); + + if (l > 1) { + l /= 100; + } + + const a = (s * Math.min(l, 1 - l)) / 100; + const f = (n: number) => { + const k = (n + h / 30) % 12; + const unroundedColor = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); + return Math.round(255 * unroundedColor + Number.EPSILON); + }; + this.r = f(0); + this.g = f(8); + this.b = f(4); + + if (values[3]) { + // eg: hsla(100, 50%, 75%, 0.8) + this.a = round2(minmax(parseFloat(values[3]))); + } + + getApplicationLogger().warn( + '[colors]', + `HSL(A) values not officially supported, but were translated to hex (${color} -> ${this.toHex()})` + ); + } else { + throw new Error(`Malformed color (${color})`); + } + } + + opacity(alpha?: number) { + if (alpha != null) { + this.a = round2(minmax(alpha)); + } + return this; + } + + toRGB() { + if (this.a !== null) { + return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})`; + } + return `rgb(${this.r}, ${this.g}, ${this.b})`; + } + + toHex() { + const hex = [ + this.r.toString(16).padStart(2, '0'), + this.g.toString(16).padStart(2, '0'), + this.b.toString(16).padStart(2, '0'), + ].join(''); + + if (this.a !== null) { + // 0 <= this.a <= 1 + const alpha = Math.round(this.a * 256) + .toString(16) + .padStart(2, '0'); + return `#${hex}${alpha}`; + } + + return `#${hex}`; + } + + toString(format: Format) { + switch (format) { + case Format.Hex: + return this.toHex(); + case Format.Rgb: + return this.toRGB(); + } + } +} + +export function hexToRGB(hex: string, alpha?: number): string { + return new Color(hex).opacity(alpha).toRGB(); +} + +export function RGBToHex(rgba: string): string { + return new Color(rgba).toHex(); +} + +// https://en.wikipedia.org/wiki/Relative_luminance#Relative_luminance_and_.22gamma_encoded.22_colorspaces +export function luminance(color: Color) { + const a = [color.r, color.g, color.b].map((v) => { + v /= 255; + return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); + }); + return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; +} + +export function contrast(a: Color, b: Color) { + const lum1 = luminance(a); + const lum2 = luminance(b); + const brightest = Math.max(lum1, lum2); + const darkest = Math.min(lum1, lum2); + + return round2((brightest + 0.05) / (darkest + 0.05)); +} + +export function contrastCheck(theme: Theme) { + // minimal recommended contrast ratio is 4. + // or 3 for larger font-sizes + + const textToBg = contrast(new Color(theme.background), new Color(theme.text)); + if (textToBg <= 4) { + getApplicationLogger().warn( + '[colors]', + `Potential accessibility issue between text and background colors (${textToBg})` + ); + } + const textSecondaryToBg = contrast( + new Color(theme.background), + new Color(theme.textSecondary) + ); + if (textSecondaryToBg <= 4) { + getApplicationLogger().warn( + '[colors]', + `Potential accessibility issue between textSecondary and background colors (${textSecondaryToBg})` + ); + } + const primaryToSecondary = contrast( + new Color(theme.background), + new Color(theme.secondary) + ); + if (primaryToSecondary <= 3) { + getApplicationLogger().warn( + '[colors]', + `Potential accessibility issue between primary and secondary colors (${primaryToSecondary})` + ); + } +} + +export function fixTheme(theme: Theme) { + Object.entries(theme).forEach(([key, value]: [string, string]) => { + if (!(key in defaultTheme.light)) { + getApplicationLogger().error( + '[colors]', + `Theme key ${key} is not valid.` + ); + } + const _key = key as keyof Theme; + try { + const color = new Color(value); + theme[_key] = color.toHex(); + } catch (e) { + theme[_key] = '#FF0000'; + getApplicationLogger().error( + '[colors]', + `Could not parse theme. ${_key} with value ${value}. Replaced it with red!` + ); + } + }); +} diff --git a/packages/react-celo/src/utils/fetchWCWallets.ts b/packages/react-celo/src/utils/fetch-wallet-connect-wallets.ts similarity index 100% rename from packages/react-celo/src/utils/fetchWCWallets.ts rename to packages/react-celo/src/utils/fetch-wallet-connect-wallets.ts diff --git a/packages/react-celo/src/utils/get-initial-network.ts b/packages/react-celo/src/utils/get-initial-network.ts new file mode 100644 index 00000000..09249450 --- /dev/null +++ b/packages/react-celo/src/utils/get-initial-network.ts @@ -0,0 +1,42 @@ +import { Network } from '../types'; +import { getApplicationLogger } from './logger'; + +// will look for a network searching first in local storage, then based on what was given. +export function getInitialNetwork( + networks: Network[], + defaultNetwork?: string, + passedNetwork?: Network, + storedNetwork?: string | null +) { + if (process.env.NODE_ENV !== 'production' && passedNetwork) { + getApplicationLogger().warn( + 'The `network` prop on CeloProvider has been deprecated, use `defaultNetwork`' + ); + } + const network = networks.find((net) => { + if (storedNetwork) { + net.name === storedNetwork; + } + + // TODO:#246 remove when network prop is removed + if (passedNetwork) { + return net.name === passedNetwork.name; + } else { + return net.name === defaultNetwork; + } + }); + + if (!network) { + const name = defaultNetwork || passedNetwork?.name || 'unknown'; + throw new Error( + `[react-celo] Could not find 'defaultNetwork' (${name}) in 'networks'. 'defaultNetwork' must equal 'network.name' on one of the 'networks' passed to CeloProvider.` + ); + } + + // ensure for now that we return the original + if (network.name === passedNetwork?.name) { + return passedNetwork; + } + + return network; +} diff --git a/packages/react-celo/src/utils/helpers.ts b/packages/react-celo/src/utils/helpers.ts index 1326e7ca..91c9067c 100644 --- a/packages/react-celo/src/utils/helpers.ts +++ b/packages/react-celo/src/utils/helpers.ts @@ -1,100 +1,31 @@ import { CeloContract, CeloTokenContract } from '@celo/contractkit/lib/base'; +import { useEffect } from 'react'; -import { CONNECTOR_TYPES, UnauthenticatedConnector } from '../connectors'; -import { localStorageKeys, WalletTypes } from '../constants'; -import { Connector, Maybe, Network } from '../types'; -import localStorage from './localStorage'; +import { localStorageKeys } from '../constants'; +import { Maybe } from '../types'; +import { getTypedStorageKey } from './local-storage'; -export const loadPreviousConfig = ( - defaultNetworkProp: Network, - defaultFeeCurrencyProp: CeloTokenContract, - networks: Network[] -): { - address: Maybe; - network: Maybe; - connector: Connector; - feeCurrency: Maybe; -} => { - let lastUsedNetworkName: Maybe = null; - let lastUsedAddress: Maybe = null; - let lastUsedWalletType: WalletTypes = WalletTypes.Unauthenticated; - let lastUsedWalletArguments: unknown[] = []; - let lastUsedFeeCurrency: CeloContract = defaultFeeCurrencyProp; - if (typeof localStorage !== 'undefined') { - const localLastUsedNetworkName = localStorage.getItem( - localStorageKeys.lastUsedNetwork - ); - if (localLastUsedNetworkName) { - lastUsedNetworkName = localLastUsedNetworkName; - } - - lastUsedAddress = localStorage.getItem(localStorageKeys.lastUsedAddress); - - const localLastUsedWalletType = localStorage.getItem( - localStorageKeys.lastUsedWalletType - ); - if (localLastUsedWalletType && localLastUsedWalletType in WalletTypes) { - lastUsedWalletType = localLastUsedWalletType as WalletTypes; - } - - const localLastUsedWalletArguments = localStorage.getItem( - localStorageKeys.lastUsedWalletArguments - ); - - if (localLastUsedWalletArguments) { - try { - lastUsedWalletArguments = JSON.parse( - localLastUsedWalletArguments - ) as unknown[]; - } catch (e) { - lastUsedWalletArguments = []; - } - } - - const localLastUsedFeeCurrency = localStorage.getItem( - localStorageKeys.lastUsedFeeCurrency - ); - - if (isValidFeeCurrency(localLastUsedFeeCurrency)) { - lastUsedFeeCurrency = localLastUsedFeeCurrency as CeloTokenContract; - } - } +export function loadPreviousState(): { + address: string | null; + networkName: string | null; + feeCurrency: CeloTokenContract | null; +} { + const address = getTypedStorageKey(localStorageKeys.lastUsedAddress); + const networkName = getTypedStorageKey(localStorageKeys.lastUsedNetwork); - const lastUsedNetwork = networks.find((n) => n.name === lastUsedNetworkName); + const localLastUsedFeeCurrency = getTypedStorageKey( + localStorageKeys.lastUsedFeeCurrency + ); - let initialConnector: Connector; - if (lastUsedWalletType && lastUsedNetwork) { - try { - initialConnector = new CONNECTOR_TYPES[lastUsedWalletType]( - lastUsedNetwork, - lastUsedFeeCurrency, - ...lastUsedWalletArguments - ); - } catch (e) { - initialConnector = new UnauthenticatedConnector( - lastUsedNetwork || defaultNetworkProp - ); - } - } else { - initialConnector = new UnauthenticatedConnector( - lastUsedNetwork || defaultNetworkProp - ); - } + const feeCurrency = isValidFeeCurrency(localLastUsedFeeCurrency) + ? (localLastUsedFeeCurrency as CeloTokenContract) + : null; return { - address: lastUsedAddress, - network: lastUsedNetwork || null, - connector: initialConnector, - feeCurrency: lastUsedFeeCurrency, + address, + networkName, + feeCurrency, }; -}; - -export function clearPreviousConfig(): void { - Object.values(localStorageKeys).forEach((val) => { - if (val === localStorageKeys.lastUsedWalletId) return; - if (val === localStorageKeys.lastUsedWalletType) return; - localStorage.removeItem(val); - }); } export function isValidFeeCurrency(currency: Maybe): boolean { @@ -109,24 +40,17 @@ export function isValidFeeCurrency(currency: Maybe): boolean { } } -export function hexToRGB(hex: string, alpha?: number): string { - const r = parseInt(hex.slice(1, 3), 16); - const g = parseInt(hex.slice(3, 5), 16); - const b = parseInt(hex.slice(5, 7), 16); - - if (alpha) { - return `rgba(${r}, ${g}, ${b}, ${Math.max(0, Math.min(1, alpha))})`; - } else { - return `rgba(${r}, ${g}, ${b})`; - } -} - -export function RGBToHex(rgba: string): string { - const values = rgba.split('(')[1].split(')')[0]; - const r = parseInt(values[0]).toString(16); - const g = parseInt(values[1]).toString(16); - const b = parseInt(values[2]).toString(16); - const alpha = values[3] ? parseInt(values[3]).toString(16) : ''; - - return `#${r}${g}${b}${alpha}`; +export function useFixedBody(isOpen: boolean) { + useEffect(() => { + if (isOpen) { + const originalOverflow = document.body.style.overflow; + const originalPadding = document.body.style.paddingRight; + document.body.style.overflow = 'hidden'; + document.body.style.paddingRight = '15px'; + return () => { + document.body.style.overflow = originalOverflow; + document.body.style.paddingRight = originalPadding; + }; + } + }, [isOpen]); } diff --git a/packages/react-celo/src/utils/local-storage.ts b/packages/react-celo/src/utils/local-storage.ts new file mode 100644 index 00000000..801bf232 --- /dev/null +++ b/packages/react-celo/src/utils/local-storage.ts @@ -0,0 +1,114 @@ +import { CeloTokenContract } from '@celo/contractkit'; +import { WalletConnectWalletOptions } from '@celo/wallet-walletconnect-v1'; + +import { localStorageKeys, WalletTypes } from '../constants'; + +class MockedLocalStorage implements Storage { + private storage = new Map(); + + getItem(key: string): string | null { + if (this.storage.has(key)) { + this.storage.get(key) as string; + } + return null; + } + + key(index: number): string | null { + if (index < 0 || index >= this.length) { + return null; + } + + let i = 0; + for (const value of this.storage.values()) { + if (i === index) { + return value; + } + i += 1; + } + return null; + } + + setItem(key: string, value: string): void { + this.storage.set(key, value); + } + + removeItem(key: string): void { + this.storage.delete(key); + } + + clear(): void { + this.storage.clear(); + } + + get length(): number { + return this.storage.size; + } +} + +const localStorage = + typeof window === 'undefined' + ? new MockedLocalStorage() + : window.localStorage; + +type ParamType = { + [localStorageKeys.lastUsedAddress]: string; + [localStorageKeys.lastUsedNetwork]: string; + [localStorageKeys.lastUsedPrivateKey]: string; + [localStorageKeys.lastUsedWalletId]: string; + [localStorageKeys.lastUsedFeeCurrency]: CeloTokenContract; + [localStorageKeys.lastUsedIndex]: number; + [localStorageKeys.lastUsedWalletType]: WalletTypes; + [localStorageKeys.lastUsedWalletArguments]: []; +}; + +export function getTypedStorageKey(key: T) { + const item = localStorage.getItem(key); + if (key === localStorageKeys.lastUsedIndex && item) { + return Number(item) as ParamType[T]; + } + if (item) { + return item as ParamType[T]; + } + return null; +} + +export function setTypedStorageKey< + T extends localStorageKeys, + V extends ParamType[T] +>(key: T, value: V): void { + localStorage.setItem(key, value.toString()); +} + +export function removeLastUsedAddress() { + localStorage.removeItem(localStorageKeys.lastUsedAddress); +} + +export type WalletArgs = + | [string] + | [CeloTokenContract] + | [WalletConnectWalletOptions] + | [number] + | []; + +export function getLastUsedWalletArgs(): WalletArgs | null { + const args = localStorage.getItem(localStorageKeys.lastUsedWalletArguments); + if (args && args.length) { + const parsed = JSON.parse(args) as WalletArgs; + + return parsed; + } + + return null; +} + +export function clearPreviousConfig(): void { + Object.values(localStorageKeys).forEach((val) => { + if (val === localStorageKeys.lastUsedWalletId) return; + if (val === localStorageKeys.lastUsedWalletType) return; + localStorage.removeItem(val); + }); +} + +export function localStorageAvailable() { + return typeof localStorage !== 'undefined'; +} diff --git a/packages/react-celo/src/utils/localStorage.ts b/packages/react-celo/src/utils/localStorage.ts deleted file mode 100644 index 069a9233..00000000 --- a/packages/react-celo/src/utils/localStorage.ts +++ /dev/null @@ -1,48 +0,0 @@ -class MockedLocalStorage implements Storage { - private storage = new Map(); - - getItem(key: string): string | null { - if (this.storage.has(key)) { - this.storage.get(key) as string; - } - return null; - } - - key(index: number): string | null { - if (index < 0 || index >= this.length) { - return null; - } - - let i = 0; - for (const value of this.storage.values()) { - if (i === index) { - return value; - } - i += 1; - } - return null; - } - - setItem(key: string, value: string): void { - this.storage.set(key, value); - } - - removeItem(key: string): void { - this.storage.delete(key); - } - - clear(): void { - this.storage.clear(); - } - - get length(): number { - return this.storage.size; - } -} - -const localStorage = - typeof window === 'undefined' - ? new MockedLocalStorage() - : window.localStorage; - -export default localStorage; diff --git a/packages/react-celo/src/utils/logger.ts b/packages/react-celo/src/utils/logger.ts new file mode 100644 index 00000000..77169406 --- /dev/null +++ b/packages/react-celo/src/utils/logger.ts @@ -0,0 +1,73 @@ +import { Maybe } from '../types'; + +export enum Level { + Silent = 0, + Debug = 1, + Default = 2, + Warning = 3, + Error = 4, +} + +export interface ILogger { + debug(...args: unknown[]): void; + log(...args: unknown[]): void; + warn(...args: unknown[]): void; + error(...args: unknown[]): void; +} + +/** + * @internal Used the defined the default applicationLogger + */ +export class Logger implements ILogger { + level: Level; + + constructor(level: Maybe = null, private namespace = '[react-celo]') { + if (!level) { + if ( + process.env.DEBUG === 'true' || + process.env.NODE_ENV !== 'production' + ) { + this.level = Level.Debug; + } else { + this.level = Level.Error; + } + } else { + this.level = level; + } + } + + debug(...args: unknown[]) { + if (this.level > Level.Debug) return; + + console.info(this.namespace, ...args); + } + + log(...args: unknown[]) { + if (this.level > Level.Default) return; + + console.log(this.namespace, ...args); + } + + warn(...args: unknown[]) { + if (this.level > Level.Warning) return; + + console.warn(this.namespace, ...args); + } + + error(...args: unknown[]) { + if (this.level > Level.Error) return; + + console.error(this.namespace, ...args); + } +} + +let applicationLogger: ILogger = new Logger(); + +function setApplicationLogger(logger: ILogger) { + applicationLogger = logger; +} + +function getApplicationLogger(): ILogger { + return applicationLogger; +} +export { getApplicationLogger, setApplicationLogger }; diff --git a/packages/react-celo/src/utils/metamask.ts b/packages/react-celo/src/utils/metamask.ts index a233b8cd..99e736e8 100644 --- a/packages/react-celo/src/utils/metamask.ts +++ b/packages/react-celo/src/utils/metamask.ts @@ -1,7 +1,6 @@ -import { MiniContractKit, newKit } from '@celo/contractkit/lib/mini-kit'; +import { newKit } from '@celo/contractkit/lib/mini-kit'; import { GoldTokenWrapper } from '@celo/contractkit/lib/wrappers/GoldTokenWrapper'; import { StableTokenWrapper } from '@celo/contractkit/lib/wrappers/StableTokenWrapper'; -import Web3 from 'web3'; import { Alfajores, Baklava, Mainnet } from '../constants'; import { Ethereum } from '../global'; @@ -31,7 +30,7 @@ const BAKLAVA_PARAMS = Object.freeze({ }, }); -const params: { [chain in ChainId]: typeof CELO_PARAMS } = { +const PARAMS: { [chain in ChainId]: typeof CELO_PARAMS } = { [ChainId.Mainnet]: CELO_PARAMS, [ChainId.Alfajores]: ALFAJORES_PARAMS, [ChainId.Baklava]: BAKLAVA_PARAMS, @@ -97,9 +96,9 @@ export const makeNetworkParams = async ( return { chainId: `0x${info.chainId.toString(16)}`, - chainName: params[info.chainId].chainName ?? info.name, + chainName: PARAMS[info.chainId].chainName ?? info.name, nativeCurrency: { - name: params[info.chainId].nativeCurrency.name, + name: PARAMS[info.chainId].nativeCurrency.name, symbol, decimals, }, @@ -125,7 +124,7 @@ export const tokenToParam = async ( name, symbol, decimals, - image: `https://celoreserve.org/assets/tokens/${symbol}.svg`, + image: `https://reserve.mento.org/assets/tokens/${symbol}.svg`, }, }; }; @@ -209,15 +208,16 @@ export async function addNetworksToMetamask(ethereum: Ethereum): Promise { ); } -export async function switchToCeloNetwork( - kit: MiniContractKit, +export async function switchToNetwork( network: Network, - ethereum: Ethereum + ethereum: Ethereum, + getChainId: () => Promise ): Promise { - const web3 = new Web3(ethereum); - const chainId = await web3.eth.getChainId(); - - if (network.chainId !== chainId) { + const [chainId, walletChainId] = await Promise.all([ + getChainId(), + getWalletChainId(ethereum), + ]); + if (network.chainId !== chainId || network.chainId !== walletChainId) { try { await ethereum.request({ method: 'wallet_switchEthereumChain', @@ -227,6 +227,7 @@ export async function switchToCeloNetwork( }, ], }); + await networkHasUpdated(getChainId, network.chainId); } catch (err) { const { code } = err as MetamaskRPCError; if ( @@ -235,7 +236,7 @@ export async function switchToCeloNetwork( ) { // ChainId not yet added to metamask await addNetworkToMetamask(ethereum, network); - return switchToCeloNetwork(kit, network, ethereum); + return switchToNetwork(network, ethereum, getChainId); } else if (code === MetamaskRPCErrorCode.AwaitingUserConfirmation) { // user has already been requested to switch the network return; @@ -245,3 +246,38 @@ export async function switchToCeloNetwork( } } } + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +const SLEEP = 500; +const MAX_WAIT_MINUTES = 3; +const MAX_RETRY = Math.round((MAX_WAIT_MINUTES * 1000) / SLEEP); + +// Hacky workaround to wait for the network to change.\ + +export const networkHasUpdated = async ( + getChainId: () => Promise, + expectedChainId: number +) => { + let attempts = 0; + let isNetworkUpdated = false; + while (!isNetworkUpdated) { + attempts++; + if (attempts >= MAX_RETRY) { + throw new Error('Network did not change'); + } + const chainId = await getChainId(); + if (chainId === expectedChainId) { + isNetworkUpdated = true; + return true; + } + await sleep(SLEEP); + } +}; + +async function getWalletChainId(ethereum: Ethereum) { + const walletChainId = ethereum.chainId + ? ethereum.chainId + : await ethereum.request({ method: 'eth_chainId' }); + return parseInt(walletChainId, 16); +} diff --git a/packages/react-celo/src/utils/network-watcher.ts b/packages/react-celo/src/utils/network-watcher.ts new file mode 100644 index 00000000..21138f1d --- /dev/null +++ b/packages/react-celo/src/utils/network-watcher.ts @@ -0,0 +1,20 @@ +import { ConnectorEvents } from '../connectors/common'; +import { Connector, Network } from '../types'; +import { getApplicationLogger } from './logger'; + +function networkWatcher(connector: Connector, networks: Network[]) { + connector.on(ConnectorEvents.WALLET_CHAIN_CHANGED, (chainId) => { + const network = networks?.find((net) => net.chainId === chainId); + getApplicationLogger().debug( + '[network-watcher] received', + chainId, + 'found', + network ? network : 'nothing' + ); + if (network && connector.continueNetworkUpdateFromWallet) { + void connector.continueNetworkUpdateFromWallet(network); + } + }); +} + +export default networkWatcher; diff --git a/packages/react-celo/src/utils/persistor.ts b/packages/react-celo/src/utils/persistor.ts new file mode 100644 index 00000000..b9e43b96 --- /dev/null +++ b/packages/react-celo/src/utils/persistor.ts @@ -0,0 +1,35 @@ +import { ConnectorEvents, ConnectorParams } from '../connectors/common'; +import { localStorageKeys } from '../constants'; +import { Connector } from '../types'; +import { clearPreviousConfig, setTypedStorageKey } from './local-storage'; + +type Updater = (connector: Connector) => void; + +const persistor: Updater = (connector: Connector) => { + connector.on(ConnectorEvents.ADDRESS_CHANGED, (address) => { + setTypedStorageKey(localStorageKeys.lastUsedAddress, address); + }); + // This might not be needed since we tend to just recreated connectors when network switches + connector.on(ConnectorEvents.NETWORK_CHANGED, (networkName) => { + setTypedStorageKey(localStorageKeys.lastUsedNetwork, networkName); + }); + connector.on(ConnectorEvents.CONNECTED, (params: ConnectorParams) => { + setTypedStorageKey(localStorageKeys.lastUsedNetwork, params.networkName); + setTypedStorageKey(localStorageKeys.lastUsedWalletType, params.walletType); + setTypedStorageKey(localStorageKeys.lastUsedAddress, params.address); + + if (params.index) { + setTypedStorageKey(localStorageKeys.lastUsedIndex, params.index); + } + + if (params.walletId) { + setTypedStorageKey(localStorageKeys.lastUsedWalletId, params.walletId); + } + }); + + connector.on(ConnectorEvents.DISCONNECTED, () => { + clearPreviousConfig(); + }); +}; + +export default persistor; diff --git a/packages/react-celo/src/utils/resurrector.ts b/packages/react-celo/src/utils/resurrector.ts new file mode 100644 index 00000000..b7fb95d3 --- /dev/null +++ b/packages/react-celo/src/utils/resurrector.ts @@ -0,0 +1,87 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { CeloContract } from '@celo/contractkit'; + +import { + CeloExtensionWalletConnector, + CoinbaseWalletConnector, + InjectedConnector, + LedgerConnector, + MetaMaskConnector, + PrivateKeyConnector, + WalletConnectConnector, +} from '../connectors'; +import { buildOptions } from '../connectors/wallet-connect'; +import { localStorageKeys, WalletTypes } from '../constants'; +import { Dapp, Network } from '../types'; +import { getTypedStorageKey } from './local-storage'; +import { getApplicationLogger } from './logger'; + +export function resurrector(networks: Network[], dapp: Dapp) { + const walletType = getTypedStorageKey(localStorageKeys.lastUsedWalletType); + const network = getNetwork(networks); + + if (!walletType || !network) return null; + getApplicationLogger().log( + '[resurrector] will create', + walletType, + 'with', + network, + dapp + ); + try { + switch (walletType) { + case WalletTypes.Ledger: { + const index = getTypedStorageKey(localStorageKeys.lastUsedIndex); + + if (index === null) return null; + + return new LedgerConnector(network, index, CeloContract.GoldToken); + } + case WalletTypes.CeloExtensionWallet: + return new CeloExtensionWalletConnector( + network, + CeloContract.GoldToken + ); + case WalletTypes.MetaMask: + return new MetaMaskConnector(network, CeloContract.GoldToken); + case WalletTypes.Injected: + return new InjectedConnector(network, CeloContract.GoldToken); + case WalletTypes.PrivateKey: { + const privateKey = getTypedStorageKey( + localStorageKeys.lastUsedPrivateKey + ); + return new PrivateKeyConnector( + network, + privateKey as string, + CeloContract.GoldToken + ); + } + case WalletTypes.CoinbaseWallet: + return new CoinbaseWalletConnector(network, dapp); + case WalletTypes.CeloDance: + case WalletTypes.CeloTerminal: + case WalletTypes.CeloWallet: + case WalletTypes.Valora: + case WalletTypes.WalletConnect: { + return new WalletConnectConnector( + network, + CeloContract.GoldToken, + buildOptions(network) + ); + } + + case WalletTypes.Unauthenticated: + return null; + } + } catch (e) { + getApplicationLogger().error('Unknown error resurrecting', walletType, e); + return null; + } +} + +function getNetwork(networks: Network[]) { + const networkName = getTypedStorageKey(localStorageKeys.lastUsedNetwork); + if (!networkName) return; + const network = networks.find((net) => net.name === networkName); + return network; +} diff --git a/packages/react-celo/src/utils/sort.ts b/packages/react-celo/src/utils/sort.ts index 14c31597..fea10af1 100644 --- a/packages/react-celo/src/utils/sort.ts +++ b/packages/react-celo/src/utils/sort.ts @@ -15,8 +15,8 @@ export const sortByPriority: SortingPredicate = (a, b) => { const A = a.listPriority(); const B = b.listPriority(); - if (A < B) return -1; - if (A > B) return 1; + if (A < B) return 1; + if (A > B) return -1; return 0; }; diff --git a/packages/react-celo/src/utils/updater.ts b/packages/react-celo/src/utils/updater.ts new file mode 100644 index 00000000..ba4ed710 --- /dev/null +++ b/packages/react-celo/src/utils/updater.ts @@ -0,0 +1,28 @@ +import { ConnectorEvents } from '../connectors/common'; +import { Dispatcher } from '../react-celo-provider-state'; +import { Connector } from '../types'; +import { getApplicationLogger } from './logger'; + +type Updater = (connector: Connector, dispatch: Dispatcher) => void; + +export const updater: Updater = (connector, dispatch) => { + const logger = getApplicationLogger(); + connector.on(ConnectorEvents.ADDRESS_CHANGED, (address) => { + dispatch('setAddress', address); + }); + connector.on(ConnectorEvents.NETWORK_CHANGED, (networkName) => { + logger.log('Network Changing to', networkName); + dispatch('setNetworkByName', networkName); + }); + connector.on(ConnectorEvents.CONNECTED, (params) => { + logger.log('Updater witnessed connection'); + dispatch('connect', params); + }); + + connector.on(ConnectorEvents.DISCONNECTED, () => { + logger.log('Updater witnessed disconnection'); + dispatch('disconnect'); + }); +}; + +export default updater; diff --git a/packages/react-celo/src/walletIcons.tsx b/packages/react-celo/src/walletIcons.tsx deleted file mode 100644 index 33aa2660..00000000 --- a/packages/react-celo/src/walletIcons.tsx +++ /dev/null @@ -1,556 +0,0 @@ -import React from 'react'; - -export const LEDGER: React.FC> = (props) => ( - - - - - - - - -); - -export const ETHEREUM: React.FC> = (props) => ( - - - - - - - - - - - - - - - -); - -export const PRIVATE_KEY: React.FC> = (props) => ( - -); - -export const WALLETCONNECT: React.FC> = ( - props -) => ( - - - - - - - -); - -export const METAMASK: React.FC> = (props) => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -export const CELO: React.FC> = (props) => ( - - - - - - - - -); - -export const VALORA: React.FC> = (props) => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -export const CHROME_EXTENSION_STORE: React.FC> = ( - props -) => ( - - - - - - - - - - - -); - -export const CELO_DANCE: React.FC> = (props) => ( - - - - - -); - -export const STEAKWALLET = - 'https://res.cloudinary.com/helpkit/image/upload/v1637141229/steakwallet_logo_dark_6703a6b026.png'; - -export const CELO_TERMINAL: React.FC> = ( - props -) => ( - - - - - - - - -); diff --git a/packages/wallet-walletconnect/package.json b/packages/wallet-walletconnect/package.json index 380ff9c6..bbfc39cb 100644 --- a/packages/wallet-walletconnect/package.json +++ b/packages/wallet-walletconnect/package.json @@ -1,6 +1,6 @@ { "name": "@celo/wallet-walletconnect", - "version": "4.0.1-dev", + "version": "4.1.0", "description": "WalletConnect wallet implementation", "author": "Celo", "license": "Apache-2.0", diff --git a/packages/walletconnect-v1/__tests__/utils/common.ts b/packages/walletconnect-v1/__tests__/utils/common.ts index 817098ac..f6e9c4c3 100644 --- a/packages/walletconnect-v1/__tests__/utils/common.ts +++ b/packages/walletconnect-v1/__tests__/utils/common.ts @@ -2,8 +2,7 @@ import { Address } from '@celo/base'; import { CeloTx } from '@celo/connect'; import { newKit } from '@celo/contractkit/lib/mini-kit'; import { EIP712TypedData } from '@celo/utils/lib/sign-typed-data-utils'; -import { toChecksumAddress } from 'ethereumjs-util'; - +import { toChecksumAddress } from '@walletconnect/utils-v1'; // personal_sign is the one RPC that has [payload, from] rather // than [from, payload] export function parsePersonalSign(params: [string, string]): { diff --git a/packages/walletconnect-v1/__tests__/utils/in-memory-wallet.ts b/packages/walletconnect-v1/__tests__/utils/in-memory-wallet.ts index dc1b562e..fa45b643 100644 --- a/packages/walletconnect-v1/__tests__/utils/in-memory-wallet.ts +++ b/packages/walletconnect-v1/__tests__/utils/in-memory-wallet.ts @@ -59,11 +59,11 @@ export function getTestWallet(): { if (session.event === 'disconnect') { const params = session.params as { message: string }[]; - const error = + const err = params && params[0] && params[0].message ? params[0].message : 'Unknown error'; - console.log('DISCONNECT', params, error); + console.log('DISCONNECT', params, err); } }; const onSessionRequest = ( diff --git a/packages/walletconnect-v1/jest.config.js b/packages/walletconnect-v1/jest.config.js index 17a8908c..30272f96 100644 --- a/packages/walletconnect-v1/jest.config.js +++ b/packages/walletconnect-v1/jest.config.js @@ -3,6 +3,5 @@ const pkg = require('./package.json'); module.exports = { ...base, - name: pkg.name, displayName: pkg.name, }; diff --git a/packages/walletconnect-v1/package.json b/packages/walletconnect-v1/package.json index 93a161f8..dc4678bb 100644 --- a/packages/walletconnect-v1/package.json +++ b/packages/walletconnect-v1/package.json @@ -1,6 +1,6 @@ { "name": "@celo/wallet-walletconnect-v1", - "version": "4.0.1-dev", + "version": "4.1.0", "description": "WalletConnect wallet legacy (v1) implementation", "author": "Celo", "license": "Apache-2.0", @@ -29,8 +29,7 @@ "@walletconnect/client-v1": "npm:@walletconnect/client@1.6.6", "@walletconnect/types-v1": "npm:@walletconnect/types@1.6.6", "@walletconnect/utils-v1": "npm:@walletconnect/utils@1.6.6", - "debug": "^4.3.3", - "ethereumjs-util": "^7.1.3" + "debug": "^4.3.3" }, "engines": { "node": ">=10" diff --git a/packages/walletconnect-v1/src/types.ts b/packages/walletconnect-v1/src/types.ts index 493c72e2..0ab14ebb 100644 --- a/packages/walletconnect-v1/src/types.ts +++ b/packages/walletconnect-v1/src/types.ts @@ -53,22 +53,55 @@ export interface WCSession { handshakeTopic: string; } +export type SessionDisconnect = { + event: 'disconnect'; + params: [ + { + message?: string; + } + ]; +}; + +export type SessionConnect = { + event: 'connect'; + params: [ + { + accounts: string[]; + chainId: number; + peerId: string; + peerMeta: unknown; + } + ]; +}; + export type SessionProposal = Request< [ { - chainId: number; approved: boolean; - networkId: number | null; - peerId: string; - peerMeta: { + accounts?: string[]; + chainId: number; + networkId?: number | null; + peerId?: string; + rpcUrl?: string; + peerMeta?: { description: string; icons: string[]; name: string; url: string; }; } - ] + ], + 'wc_sessionUpdate' >; + +export type SessionUpdate = Request< + { + accounts: string[]; + chainId: number; + }[], + 'session_update' +>; + export type AccountsProposal = Request; export type SignTransactionProposal = Request< diff --git a/packages/walletconnect-v1/src/utils/from-rpc-sig.test.ts b/packages/walletconnect-v1/src/utils/from-rpc-sig.test.ts new file mode 100644 index 00000000..75704cdd --- /dev/null +++ b/packages/walletconnect-v1/src/utils/from-rpc-sig.test.ts @@ -0,0 +1,40 @@ +import { fromRpcSig } from './from-rpc-sig'; + +describe('fromRpcSig', () => { + const r = Buffer.from( + '99e71a99cb2270b8cac5254f9e99b6210c6c10224a1579cf389ef88b20a1abe9', + 'hex' + ); + const s = Buffer.from( + '129ff05af364204442bdb53ab6f18a99ab48acc9326fa689f228040429e3ca66', + 'hex' + ); + it('converts', () => { + const sig = + '0x99e71a99cb2270b8cac5254f9e99b6210c6c10224a1579cf389ef88b20a1abe9129ff05af364204442bdb53ab6f18a99ab48acc9326fa689f228040429e3ca661b'; + + expect(fromRpcSig(sig)).toMatchObject({ + v: 27, + r, + s, + }); + }); + it('throws when length is short', () => { + expect(() => fromRpcSig('')).toThrow(); + expect(() => + fromRpcSig( + '0x99e71a99cb2270b8cac5254f9e99b6210c6c10224a1579cf389ef88b20a1abe9129ff05af364' + ) + ).toThrow(); + }); + + it('supports EIP-2098', () => { + const sig = + '0x99e71a99cb2270b8cac5254f9e99b6210c6c10224a1579cf389ef88b20a1abe9929ff05af364204442bdb53ab6f18a99ab48acc9326fa689f228040429e3ca66'; + expect(fromRpcSig(sig)).toMatchObject({ + v: 28, + r, + s, + }); + }); +}); diff --git a/packages/walletconnect-v1/src/utils/from-rpc-sig.ts b/packages/walletconnect-v1/src/utils/from-rpc-sig.ts new file mode 100644 index 00000000..3f3e58c3 --- /dev/null +++ b/packages/walletconnect-v1/src/utils/from-rpc-sig.ts @@ -0,0 +1,88 @@ +import { convertBufferToNumber } from '@walletconnect/utils-v1'; + +export interface ECDSASignature { + v: number; + r: Buffer; + s: Buffer; +} + +/** + * + * Convert signature format of the `eth_sign` RPC method to signature parameters + * Borrowed from ethereumjs https://github.com/ethereumjs/ethereumjs-monorepo/blob/ade4233ddffffdd146b386de701762196a8c941c/packages/util/src/signature.ts + * Copyright ethereumjs License: https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/util/LICENSE + * NOTE: all because of a bug in geth: https://github.com/ethereum/go-ethereum/issues/2053 + */ +export const fromRpcSig = function (sig: string): ECDSASignature { + const buf: Buffer = stringToBuffer(sig); + + let r: Buffer; + let s: Buffer; + let v: number; + if (buf.length >= 65) { + r = buf.slice(0, 32); + s = buf.slice(32, 64); + v = convertBufferToNumber(buf.slice(64)); + } else if (buf.length === 64) { + // Compact Signature Representation (https://eips.ethereum.org/EIPS/eip-2098) + r = buf.slice(0, 32); + s = buf.slice(32, 64); + v = convertBufferToNumber(buf.slice(32, 33)) >> 7; + s[0] &= 0x7f; + } else { + throw new Error('Invalid signature length'); + } + + // support both versions of `eth_sign` responses + if (v < 27) { + v += 27; + } + + return { + v, + r, + s, + }; +}; + +function stringToBuffer(v: string) { + if (v === null || v === undefined) { + return Buffer.allocUnsafe(0); + } + + if (Buffer.isBuffer(v)) { + return Buffer.from(v); + } + + if (typeof v === 'string') { + if (!isHexString(v)) { + throw new Error( + `Cannot convert string to buffer. toBuffer only supports 0x-prefixed hex strings and this string was given: ${v}` + ); + } + return Buffer.from(padToEven(stripHexPrefix(v)), 'hex'); + } + + throw new Error('invalid type'); +} + +function stripHexPrefix(value: string) { + return isHexString(value) ? value.slice(2) : value; +} + +function isHexString(str: string) { + if (typeof str !== 'string') { + throw new Error( + `[stripHexPrefix] input must be type 'string', received ${typeof str}` + ); + } + return str.startsWith('0x'); +} + +function padToEven(value: string) { + if (value.length % 2) { + return `0${value}`; + } + + return value; +} diff --git a/packages/walletconnect-v1/src/wc-signer.ts b/packages/walletconnect-v1/src/wc-signer.ts index b88a734d..280d23c3 100644 --- a/packages/walletconnect-v1/src/wc-signer.ts +++ b/packages/walletconnect-v1/src/wc-signer.ts @@ -1,9 +1,9 @@ import { CeloTx, EncodedTransaction, Signer } from '@celo/connect'; import { EIP712TypedData } from '@celo/utils/lib/sign-typed-data-utils'; import WalletConnect from '@walletconnect/client-v1'; -import * as ethUtil from 'ethereumjs-util'; import { SupportedMethods, WCSession } from './types'; +import { ECDSASignature, fromRpcSig } from './utils/from-rpc-sig'; /** * Implements the signer interface on top of the WalletConnect interface. @@ -38,24 +38,20 @@ export class WalletConnectSigner implements Signer { return signedTx; } - async signTypedData( - data: EIP712TypedData - ): Promise> { + async signTypedData(data: EIP712TypedData): Promise { const signature = await this.request( SupportedMethods.signTypedData, [this.getNativeKey(), JSON.stringify(data)] ); - return ethUtil.fromRpcSig(signature); + return fromRpcSig(signature); } - async signPersonalMessage( - data: string - ): Promise> { + async signPersonalMessage(data: string): Promise { const signature = await this.request( SupportedMethods.personalSign, [data, this.getNativeKey()] ); - return ethUtil.fromRpcSig(signature); + return fromRpcSig(signature); } getNativeKey = (): string => this.account; diff --git a/packages/walletconnect-v1/src/wc-wallet.ts b/packages/walletconnect-v1/src/wc-wallet.ts index ffeb8a7a..cd1bc495 100644 --- a/packages/walletconnect-v1/src/wc-wallet.ts +++ b/packages/walletconnect-v1/src/wc-wallet.ts @@ -1,25 +1,22 @@ /// import { sleep } from '@celo/base'; -import { CeloTx, EncodedTransaction } from '@celo/connect'; +import { CeloTx, EncodedTransaction } from '@celo/connect/lib/types'; import { RemoteWallet } from '@celo/wallet-remote'; import WalletConnect from '@walletconnect/client-v1'; import { ICreateSessionOptions, - IInternalEvent, IWalletConnectSDKOptions, } from '@walletconnect/types'; import { CANCELED, defaultBridge } from './constants'; import { - AccountsProposal, CLIENT_EVENTS, - ComputeSharedSecretProposal, - DecryptProposal, - PersonalSignProposal, + EthProposal, + SessionConnect, + SessionDisconnect, SessionProposal, - SignTransactionProposal, - SignTypedSignProposal, + SessionUpdate, WalletConnectWalletOptions, } from './types'; import Canceler from './utils/canceler'; @@ -79,10 +76,11 @@ export class WalletConnectWallet extends RemoteWallet { return new WalletConnect(this.initOptions); } - /** - * Get the URI needed for out of band session establishment - */ - public async getUri(): Promise { + public async setupClient() { + if (this.client && this.client.connected) { + return; + } + this.client = this.getWalletConnectClient(); this.client.on(CLIENT_EVENTS.connect, this.onSessionCreated); @@ -98,17 +96,38 @@ export class WalletConnectWallet extends RemoteWallet { // create new session await this.client.createSession(this.connectOptions); } + } + + async switchToChain(params: { + chainId: number; + networkId: number; + rpcUrl: string; + nativeCurrency: { name: string; symbol: string }; + }) { + return this.client!.updateChain(params); + } + + /** + * Get the URI needed for out of band session establishment + */ + public async getUri() { + await this.setupClient(); return this.client?.uri; } - onSessionCreated = (error: Error | null, session: IInternalEvent): void => { + // heads up! common pattern for the onSession*** methods is to overwrite them externally + + onSessionCreated = (error: Error | null, session: SessionConnect): void => { console.info('onSessionCreated', error, session); if (error) { throw error; } }; - onSessionDeleted = (error: Error | null, session: IInternalEvent): void => { + onSessionDeleted = ( + error: Error | null, + session: SessionDisconnect + ): void => { console.info('onSessionDeleted', error); if (error) { throw error; @@ -116,11 +135,11 @@ export class WalletConnectWallet extends RemoteWallet { if (session.event === 'disconnect') { const params = session.params as { message: string }[]; - const error = + const errMessage = params && params[0] && params[0].message ? params[0].message : 'Unknown error'; - void this.close(error); + void this.close(errMessage); } }; onSessionRequest = (error: Error | null, session: SessionProposal): void => { @@ -129,22 +148,13 @@ export class WalletConnectWallet extends RemoteWallet { throw error; } }; - onSessionUpdated = (error: Error | null, session: SessionProposal): void => { + onSessionUpdated = (error: Error | null, session: SessionUpdate): void => { console.info('onSessionUpdated', error, session); if (error) { throw error; } }; - onCallRequest = ( - error: Error | null, - payload: - | AccountsProposal - | SignTransactionProposal - | PersonalSignProposal - | SignTypedSignProposal - | DecryptProposal - | ComputeSharedSecretProposal - ): void => { + onCallRequest = (error: Error | null, payload: EthProposal): void => { console.info('onCallRequest', error, payload); if (error) { throw error; @@ -212,10 +222,9 @@ export class WalletConnectWallet extends RemoteWallet { throw new Error('Wallet must be initialized before calling close()'); } this.canceler.cancel(); - + // https://github.com/WalletConnect/walletconnect-monorepo/issues/315 + localStorage.removeItem('walletconnect'); if (this.client.connected) { - // https://github.com/WalletConnect/walletconnect-monorepo/issues/315 - localStorage.removeItem('walletconnect'); await this.client.killSession({ message }); } } diff --git a/readme.md b/readme.md index b16d8e16..73b83ee0 100644 --- a/readme.md +++ b/readme.md @@ -5,10 +5,10 @@ [![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://github.com/celo-org/react-celo/blob/master/LICENSEs) [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/celo-org/react-celo/issues) [![Open Source Love svg1](https://badges.frapsoft.com/os/v1/open-source.svg?v=103)](https://github.com/ellerbrock/open-source-badges/) -[![npm version](https://badge.fury.io/js/%40celo-tools%2Freact-celo.png)](https://badge.fury.io/js/%40celo-tools%2Freact-celo) +[![npm version](https://badge.fury.io/js/%40celo%2Freact-celo.png)](https://badge.fury.io/js/%40celo%2Freact-celo) [![codecov](https://codecov.io/gh/celo-org/react-celo/branch/master/graph/badge.svg?token=vy6ALIKLwt)](https://codecov.io/gh/celo-org/react-celo) -The easiest way to access [ContractKit](https://www.npmjs.com/package/@celo/contractkit) in your React applications 🔥. `react-celo` is a [React hook](https://reactjs.org/docs/hooks-intro.html) for managing access to ContractKit with a built-in headless modal system for connecting to your users wallet of choice. +The easiest way to access [Celo](https://www.npmjs.com/package/@celo/contractkit) in your React applications 🔥. `react-celo` is a [React hook](https://reactjs.org/docs/hooks-intro.html) for managing access to Celo with a built-in headless modal system for connecting to your users wallet of choice. Now your DApp can be made available to everyone in the Celo ecosystem, from Valora users to self custodied Ledger users. @@ -365,6 +365,21 @@ interface Theme { } ``` +### Logging and debugging + +We log by default `debug` or above in development mode. It is determined your environement variables: by either setting `DEBUG` to `true` or setting `NODE_ENV` to something else than `production`). In production mode, we log only `error`. + +But you are welcome to provide your own logger at the provider level. It should implement our `ILogger` interface which looks like that: + +```ts +interface ILogger { + debug(...args: unknown[]): void; + log(...args: unknown[]): void; + warn(...args: unknown[]): void; + error(...args: unknown[]): void; +} +``` + ## Development To run all the packages locally at once, simply clone this repository and run: diff --git a/yarn.lock b/yarn.lock index 4e4cee97..b4492f70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31,16 +31,16 @@ dependencies: "@babel/highlight" "^7.16.0" +"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.17.10": + version "7.17.10" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.10.tgz#711dc726a492dfc8be8220028b1b92482362baab" + integrity sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw== + "@babel/compat-data@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.0.tgz#ea269d7f78deb3a7826c39a4048eecda541ebdaa" integrity sha512-DGjt2QZse5SGd9nfOSqO4WLJ8NN/oHkijbXbPrxuoJO3oIPJL3TciZs9FX+cOHNiY9E9l0opL8g7BmLe3T+9ew== -"@babel/compat-data@^7.17.10": - version "7.17.10" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.10.tgz#711dc726a492dfc8be8220028b1b92482362baab" - integrity sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw== - "@babel/core@^7.11.6": version "7.17.10" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.10.tgz#74ef0fbf56b7dfc3f198fc2d927f4f03e12f4b05" @@ -101,6 +101,25 @@ "@jridgewell/gen-mapping" "^0.1.0" jsesc "^2.5.1" +"@babel/generator@^7.18.2": + version "7.18.2" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.18.2.tgz#33873d6f89b21efe2da63fe554460f3df1c5880d" + integrity sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw== + dependencies: + "@babel/types" "^7.18.2" + "@jridgewell/gen-mapping" "^0.3.0" + jsesc "^2.5.1" + +"@babel/helper-compilation-targets@^7.13.0": + version "7.18.2" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.2.tgz#67a85a10cbd5fc7f1457fec2e7f45441dc6c754b" + integrity sha512-s1jnPotJS9uQnzFtiZVBUxe67CuBa679oWFHpxYYnTpRL/1ffhyX44R9uYiXoa/pLXcY9H2moJta0iaanlk/rQ== + dependencies: + "@babel/compat-data" "^7.17.10" + "@babel/helper-validator-option" "^7.16.7" + browserslist "^4.20.2" + semver "^6.3.0" + "@babel/helper-compilation-targets@^7.16.0": version "7.16.3" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.3.tgz#5b480cd13f68363df6ec4dc8ac8e2da11363cbf0" @@ -121,6 +140,20 @@ browserslist "^4.20.2" semver "^6.3.0" +"@babel/helper-define-polyfill-provider@^0.3.1": + version "0.3.1" + resolved "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz#52411b445bdb2e676869e5a74960d2d3826d2665" + integrity sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA== + dependencies: + "@babel/helper-compilation-targets" "^7.13.0" + "@babel/helper-module-imports" "^7.12.13" + "@babel/helper-plugin-utils" "^7.13.0" + "@babel/traverse" "^7.13.0" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + semver "^6.1.2" + "@babel/helper-environment-visitor@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz#ff484094a839bde9d89cd63cba017d7aae80ecd7" @@ -128,6 +161,11 @@ dependencies: "@babel/types" "^7.16.7" +"@babel/helper-environment-visitor@^7.18.2": + version "7.18.2" + resolved "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.2.tgz#8a6d2dedb53f6bf248e31b4baf38739ee4a637bd" + integrity sha512-14GQKWkX9oJzPiQQ7/J36FTXcD4kSp8egKjO9nINlSKiHITRA9q/R74qu8S9xlc/b/yjsJItQUeeh3xnGN0voQ== + "@babel/helper-function-name@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.16.0.tgz#b7dd0797d00bbfee4f07e9c4ea5b0e30c8bb1481" @@ -173,6 +211,13 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" + integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== + dependencies: + "@babel/types" "^7.16.7" + "@babel/helper-module-imports@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.0.tgz#90538e60b672ecf1b448f5f4f5433d37e79a3ec3" @@ -180,13 +225,6 @@ dependencies: "@babel/types" "^7.16.0" -"@babel/helper-module-imports@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" - integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== - dependencies: - "@babel/types" "^7.16.7" - "@babel/helper-module-transforms@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.0.tgz#1c82a8dd4cb34577502ebd2909699b194c3e9bb5" @@ -227,6 +265,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9" integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ== +"@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.17.12": + version "7.17.12" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.17.12.tgz#86c2347da5acbf5583ba0a10aed4c9bf9da9cf96" + integrity sha512-JDkf04mqtN3y4iAbO1hv9U2ARpPyPL1zqyWs/2WG1pgSq9llHFjStX5jdxb84himgJm+8Ng+x0oiWF/nw/XQKA== + "@babel/helper-replace-supers@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.16.0.tgz#73055e8d3cf9bcba8ddb55cad93fedc860f68f17" @@ -350,6 +393,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.10.tgz#873b16db82a8909e0fbd7f115772f4b739f6ce78" integrity sha512-n2Q6i+fnJqzOaq2VkdXxy2TCPCWQZHiCo0XqmrCvDWcZQKRyZzYi4Z0yxlBuN0w+r2ZHmre+Q087DSrw3pbJDQ== +"@babel/parser@^7.18.0": + version "7.18.4" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz#6774231779dd700e0af29f6ad8d479582d7ce5ef" + integrity sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow== + "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" @@ -441,6 +489,18 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" +"@babel/plugin-transform-runtime@^7.5.5": + version "7.18.2" + resolved "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.18.2.tgz#04637de1e45ae8847ff14b9beead09c33d34374d" + integrity sha512-mr1ufuRMfS52ttq+1G1PD8OJNqgcTFjq3hwn8SZ5n1x1pBhi0E36rYMdTK0TsKtApJ4lDEdfXJwtGobQMHSMPg== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-plugin-utils" "^7.17.12" + babel-plugin-polyfill-corejs2 "^0.3.0" + babel-plugin-polyfill-corejs3 "^0.5.0" + babel-plugin-polyfill-regenerator "^0.3.0" + semver "^6.3.0" + "@babel/runtime@^7.12.5": version "7.16.3" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5" @@ -448,6 +508,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.5.5": + version "7.18.3" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.3.tgz#c7b654b57f6f63cf7f8b418ac9ca04408c4579f4" + integrity sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.9.2": version "7.17.9" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72" @@ -473,6 +540,22 @@ "@babel/parser" "^7.16.7" "@babel/types" "^7.16.7" +"@babel/traverse@^7.13.0": + version "7.18.2" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.2.tgz#b77a52604b5cc836a9e1e08dca01cba67a12d2e8" + integrity sha512-9eNwoeovJ6KH9zcCNnENY7DMFwTU9JdGCFtqNLfUAqtUHRCOsTOqWoffosP8vKmNYeSBUv3yVJXjfd8ucwOjUA== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.18.2" + "@babel/helper-environment-visitor" "^7.18.2" + "@babel/helper-function-name" "^7.17.9" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.18.0" + "@babel/types" "^7.18.2" + debug "^4.1.0" + globals "^11.1.0" + "@babel/traverse@^7.16.0", "@babel/traverse@^7.16.3", "@babel/traverse@^7.7.2": version "7.16.3" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.3.tgz#f63e8a938cc1b780f66d9ed3c54f532ca2d14787" @@ -528,6 +611,14 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" +"@babel/types@^7.18.2": + version "7.18.4" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.18.4.tgz#27eae9b9fd18e9dccc3f9d6ad051336f307be354" + integrity sha512-ThN1mBcMq5pG/Vm2IcBmPPfyPXbd8S02rS+OBIDENdufvqC7Z/jHPCv9IcP01277aKtDI8g/2XysBN4hA8niiw== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -642,6 +733,37 @@ eth-lib "^0.2.8" ethereumjs-util "^5.2.0" +"@clabs/packages-publisher@0.0.1-alpha.3": + version "0.0.1-alpha.3" + resolved "https://registry.npmjs.org/@clabs/packages-publisher/-/packages-publisher-0.0.1-alpha.3.tgz#fbaf6866e3751ada30cb6e2e8d622bfa97c86a4b" + integrity sha512-ooxBTiRStigZFaGSF4SlAW20t7aA/y22D04QcEN0aAMR3JUFfOZ4CDBw/zZjmt94dbM315I+cfAZF2aJjwyENA== + dependencies: + execa "5.1.1" + kleur "^4.1.4" + prompts "^2.4.2" + semver "^7.3.6" + yargs "^17.4.1" + +"@coinbase/wallet-sdk@^3.2.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@coinbase/wallet-sdk/-/wallet-sdk-3.2.0.tgz#cceda6c2ed213b663c5cbb35fcf65a57c24323f0" + integrity sha512-uw9rdsfzXsdmC9FblMt2UH4094vLl9h3i1za5A2MaC8Ak1X/E1QbGr1oi64QAx9HD+M75zmy506F+zT0/s5dpQ== + dependencies: + "@metamask/safe-event-emitter" "2.0.0" + bind-decorator "^1.0.11" + bn.js "^5.1.1" + clsx "^1.1.0" + eth-block-tracker "4.4.3" + eth-json-rpc-filters "4.2.2" + eth-rpc-errors "4.0.2" + js-sha256 "0.9.0" + json-rpc-engine "6.1.0" + keccak "^3.0.1" + preact "^10.5.9" + qs "^6.10.3" + rxjs "^6.6.3" + stream-browserify "^3.0.0" + "@eslint/eslintrc@^1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.5.tgz#33f1b838dbf1f923bfa517e008362b78ddbbf318" @@ -1656,6 +1778,15 @@ "@jridgewell/set-array" "^1.0.0" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.1" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz#cf92a983c83466b8c0ce9124fadeaf09f7c66ea9" + integrity sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/resolve-uri@^3.0.3": version "3.0.7" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz#30cd49820a962aff48c8fffc5cd760151fca61fe" @@ -2406,6 +2537,11 @@ npmlog "^4.1.2" write-file-atomic "^3.0.3" +"@metamask/safe-event-emitter@2.0.0", "@metamask/safe-event-emitter@^2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz#af577b477c683fad17c619a78208cede06f9605c" + integrity sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q== + "@next/env@12.1.6": version "12.1.6" resolved "https://registry.yarnpkg.com/@next/env/-/env-12.1.6.tgz#5f44823a78335355f00f1687cfc4f1dafa3eca08" @@ -3775,6 +3911,13 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== +async-mutex@^0.2.6: + version "0.2.6" + resolved "https://registry.npmjs.org/async-mutex/-/async-mutex-0.2.6.tgz#0d7a3deb978bc2b984d5908a2038e1ae2e54ff40" + integrity sha512-Hs4R+4SPgamu6rSGW8C7cV9gaWUKEHykfzCCvIRuaVv636Ju10ZdeUbvb4TBEW0INuq2DHZqXbK4Nd3yG4RaRw== + dependencies: + tslib "^2.0.0" + async@~0.9.0: version "0.9.2" resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" @@ -3866,6 +4009,30 @@ babel-plugin-jest-hoist@^28.0.2: "@types/babel__core" "^7.1.14" "@types/babel__traverse" "^7.0.6" +babel-plugin-polyfill-corejs2@^0.3.0: + version "0.3.1" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz#440f1b70ccfaabc6b676d196239b138f8a2cfba5" + integrity sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w== + dependencies: + "@babel/compat-data" "^7.13.11" + "@babel/helper-define-polyfill-provider" "^0.3.1" + semver "^6.1.1" + +babel-plugin-polyfill-corejs3@^0.5.0: + version "0.5.2" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz#aabe4b2fa04a6e038b688c5e55d44e78cd3a5f72" + integrity sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.3.1" + core-js-compat "^3.21.0" + +babel-plugin-polyfill-regenerator@^0.3.0: + version "0.3.1" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz#2c0678ea47c75c8cc2fbb1852278d8fb68233990" + integrity sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.3.1" + babel-preset-current-node-syntax@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" @@ -3945,6 +4112,11 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +bind-decorator@^1.0.11: + version "1.0.11" + resolved "https://registry.npmjs.org/bind-decorator/-/bind-decorator-1.0.11.tgz#e41bc06a1f65dd9cec476c91c5daf3978488252f" + integrity sha512-yzkH0uog6Vv/vQ9+rhSKxecnqGUZHYncg7qS7voz3Q76+TAi1SGiOKk2mlOvusQnFz9Dc4BC/NMkeXu11YgjJg== + bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -3981,7 +4153,7 @@ bn.js@4.11.8: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.0, bn.js@^4.11.1, bn.js@^4.11.6, bn.js@^4.11.9: +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.0, bn.js@^4.11.1, bn.js@^4.11.6, bn.js@^4.11.8, bn.js@^4.11.9: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== @@ -4119,6 +4291,17 @@ browserslist@^4.20.2: node-releases "^2.0.3" picocolors "^1.0.0" +browserslist@^4.20.3: + version "4.20.4" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.20.4.tgz#98096c9042af689ee1e0271333dbc564b8ce4477" + integrity sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw== + dependencies: + caniuse-lite "^1.0.30001349" + electron-to-chromium "^1.4.147" + escalade "^3.1.1" + node-releases "^2.0.5" + picocolors "^1.0.0" + bs-logger@0.x: version "0.2.6" resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" @@ -4149,6 +4332,11 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" +btoa@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" + integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -4288,6 +4476,11 @@ caniuse-lite@^1.0.30001332: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001340.tgz#029a2f8bfc025d4820fafbfaa6259fd7778340c7" integrity sha512-jUNz+a9blQTQVu4uFcn17uAD8IDizPzQkIKh3LCJfg9BkyIqExYYdyc/ZSlWUSKb8iYiXxKsxbv4zYSvkqjrxw== +caniuse-lite@^1.0.30001349: + version "1.0.30001352" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001352.tgz#cc6f5da3f983979ad1e2cdbae0505dccaa7c6a12" + integrity sha512-GUgH8w6YergqPQDGWhJGt8GDRnY0L/iJVQcU3eJ46GYf52R8tk0Wxp0PymuFVZboJYXGiCqwozAYZNRjVj6IcA== + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -4464,6 +4657,16 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= +clone@^2.1.1: + version "2.1.2" + resolved "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== + +clsx@^1.1.0: + version "1.1.1" + resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" + integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== + cmd-shim@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-4.1.0.tgz#b3a904a6743e9fede4148c6f3800bf2a08135bdd" @@ -4707,6 +4910,14 @@ cookiejar@^2.1.1: resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA== +core-js-compat@^3.21.0: + version "3.22.8" + resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.22.8.tgz#46fa34ce1ddf742acd7f95f575f66bbb21e05d62" + integrity sha512-pQnwg4xtuvc2Bs/5zYQPaEYYSuTxsF7LBWF0SvnVhthZo/Qe+rJpcEekrdNK5DWwDJ0gv0oI9NNX5Mppdy0ctg== + dependencies: + browserslist "^4.20.3" + semver "7.0.0" + core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -4946,8 +5157,8 @@ decimal.js@^10.3.1: decode-uri-component@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og== decompress-response@^3.2.0, decompress-response@^3.3.0: version "3.3.0" @@ -5206,6 +5417,11 @@ electron-to-chromium@^1.4.118: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz#186180a45617283f1c012284458510cd99d6787f" integrity sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA== +electron-to-chromium@^1.4.147: + version "1.4.151" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.151.tgz#d1c09dd3a06cb81ef03a3bbbff6905827c33ab4b" + integrity sha512-XaG2LpZi9fdiWYOqJh0dJy4SlVywCvpgYXhzOlZTp4JqSKqxn5URqOjbm9OMYB3aInA2GuHQiem1QUOc1yT0Pw== + electron-to-chromium@^1.4.17: version "1.4.30" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.30.tgz#0f75a1dce26dffbd5a0f7212e5b87fe0b61cbc76" @@ -5636,6 +5852,18 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +eth-block-tracker@4.4.3: + version "4.4.3" + resolved "https://registry.npmjs.org/eth-block-tracker/-/eth-block-tracker-4.4.3.tgz#766a0a0eb4a52c867a28328e9ae21353812cf626" + integrity sha512-A8tG4Z4iNg4mw5tP1Vung9N9IjgMNqpiMoJ/FouSFwNCGHv2X0mmOYwtQOJzki6XN7r7Tyo01S29p7b224I4jw== + dependencies: + "@babel/plugin-transform-runtime" "^7.5.5" + "@babel/runtime" "^7.5.5" + eth-query "^2.1.0" + json-rpc-random-id "^1.0.1" + pify "^3.0.0" + safe-event-emitter "^1.0.1" + eth-ens-namehash@2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/eth-ens-namehash/-/eth-ens-namehash-2.0.8.tgz#229ac46eca86d52e0c991e7cb2aef83ff0f68bcf" @@ -5644,6 +5872,35 @@ eth-ens-namehash@2.0.8: idna-uts46-hx "^2.3.1" js-sha3 "^0.5.7" +eth-json-rpc-filters@4.2.2: + version "4.2.2" + resolved "https://registry.npmjs.org/eth-json-rpc-filters/-/eth-json-rpc-filters-4.2.2.tgz#eb35e1dfe9357ace8a8908e7daee80b2cd60a10d" + integrity sha512-DGtqpLU7bBg63wPMWg1sCpkKCf57dJ+hj/k3zF26anXMzkmtSBDExL8IhUu7LUd34f0Zsce3PYNO2vV2GaTzaw== + dependencies: + "@metamask/safe-event-emitter" "^2.0.0" + async-mutex "^0.2.6" + eth-json-rpc-middleware "^6.0.0" + eth-query "^2.1.2" + json-rpc-engine "^6.1.0" + pify "^5.0.0" + +eth-json-rpc-middleware@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/eth-json-rpc-middleware/-/eth-json-rpc-middleware-6.0.0.tgz#4fe16928b34231a2537856f08a5ebbc3d0c31175" + integrity sha512-qqBfLU2Uq1Ou15Wox1s+NX05S9OcAEL4JZ04VZox2NS0U+RtCMjSxzXhLFWekdShUPZ+P8ax3zCO2xcPrp6XJQ== + dependencies: + btoa "^1.2.1" + clone "^2.1.1" + eth-query "^2.1.2" + eth-rpc-errors "^3.0.0" + eth-sig-util "^1.4.2" + ethereumjs-util "^5.1.2" + json-rpc-engine "^5.3.0" + json-stable-stringify "^1.0.1" + node-fetch "^2.6.1" + pify "^3.0.0" + safe-event-emitter "^1.0.1" + eth-lib@0.2.8, eth-lib@^0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/eth-lib/-/eth-lib-0.2.8.tgz#b194058bef4b220ad12ea497431d6cb6aa0623c8" @@ -5665,6 +5922,43 @@ eth-lib@^0.1.26: ws "^3.0.0" xhr-request-promise "^0.1.2" +eth-query@^2.1.0, eth-query@^2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/eth-query/-/eth-query-2.1.2.tgz#d6741d9000106b51510c72db92d6365456a6da5e" + integrity sha512-srES0ZcvwkR/wd5OQBRA1bIJMww1skfGS0s8wlwK3/oNP4+wnds60krvu5R1QbpRQjMmpG5OMIWro5s7gvDPsA== + dependencies: + json-rpc-random-id "^1.0.0" + xtend "^4.0.1" + +eth-rpc-errors@4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/eth-rpc-errors/-/eth-rpc-errors-4.0.2.tgz#11bc164e25237a679061ac05b7da7537b673d3b7" + integrity sha512-n+Re6Gu8XGyfFy1it0AwbD1x0MUzspQs0D5UiPs1fFPCr6WAwZM+vbIhXheBFrpgosqN9bs5PqlB4Q61U/QytQ== + dependencies: + fast-safe-stringify "^2.0.6" + +eth-rpc-errors@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/eth-rpc-errors/-/eth-rpc-errors-3.0.0.tgz#d7b22653c70dbf9defd4ef490fd08fe70608ca10" + integrity sha512-iPPNHPrLwUlR9xCSYm7HHQjWBasor3+KZfRvwEWxMz3ca0yqnlBeJrnyphkGIXZ4J7AMAaOLmwy4AWhnxOiLxg== + dependencies: + fast-safe-stringify "^2.0.6" + +eth-rpc-errors@^4.0.2: + version "4.0.3" + resolved "https://registry.npmjs.org/eth-rpc-errors/-/eth-rpc-errors-4.0.3.tgz#6ddb6190a4bf360afda82790bb7d9d5e724f423a" + integrity sha512-Z3ymjopaoft7JDoxZcEb3pwdGh7yiYMhOwm2doUt6ASXlMavpNlK6Cre0+IMl2VSGyEU9rkiperQhp5iRxn5Pg== + dependencies: + fast-safe-stringify "^2.0.6" + +eth-sig-util@^1.4.2: + version "1.4.2" + resolved "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz#8d958202c7edbaae839707fba6f09ff327606210" + integrity sha512-iNZ576iTOGcfllftB73cPB5AN+XUQAT/T8xzsILsghXC1o8gJUqe3RHlcDqagu+biFpYQ61KQrZZJza8eRSYqw== + dependencies: + ethereumjs-abi "git+https://github.com/ethereumjs/ethereumjs-abi.git" + ethereumjs-util "^5.1.1" + eth-testing@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eth-testing/-/eth-testing-1.0.0.tgz#7020da1e4c873fec84cd2c437f8cd277b6b2d6e1" @@ -5700,6 +5994,13 @@ ethereum-cryptography@^0.1.3: secp256k1 "^4.0.1" setimmediate "^1.0.5" +"ethereumjs-abi@git+https://github.com/ethereumjs/ethereumjs-abi.git": + version "0.6.8" + resolved "git+https://github.com/ethereumjs/ethereumjs-abi.git#ee3994657fa7a427238e6ba92a84d0b529bbcde0" + dependencies: + bn.js "^4.11.8" + ethereumjs-util "^6.0.0" + ethereumjs-common@^1.3.2, ethereumjs-common@^1.5.0: version "1.5.2" resolved "https://registry.yarnpkg.com/ethereumjs-common/-/ethereumjs-common-1.5.2.tgz#2065dbe9214e850f2e955a80e650cb6999066979" @@ -5713,7 +6014,7 @@ ethereumjs-tx@^2.1.1: ethereumjs-common "^1.5.0" ethereumjs-util "^6.0.0" -ethereumjs-util@^5.2.0: +ethereumjs-util@^5.1.1, ethereumjs-util@^5.1.2, ethereumjs-util@^5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz#a833f0e5fca7e5b361384dc76301a721f537bf65" integrity sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ== @@ -5832,12 +6133,12 @@ eventemitter3@4.0.4: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384" integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ== -eventemitter3@^4.0.4: +eventemitter3@^4.0.4, eventemitter3@^4.0.7: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -events@^3.1.0, events@^3.3.0: +events@^3.0.0, events@^3.1.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -5850,7 +6151,7 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" -execa@^5.0.0, execa@^5.1.1: +execa@5.1.1, execa@^5.0.0, execa@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== @@ -6004,6 +6305,11 @@ fast-redact@^3.0.0: resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.0.2.tgz#c940ba7162dde3aeeefc522926ae8c5231412904" integrity sha512-YN+CYfCVRVMUZOUPeinHNKgytM1wPI/C/UCLEi56EsY2dwwvI00kIJHJoI7pMVqGoMew8SMZ2SSfHKHULHXDsg== +fast-safe-stringify@^2.0.6: + version "2.1.1" + resolved "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + fast-safe-stringify@^2.0.7, fast-safe-stringify@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.8.tgz#dc2af48c46cf712b683e849b2bbd446b32de936f" @@ -6056,8 +6362,8 @@ fill-range@^7.0.1: filter-obj@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" - integrity sha1-mzERErxsYSehbgFsbF1/GeCAXFs= + resolved "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" + integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ== finalhandler@~1.1.2: version "1.1.2" @@ -6208,7 +6514,7 @@ fsevents@^2.3.2, fsevents@~2.3.2: function-bind@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== functional-red-black-tree@^1.0.1: @@ -6240,7 +6546,16 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: +get-intrinsic@^1.0.2: + version "1.1.2" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598" + integrity sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== @@ -6534,6 +6849,11 @@ has-symbols@^1.0.1, has-symbols@^1.0.2: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + has-to-string-tag-x@^1.2.0: version "1.4.1" resolved "https://registry.yarnpkg.com/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz#a045ab383d7b4b2012a00148ab0aa5f290044d4d" @@ -6555,7 +6875,7 @@ has-unicode@^2.0.0, has-unicode@^2.0.1: has@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== dependencies: function-bind "^1.1.1" @@ -6795,7 +7115,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -7083,11 +7403,11 @@ is-shared-array-buffer@^1.0.1: integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== is-ssh@^1.3.0: - version "1.3.3" - resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.3.3.tgz#7f133285ccd7f2c2c7fc897b771b53d95a2b2c7e" - integrity sha512-NKzJmQzJfEEma3w5cJNcUMxoXfDjz0Zj0eyCalHn2E6VOwlzjZo0yuO2fcBSf8zhFuVCL/82/r5gRcoi6aEPVQ== + version "1.4.0" + resolved "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz#4f8220601d2839d8fa624b3106f8e8884f01b8b2" + integrity sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ== dependencies: - protocols "^1.1.0" + protocols "^2.0.1" is-stream@^1.0.0: version "1.1.0" @@ -7627,6 +7947,11 @@ joycon@^2.2.5: resolved "https://registry.yarnpkg.com/joycon/-/joycon-2.2.5.tgz#8d4cf4cbb2544d7b7583c216fcdfec19f6be1615" integrity sha512-YqvUxoOcVPnCp0VU1/56f+iKSdvIRJYPznH22BdXV3xMk75SFXhWeJkZ8C9XxUWt1b5x2X1SxuFygW1U0FmkEQ== +js-sha256@0.9.0: + version "0.9.0" + resolved "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966" + integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA== + js-sha3@0.5.7, js-sha3@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.5.7.tgz#0d4ffd8002d5333aabaf4a23eed2f6374c9f28e7" @@ -7715,6 +8040,27 @@ json-parse-even-better-errors@^2.3.0: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +json-rpc-engine@6.1.0, json-rpc-engine@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/json-rpc-engine/-/json-rpc-engine-6.1.0.tgz#bf5ff7d029e1c1bf20cb6c0e9f348dcd8be5a393" + integrity sha512-NEdLrtrq1jUZyfjkr9OCz9EzCNhnRyWtt1PAnvnhwy6e8XETS0Dtc+ZNCO2gvuAoKsIn2+vCSowXTYE4CkgnAQ== + dependencies: + "@metamask/safe-event-emitter" "^2.0.0" + eth-rpc-errors "^4.0.2" + +json-rpc-engine@^5.3.0: + version "5.4.0" + resolved "https://registry.npmjs.org/json-rpc-engine/-/json-rpc-engine-5.4.0.tgz#75758609d849e1dba1e09021ae473f3ab63161e5" + integrity sha512-rAffKbPoNDjuRnXkecTjnsE3xLLrb00rEkdgalINhaYVYIxDwWtvYBr9UFbhTvPB1B2qUOLoFd/cV6f4Q7mh7g== + dependencies: + eth-rpc-errors "^3.0.0" + safe-event-emitter "^1.0.1" + +json-rpc-random-id@^1.0.0, json-rpc-random-id@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/json-rpc-random-id/-/json-rpc-random-id-1.0.1.tgz#ba49d96aded1444dbb8da3d203748acbbcdec8c8" + integrity sha512-RJ9YYNCkhVDBuP4zN5BBtYAzEl03yq/jIIsyif0JY9qyJuQQZNeDK7anAPKKlyEtLSj2s8h6hNh2F8zO5q7ScA== + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -7730,6 +8076,13 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= +json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + integrity sha512-i/J297TW6xyj7sDFa7AmBPkQvLIxWr2kKPWI26tXydnZrzVAocNqn5DMNT1Mzk0vit1V5UkRM7C1KdVNp7Lmcg== + dependencies: + jsonify "~0.0.0" + json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -7770,6 +8123,11 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + integrity sha512-trvBk1ki43VZptdBI5rIlG4YOzyeH/WefQt5rj1grasPn4iiZWKet8nkgc4GlsAylaztn0qZfUYOiTsASJFdNA== + jsonparse@^1.2.0, jsonparse@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" @@ -7793,7 +8151,7 @@ jsprim@^1.2.2: array-includes "^3.1.2" object.assign "^4.1.2" -keccak@^3.0.0: +keccak@^3.0.0, keccak@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.2.tgz#4c2c6e8c54e04f2670ee49fa734eb9da152206e0" integrity sha512-PyKKjkH53wDMLGrvmRGSNWgmSxZOUqbnXwKL9tmgbFYA1iAYqW21kfR7mZXV0MlESiefxQQE9X9fTa3X+2MPDQ== @@ -7834,6 +8192,11 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +kleur@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.4.tgz#8c202987d7e577766d039a8cd461934c01cda04d" + integrity sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA== + lerna@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/lerna/-/lerna-4.0.0.tgz#b139d685d50ea0ca1be87713a7c2f44a5b678e9e" @@ -7998,6 +8361,11 @@ lodash._reinterpolate@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + lodash.ismatch@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" @@ -8657,6 +9025,11 @@ node-releases@^2.0.3: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.4.tgz#f38252370c43854dc48aa431c766c6c398f40476" integrity sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ== +node-releases@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz#280ed5bc3eba0d96ce44897d8aee478bfb3d9666" + integrity sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q== + node-ts@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/node-ts/-/node-ts-5.1.2.tgz#4e1fa51e87506e042c7ea2b1e465684a79532274" @@ -8717,7 +9090,7 @@ normalize-url@^4.1.0: normalize-url@^6.1.0: version "6.1.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== npm-bundled@^1.1.1: @@ -8858,7 +9231,7 @@ object-hash@^2.2.0: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== -object-inspect@^1.11.0, object-inspect@^1.9.0: +object-inspect@^1.11.0: version "1.11.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1" integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg== @@ -8868,6 +9241,11 @@ object-inspect@^1.11.1: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== +object-inspect@^1.9.0: + version "1.12.2" + resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -9167,10 +9545,10 @@ parse-json@^5.0.0, parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -parse-path@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/parse-path/-/parse-path-4.0.3.tgz#82d81ec3e071dcc4ab49aa9f2c9c0b8966bb22bf" - integrity sha512-9Cepbp2asKnWTJ9x2kpw6Fe8y9JDbqwahGCTvklzd/cEq5C5JC59x2Xb0Kx+x0QZ8bvNquGO8/BWP0cwBHzSAA== +parse-path@^4.0.4: + version "4.0.4" + resolved "https://registry.npmjs.org/parse-path/-/parse-path-4.0.4.tgz#4bf424e6b743fb080831f03b536af9fc43f0ffea" + integrity sha512-Z2lWUis7jlmXC1jeOG9giRO2+FsuyNipeQ43HAjqAZjwSe3SEf+q/84FGPHoso3kyntbxa4c4i77t3m6fGf8cw== dependencies: is-ssh "^1.3.0" protocols "^1.4.0" @@ -9178,13 +9556,13 @@ parse-path@^4.0.0: query-string "^6.13.8" parse-url@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-6.0.0.tgz#f5dd262a7de9ec00914939220410b66cff09107d" - integrity sha512-cYyojeX7yIIwuJzledIHeLUBVJ6COVLeT4eF+2P6aKVzwvgKQPndCBv3+yQ7pcWjqToYwaligxzSYNNmGoMAvw== + version "6.0.2" + resolved "https://registry.npmjs.org/parse-url/-/parse-url-6.0.2.tgz#4a30b057bfc452af64512dfb1a7755c103db3ea1" + integrity sha512-uCSjOvD3T+6B/sPWhR+QowAZcU/o4bjPrVBQBGFxcDF6J6FraCGIaDBsdoQawiaaAVdHvtqBe3w3vKlfBKySOQ== dependencies: is-ssh "^1.3.0" normalize-url "^6.1.0" - parse-path "^4.0.0" + parse-path "^4.0.4" protocols "^1.4.0" parse5@6.0.1: @@ -9398,6 +9776,11 @@ postcss@8.4.5, postcss@^8.4.5: picocolors "^1.0.0" source-map-js "^1.0.1" +preact@^10.5.9: + version "10.7.3" + resolved "https://registry.npmjs.org/preact/-/preact-10.7.3.tgz#f98c09a29cb8dbb22e5fc824a1edcc377fc42b5a" + integrity sha512-giqJXP8VbtA1tyGa3f1n9wiN7PrHtONrDyE3T+ifjr/tTkg+2N4d/6sjC9WyJKv8wM7rOYDveqy5ZoFmYlwo4w== + prebuild-install@^6.0.1: version "6.1.4" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.4.tgz#ae3c0142ad611d58570b89af4986088a4937e00f" @@ -9517,7 +9900,7 @@ prompt@^1.2.1: revalidator "0.1.x" winston "2.x" -prompts@^2.0.1: +prompts@^2.0.1, prompts@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== @@ -9546,11 +9929,16 @@ proto-list@~1.2.1: resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= -protocols@^1.1.0, protocols@^1.4.0: +protocols@^1.4.0: version "1.4.8" - resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.8.tgz#48eea2d8f58d9644a4a32caae5d5db290a075ce8" + resolved "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz#48eea2d8f58d9644a4a32caae5d5db290a075ce8" integrity sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg== +protocols@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz#8f155da3fc0f32644e83c5782c8e8212ccf70a86" + integrity sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q== + proxy-addr@~2.0.5: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -9621,10 +10009,17 @@ qs@6.7.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@^6.10.3: + version "6.10.5" + resolved "https://registry.npmjs.org/qs/-/qs-6.10.5.tgz#974715920a80ff6a262264acd2c7e6c2a53282b4" + integrity sha512-O5RlPh0VFtR78y79rgcgKK4wbAI0C5zGVLztOIdpWX6ep368q5Hv6XRxDvXuZ9q3C6v+e3n8UfZZJw7IIG27eQ== + dependencies: + side-channel "^1.0.4" + qs@^6.9.4: - version "6.10.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a" - integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg== + version "6.11.0" + resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== dependencies: side-channel "^1.0.4" @@ -9653,7 +10048,7 @@ query-string@^5.0.1: query-string@^6.13.5, query-string@^6.13.8: version "6.14.1" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a" + resolved "https://registry.npmjs.org/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a" integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw== dependencies: decode-uri-component "^0.2.0" @@ -9895,7 +10290,7 @@ read@1, read@1.0.x, read@~1.0.1: dependencies: mute-stream "~0.0.4" -readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: +readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -10026,7 +10421,7 @@ resolve@^1.10.0, resolve@^1.20.0: is-core-module "^2.2.0" path-parse "^1.0.6" -resolve@^1.21.0: +resolve@^1.14.2, resolve@^1.21.0: version "1.22.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== @@ -10119,7 +10514,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@6, rxjs@^6.6.0: +rxjs@6, rxjs@^6.6.0, rxjs@^6.6.3: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== @@ -10143,6 +10538,13 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-event-emitter@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/safe-event-emitter/-/safe-event-emitter-1.0.1.tgz#5b692ef22329ed8f69fdce607e50ca734f6f20af" + integrity sha512-e1wFe99A91XYYxoQbcq2ZJUWurxEyP8vfz7A7vuUe1s95q8r5ebraVaA1BukYJcpM6V16ugWoD9vngi8Ccu5fg== + dependencies: + events "^3.0.0" + safe-json-utils@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/safe-json-utils/-/safe-json-utils-1.1.1.tgz#0e883874467d95ab914c3f511096b89bfb3e63b1" @@ -10186,6 +10588,11 @@ secp256k1@^4.0.1: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +semver@7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" + integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== + semver@7.x, semver@^7.1.1, semver@^7.1.3, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: version "7.3.5" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" @@ -10193,11 +10600,18 @@ semver@7.x, semver@^7.1.1, semver@^7.1.3, semver@^7.3.2, semver@^7.3.4, semver@^ dependencies: lru-cache "^6.0.0" -semver@^6.0.0, semver@^6.3.0: +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.3.6: + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== + dependencies: + lru-cache "^6.0.0" + send@0.17.1: version "0.17.1" resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" @@ -10282,7 +10696,7 @@ shebang-regex@^3.0.0: side-channel@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== dependencies: call-bind "^1.0.0" @@ -10475,7 +10889,7 @@ spdx-license-ids@^3.0.0: split-on-first@^1.0.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" + resolved "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== split2@^3.0.0, split2@^3.1.1: @@ -10543,6 +10957,14 @@ stack-utils@^2.0.3: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +stream-browserify@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" + integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== + dependencies: + inherits "~2.0.4" + readable-stream "^3.5.0" + stream-combiner@~0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" @@ -10557,8 +10979,8 @@ strict-uri-encode@^1.0.0: strict-uri-encode@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" - integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= + resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" + integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== string-argv@^0.1.1: version "0.1.2" @@ -11080,6 +11502,11 @@ tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.0.0: + version "2.4.0" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + tslib@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" @@ -12218,7 +12645,7 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xtend@^4.0.0, xtend@^4.0.2, xtend@~4.0.1: +xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== @@ -12318,3 +12745,16 @@ yargs@^17.3.1: string-width "^4.2.3" y18n "^5.0.5" yargs-parser "^21.0.0" + +yargs@^17.4.1: + version "17.5.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e" + integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.0.0"