Skip to content

Commit 54e43ee

Browse files
Add Magic (Uniswap#116)
* Add Magic * ESLint fixes * support default import Co-authored-by: Noah Zinsmeister <[email protected]>
1 parent 0596631 commit 54e43ee

File tree

11 files changed

+724
-1
lines changed

11 files changed

+724
-1
lines changed

docs/connectors/magic.md

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# `web3-react` Documentation - Magic
2+
3+
- [Install](#install)
4+
- [Arguments](#arguments)
5+
- [Example](#example)
6+
7+
## Install
8+
9+
`yarn add @web3-react/magic-connector`
10+
11+
## Arguments
12+
13+
```typescript
14+
apiKey: string
15+
chainId: number
16+
email: string
17+
```
18+
19+
## Example
20+
21+
```javascript
22+
import { MagicConnector } from '@web3-react/magic-connector'
23+
24+
const magic = new MagicConnector({ apiKey: '', chainId: 4, email: '' })
25+
```
26+
27+
Note: Once the connector has been activated, the Magic SDK instance can be accessed under the `.magic` property.
28+
29+
## Errors
30+
31+
### UserRejectedRequestError
32+
33+
Happens when the user closes the connection window.
34+
35+
#### Example
36+
37+
```javascript
38+
import { UserRejectedRequestError } from '@web3-react/magic-connector'
39+
40+
function Component() {
41+
const { error } = useWeb3React()
42+
const isNoEthereumProviderError = error instanceof UserRejectedRequestError
43+
// ...
44+
}
45+
```
46+
47+
### FailedVerificationError
48+
49+
Happens when the Magic link verification fails.
50+
51+
#### Example
52+
53+
```javascript
54+
import { FailedVerificationError } from '@web3-react/magic-connector'
55+
56+
function Component() {
57+
const { error } = useWeb3React()
58+
const isNoEthereumProviderError = error instanceof FailedVerificationError
59+
// ...
60+
}
61+
```
62+
63+
### MagicLinkRateLimitError
64+
65+
Happens when the Magic rate limit has been reached.
66+
67+
#### Example
68+
69+
```javascript
70+
import { MagicLinkRateLimitError } from '@web3-react/magic-connector'
71+
72+
function Component() {
73+
const { error } = useWeb3React()
74+
const isNoEthereumProviderError = error instanceof MagicLinkRateLimitError
75+
// ...
76+
}
77+
```
78+
79+
### MagicLinkExpiredError
80+
81+
Happens when the Magic link has expired.
82+
83+
#### Example
84+
85+
```javascript
86+
import { MagicLinkExpiredError } from '@web3-react/magic-connector'
87+
88+
function Component() {
89+
const { error } = useWeb3React()
90+
const isNoEthereumProviderError = error instanceof MagicLinkExpiredError
91+
// ...
92+
}
93+
```

example/connectors.ts

+7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { TrezorConnector } from '@web3-react/trezor-connector'
77
import { FrameConnector } from '@web3-react/frame-connector'
88
import { AuthereumConnector } from '@web3-react/authereum-connector'
99
import { FortmaticConnector } from '@web3-react/fortmatic-connector'
10+
import { MagicConnector } from '@web3-react/magic-connector'
1011
import { PortisConnector } from '@web3-react/portis-connector'
1112
import { SquarelinkConnector } from '@web3-react/squarelink-connector'
1213
import { TorusConnector } from '@web3-react/torus-connector'
@@ -52,6 +53,12 @@ export const authereum = new AuthereumConnector({ chainId: 42 })
5253

5354
export const fortmatic = new FortmaticConnector({ apiKey: process.env.FORTMATIC_API_KEY as string, chainId: 4 })
5455

56+
export const magic = new MagicConnector({
57+
apiKey: process.env.MAGIC_API_KEY as string,
58+
chainId: 4,
59+
email: process.env.MAGIC_EMAIL,
60+
})
61+
5562
export const portis = new PortisConnector({ dAppId: process.env.PORTIS_DAPP_ID as string, networks: [1, 100] })
5663

5764
export const squarelink = new SquarelinkConnector({

example/next.config.js

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ module.exports = {
33
RPC_URL_1: 'https://mainnet.infura.io/v3/60ab76e16df54c808e50a79975b4779f',
44
RPC_URL_4: 'https://rinkeby.infura.io/v3/60ab76e16df54c808e50a79975b4779f',
55
FORTMATIC_API_KEY: 'pk_test_A6260FCBAA2EBDFB',
6+
MAGIC_API_KEY: 'pk_test_398B82F5F0E88874',
7+
MAGIC_EMAIL: '[email protected]',
68
PORTIS_DAPP_ID: 'e9be171c-2b7f-4ff0-8db9-327707511ee2',
79
SQUARELINK_CLIENT_ID: '5f2a2233db82b06b24f9'
810
}

example/pages/index.tsx

+17
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
frame,
2121
authereum,
2222
fortmatic,
23+
magic,
2324
portis,
2425
squarelink,
2526
torus
@@ -36,6 +37,7 @@ enum ConnectorNames {
3637
Frame = 'Frame',
3738
Authereum = 'Authereum',
3839
Fortmatic = 'Fortmatic',
40+
Magic = 'Magic',
3941
Portis = 'Portis',
4042
Squarelink = 'Squarelink',
4143
Torus = 'Torus'
@@ -51,6 +53,7 @@ const connectorsByName: { [connectorName in ConnectorNames]: any } = {
5153
[ConnectorNames.Frame]: frame,
5254
[ConnectorNames.Authereum]: authereum,
5355
[ConnectorNames.Fortmatic]: fortmatic,
56+
[ConnectorNames.Magic]: magic,
5457
[ConnectorNames.Portis]: portis,
5558
[ConnectorNames.Squarelink]: squarelink,
5659
[ConnectorNames.Torus]: torus
@@ -417,6 +420,20 @@ function App() {
417420
Kill Fortmatic Session
418421
</button>
419422
)}
423+
{connector === connectorsByName[ConnectorNames.Magic] && (
424+
<button
425+
style={{
426+
height: '3rem',
427+
borderRadius: '1rem',
428+
cursor: 'pointer'
429+
}}
430+
onClick={() => {
431+
;(connector as any).close()
432+
}}
433+
>
434+
Kill Magic Session
435+
</button>
436+
)}
420437
{connector === connectorsByName[ConnectorNames.Portis] && (
421438
<>
422439
{chainId !== undefined && (

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"portis",
2222
"squarelink",
2323
"torus",
24-
"authereum"
24+
"authereum",
25+
"magic"
2526
],
2627
"author": "Noah Zinsmeister <[email protected]>",
2728
"repository": {

packages/magic-connector/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# @web3-react/magic-connector
2+
3+
Please visit the [parent `web3-react` repository](https://github.com/NoahZinsmeister/web3-react) for documentation and details on this package.

packages/magic-connector/package.json

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "@web3-react/magic-connector",
3+
"publishConfig": {
4+
"access": "public"
5+
},
6+
"version": "6.0.9",
7+
"description": "A simple, maximally extensible, dependency minimized framework for building modern Ethereum dApps",
8+
"keywords": [
9+
"react",
10+
"react-hooks",
11+
"hooks",
12+
"ethereum",
13+
"javascript",
14+
"typescript",
15+
"web3",
16+
"context",
17+
"frontend",
18+
"dapp",
19+
"magic"
20+
],
21+
"author": "Noah Zinsmeister <[email protected]>",
22+
"repository": {
23+
"type": "git",
24+
"url": "git://github.com/NoahZinsmeister/web3-react.git"
25+
},
26+
"main": "dist/index.js",
27+
"module": "dist/magic-connector.esm.js",
28+
"typings": "dist/index.d.ts",
29+
"files": [
30+
"dist"
31+
],
32+
"scripts": {
33+
"start": "tsdx watch",
34+
"build": "tsdx build",
35+
"lint": "tsdx lint src"
36+
},
37+
"dependencies": {
38+
"@web3-react/abstract-connector": "^6.0.7",
39+
"@web3-react/types": "^6.0.7",
40+
"magic-sdk": "^2.4.7",
41+
"tiny-invariant": "^1.0.6"
42+
},
43+
"license": "GPL-3.0-or-later"
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
declare module 'magic'

packages/magic-connector/src/index.ts

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { ConnectorUpdate } from '@web3-react/types'
2+
import { AbstractConnector } from '@web3-react/abstract-connector'
3+
import invariant from 'tiny-invariant'
4+
5+
type NetworkName = 'mainnet' | 'ropsten' | 'rinkeby' | 'kovan'
6+
7+
const chainIdToNetwork: { [network: number]: NetworkName } = {
8+
1: 'mainnet',
9+
3: 'ropsten',
10+
4: 'rinkeby',
11+
42: 'kovan'
12+
}
13+
14+
interface MagicConnectorArguments {
15+
apiKey: string
16+
chainId: number
17+
email: string
18+
}
19+
20+
export class UserRejectedRequestError extends Error {
21+
public constructor() {
22+
super()
23+
this.name = this.constructor.name
24+
this.message = 'The user rejected the request.'
25+
}
26+
}
27+
28+
export class FailedVerificationError extends Error {
29+
public constructor() {
30+
super()
31+
this.name = this.constructor.name
32+
this.message = 'The email verification failed.'
33+
}
34+
}
35+
36+
export class MagicLinkRateLimitError extends Error {
37+
public constructor() {
38+
super()
39+
this.name = this.constructor.name
40+
this.message = 'The Magic rate limit has been reached.'
41+
}
42+
}
43+
44+
export class MagicLinkExpiredError extends Error {
45+
public constructor() {
46+
super()
47+
this.name = this.constructor.name
48+
this.message = 'The Magic link has expired.'
49+
}
50+
}
51+
52+
export class MagicConnector extends AbstractConnector {
53+
private readonly apiKey: string
54+
private readonly chainId: number
55+
private readonly email: string
56+
57+
public magic: any
58+
59+
constructor({ apiKey, chainId, email }: MagicConnectorArguments) {
60+
invariant(Object.keys(chainIdToNetwork).includes(chainId.toString()), `Unsupported chainId ${chainId}`)
61+
invariant(email && email.includes('@'), `Invalid email: ${email}`)
62+
super({ supportedChainIds: [chainId] })
63+
64+
this.apiKey = apiKey
65+
this.chainId = chainId
66+
this.email = email
67+
}
68+
69+
public async activate(): Promise<ConnectorUpdate> {
70+
const MagicSDK = await import('magic-sdk').then(m => m?.default ?? m)
71+
const { Magic, RPCError, RPCErrorCode } = MagicSDK
72+
73+
if (!this.magic) {
74+
this.magic = new Magic(this.apiKey, { network: chainIdToNetwork[this.chainId] })
75+
}
76+
77+
const isLoggedIn = await this.magic.user.isLoggedIn()
78+
const loggedInEmail = isLoggedIn ? (await this.magic.user.getMetadata()).email : null
79+
80+
if (isLoggedIn && loggedInEmail !== this.email) {
81+
await this.magic.user.logout()
82+
}
83+
84+
if (!isLoggedIn) {
85+
try {
86+
await this.magic.auth.loginWithMagicLink({ email: this.email })
87+
} catch (err) {
88+
if (!(err instanceof RPCError)) {
89+
throw err
90+
}
91+
if (err.code === RPCErrorCode.MagicLinkFailedVerification) {
92+
throw new FailedVerificationError()
93+
}
94+
if (err.code === RPCErrorCode.MagicLinkExpired) {
95+
throw new MagicLinkExpiredError()
96+
}
97+
if (err.code === RPCErrorCode.MagicLinkRateLimited) {
98+
throw new MagicLinkRateLimitError()
99+
}
100+
// This error gets thrown when users close the login window.
101+
// -32603 = JSON-RPC InternalError
102+
if (err.code === -32603) {
103+
throw new UserRejectedRequestError()
104+
}
105+
}
106+
}
107+
108+
const provider = this.magic.rpcProvider
109+
const account = await provider.enable().then((accounts: string[]): string => accounts[0])
110+
111+
return { provider, chainId: this.chainId, account }
112+
}
113+
114+
public async getProvider(): Promise<any> {
115+
return this.magic.rpcProvider
116+
}
117+
118+
public async getChainId(): Promise<number | string> {
119+
return this.chainId
120+
}
121+
122+
public async getAccount(): Promise<null | string> {
123+
return this.magic.rpcProvider.send('eth_accounts').then((accounts: string[]): string => accounts[0])
124+
}
125+
126+
public deactivate() {}
127+
128+
public async close() {
129+
await this.magic.user.logout()
130+
this.emitDeactivate()
131+
}
132+
}
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"include": ["src"],
4+
"compilerOptions": {
5+
"rootDir": "./",
6+
"baseUrl": "./",
7+
"paths": {
8+
"*": ["src/*", "node_modules/*"]
9+
}
10+
}
11+
}

0 commit comments

Comments
 (0)