├── .env.example ├── .gitignore ├── README.MD ├── package.json ├── public └── index.html ├── src ├── components │ ├── App.js │ ├── Callback.js │ ├── ContractCall.js │ ├── Home.js │ ├── Info.js │ ├── Loading.js │ ├── Login.js │ └── SendTransaction.js ├── contract │ ├── HelloWorld.sol │ └── abi.js ├── index.js ├── magic.js └── styles.css └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_MAGIC_PUBLISHABLE_KEY=pk_live_ 2 | REACT_APP_ALCHEMY_API_KEY=abc123 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | /node_modules -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Resources 2 | - [GitHub Repo](https://github.com/magiclabs/magic-polygon) 3 | - [Demo](https://magic-polygon.vercel.app/login) 4 | 5 | # Quick Start 6 | 7 | ``` 8 | $ git clone https://github.com/magiclabs/magic-polygon.git 9 | $ cd magic-polygon 10 | $ mv .env.local .env // enter your Magic Publishable Key (from https://dashboard.magic.link) 11 | $ yarn install 12 | $ yarn start 13 | ``` 14 | 15 | # Introduction 16 | 17 | With the rising gas costs on Ethereum, many developers are looking towards scaling solutions to help with both improved transaction speed, as well as cheaper gas costs for users. Polygon (formerly Matic) is one such solution. 18 | 19 | Polygon is a protocol which enables connecting Ethereum-compatible blockchains, and is also a Proof of Stake side-chain scaling solution for Ethereum. The side-chain runs alongside Ethereum's blockchain, and processes transactions before finalizing them on Ethereum. 20 | 21 | With Magic, developers can connect to Polygon by simply specifying the network URL when initiating a Magic instance. This guide will show how you can create a web3-enabled app, allow users to switch between Ethereum and Polygon networks, call smart contracts, and send transactions. 22 | 23 | _Note: `ETH` is the native token to Ethereum, `MATIC` is the native token to Polygon._ 24 | 25 | # Tutorial 26 | 27 | _Note: this app was bootstrapped with the `npx make-magic` React template._ 28 | 29 | ## Connecting to Ethereum / Polgyon 30 | 31 | In `magic.js`, we will need two `Magic` and two `Web3` instances, one for each network, since we're allowing users to switch between the two. If you're only interested in connecting to Polygon, then only one instance of `Magic` and `Web3` should be created. We also are adding `magicEthereum.network = "ethereum"` to be able to identify the Magic network we're creating. 32 | 33 | You’ll use the same API key for both `Magic` instances so that the user’s public address does not change. 34 | 35 | ```js 36 | import { Magic } from 'magic-sdk'; 37 | import Web3 from 'web3'; 38 | 39 | /** 40 | * Configure Polygon Connection 41 | */ 42 | const polygonNodeOptions = { 43 | rpcUrl: 'https://rpc-mumbai.matic.today', 44 | chainId: 80001, 45 | }; 46 | 47 | export const magicMatic = new Magic( 48 | process.env.REACT_APP_MAGIC_PUBLISHABLE_KEY, 49 | { 50 | network: polygonNodeOptions, 51 | }, 52 | ); 53 | magicMatic.network = 'matic'; 54 | 55 | export const maticWeb3 = new Web3(magicMatic.rpcProvider); 56 | 57 | // Connect to Ethereum (Goerli Testnet) 58 | export const magicEthereum = new Magic( 59 | process.env.REACT_APP_MAGIC_PUBLISHABLE_KEY, 60 | { 61 | network: 'goerli', 62 | }, 63 | ); 64 | magicEthereum.network = 'ethereum'; 65 | 66 | export const ethWeb3 = new Web3(magicEthereum.rpcProvider); 67 | 68 | ``` 69 | 70 | ## Switching Between Networks 71 | 72 | Users are able to switch between the Ethereum and Polygon networks with the `select` element dropdown list. Since one `Magic` instance points towards Ethereum, and the other Polygon, we simply update the instance that we’re using for our app based on whichever network the user selects. 73 | 74 | ```js 75 | import { magicEthereum, magicMatic, ethWeb3, maticWeb3 } from "../magic"; 76 | 77 | const handleChangeNetwork = (e) => { 78 | e.target.value === 'ethereum' ? setMagic(magicEthereum) : setMagic(magicMatic); 79 | fetchBalance(userMetadata.publicAddress); 80 | fetchContractMessage(); 81 | } 82 | 83 | return ( 84 |
85 | 89 |
90 | ) 91 | ``` 92 | 93 | ## Viewing User Balance 94 | 95 | A user's public address will be the same on both Ethereum and Polygon (as long as you are using the same API key for each instance) so a simple `web3.eth.getBalance` call is all that is needed for either network. Because the native token of Ethereum is `ETH`, and for Polygon is `MATIC`, we're displaying the appropriate token symbol based on the network we're connected to. 96 | 97 | ```js 98 | const fetchBalance = (address) => { 99 | web3.eth.getBalance(address).then(bal => setBalance(web3.utils.fromWei(bal))) 100 | } 101 | 102 | return ( 103 |

Balance

104 |
105 | {balance.toString().substring(0, 6)} {magic.network === 'matic' ? 'MATIC' : 'ETH'} 106 |
107 | ) 108 | ``` 109 | 110 | ## Send Transaction 111 | 112 | Sending a transaction is also very simple and the same for either network you're connected to. All that's needed is to provide an amount to send, and `from` and `to` addresses. If no `gas` or `gasPrice` are explicitly passed in, the gas limit and price will be calculated automatically. Otherwise, the values passed in will be used. 113 | 114 | ```js 115 | const web3 = magic.network === "ethereum" ? ethWeb3 : maticWeb3; 116 | 117 | const sendTransaction = async () => { 118 | if (!toAddress || !amount) return; 119 | const receipt = await web3.eth.sendTransaction({ 120 | from: publicAddress, 121 | to: toAddress, 122 | value: web3.utils.toWei(amount) 123 | }); 124 | } 125 | 126 | return ( 127 |
128 |

Send Transaction

129 | setToAddress(e.target.value)} 133 | placeholder="To Address" 134 | /> 135 | setAmount(e.target.value)} 139 | placeholder="Amount" 140 | /> 141 | 142 |
143 | ) 144 | ``` 145 | 146 | ## Calling Smart Contracts 147 | 148 | Separate smart contracts will need to be deployed on each Ethereum and Polygon for your users to interact with them. So you'll also need to dynamically know the correct address that the contract is deployed to in order to call it. 149 | 150 | ```js 151 | const network = magic.network === "ethereum" ? 'ethereum' : 'matic'; 152 | const goerliContractAddress = '0x8cb46E4bFc14Ce010dFbE5Ecb61BA64d798D3A67'; 153 | const maticContractAddress = '0x9ebE0B009146643bb3560375A4562D8d89E135e9'; 154 | const contract = new web3.eth.Contract(abi, network === "ethereum" ? goerliContractAddress : maticContractAddress); 155 | 156 | // Grabbing `message` variable value stored in the smart contract 157 | const fetchContractMessage = () => contract.methods.message().call().then(setMessage) 158 | 159 | // Update contract `message` value on the blockchain 160 | const updateContractMessage = async () => { 161 | if (!newMessage) return; 162 | const receipt = await contract.methods.update(newMessage).send({ from: user.publicAddress }); 163 | } 164 | 165 | return ( 166 |

Contract Message

167 |
{message}
168 | 169 |

Update Message

170 | setNewMessage(e.target.value)} 174 | placeholder="New Message" /> 175 | 176 | 177 | ) 178 | ``` 179 | 180 | ## Done 181 | 182 | That's all there is to it! You've now got an app that allows users to create a wallet with just their email, and connect to multiple networks within your app. 183 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magic-polygon", 3 | "version": "0.1.0", 4 | "description": "A React + JS app with Magic passwordless authentication.", 5 | "scripts": { 6 | "start": "react-scripts start", 7 | "build": "react-scripts build", 8 | "test": "react-scripts test", 9 | "eject": "react-scripts eject" 10 | }, 11 | "dependencies": { 12 | "magic-sdk": "^8.0.1", 13 | "react": "^16.13.1", 14 | "react-dom": "^16.13.1", 15 | "react-router": "^5.2.0", 16 | "react-router-dom": "^5.2.0", 17 | "web3": "^1.3.4" 18 | }, 19 | "devDependencies": { 20 | "react-scripts": "4.0.2" 21 | }, 22 | "browserslist": { 23 | "production": [ 24 | ">0.2%", 25 | "not dead", 26 | "not op_mini all" 27 | ], 28 | "development": [ 29 | "last 1 chrome version", 30 | "last 1 firefox version", 31 | "last 1 safari version" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 23 | Magic + Polygon 24 | 25 | 26 | 27 | 28 |
29 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Switch } from "react-router"; 3 | import { BrowserRouter } from "react-router-dom"; 4 | 5 | // Views 6 | import Login from "./Login"; 7 | import Callback from "./Callback"; 8 | import Home from "./Home"; 9 | 10 | export default function App() { 11 | return ( 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 | ); 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/components/Callback.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useHistory } from "react-router"; 3 | import { magicEthereum } from "../magic"; 4 | import Loading from "./Loading"; 5 | 6 | export default function Callback() { 7 | const history = useHistory(); 8 | 9 | useEffect(() => { 10 | // On mount, we try to login with a Magic credential in the URL query. 11 | magicEthereum.auth.loginWithCredential().finally(() => { 12 | history.push("/"); 13 | }); 14 | }, []); 15 | 16 | return ; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/components/ContractCall.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from "react"; 2 | 3 | export default function ContractCall({ network, user, fetchBalance, message, contract, fetchContractMessage }) { 4 | const [newMessage, setNewMessage] = useState(''); 5 | const [disabled, setDisabled] = useState(false); 6 | const [txnHash, setTxnHash] = useState(); 7 | const updateBtnRef = useRef(); 8 | 9 | // Update contract `message` value on the blockchain 10 | const updateContractMessage = async () => { 11 | if (!newMessage) return; 12 | disableForm(); 13 | const receipt = await contract.methods.update(newMessage).send({ from: user.publicAddress }); 14 | setTxnHash(receipt.transactionHash); 15 | enableForm(); 16 | } 17 | 18 | // Disable input form while the transaction is being confirmed 19 | const disableForm = () => { 20 | setTxnHash(); // Clear link to previous transaction hash 21 | setDisabled(true); 22 | updateBtnRef.current.innerText = 'Submitted...'; 23 | } 24 | 25 | // Re-enable input form once the transaction is confirmed 26 | const enableForm = () => { 27 | setDisabled(false); 28 | setNewMessage(''); 29 | fetchBalance(user.publicAddress); 30 | fetchContractMessage() 31 | updateBtnRef.current.innerText = 'Update'; 32 | } 33 | 34 | 35 | return ( 36 |
37 |

Contract Message

38 |
{message}
39 | 40 |

Update Message

41 | setNewMessage(e.target.value)} className="full-width" placeholder="New Message" /> 42 | 43 | { 44 | txnHash && 45 |
46 | 47 | View Transaction 48 | ↗️ 49 |
50 | } 51 |
52 | ) 53 | } -------------------------------------------------------------------------------- /src/components/Home.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useHistory } from "react-router"; 3 | import { magicEthereum, magicMatic, ethWeb3, maticWeb3 } from "../magic"; 4 | import Loading from "./Loading"; 5 | import ContractCall from "./ContractCall"; 6 | import SendTransaction from './SendTransaction'; 7 | import Info from "./Info"; 8 | import { abi } from '../contract/abi.js'; 9 | 10 | export default function Home() { 11 | const [magic, setMagic] = useState(magicEthereum); 12 | const web3 = magic.network === "ethereum" ? ethWeb3 : maticWeb3; 13 | const [userMetadata, setUserMetadata] = useState(); 14 | const [balance, setBalance] = useState('...'); 15 | const network = magic.network === "ethereum" ? 'ethereum' : 'matic'; 16 | const goerliContractAddress = '0x3EA3913A352cDd49889c7b0dEc8Dd9491d063453'; 17 | const maticContractAddress = '0xfD827cC6d5b959287D7e1680dBA587ffE5dFcbB4'; 18 | const contract = new web3.eth.Contract(abi, network === "ethereum" ? goerliContractAddress : maticContractAddress); 19 | const [message, setMessage] = useState('...'); 20 | const history = useHistory(); 21 | 22 | useEffect(() => { 23 | // On mount, we check if a user is logged in. 24 | // If so, we'll retrieve the authenticated user's profile, balance and contract message. 25 | magic.user.isLoggedIn().then(magicIsLoggedIn => { 26 | if (magicIsLoggedIn) { 27 | magic.user.getMetadata().then(user => { 28 | setUserMetadata(user); 29 | fetchBalance(user.publicAddress); 30 | fetchContractMessage(); 31 | }); 32 | } else { 33 | // If no user is logged in, redirect to `/login` 34 | history.push("/login"); 35 | } 36 | }); 37 | }, [magic]); 38 | 39 | const handleChangeNetwork = (e) => { 40 | e.target.value === 'ethereum' ? setMagic(magicEthereum) : setMagic(magicMatic); 41 | fetchBalance(userMetadata.publicAddress); 42 | fetchContractMessage(); 43 | } 44 | 45 | const fetchBalance = (address) => { 46 | web3.eth.getBalance(address).then(bal => setBalance(web3.utils.fromWei(bal))) 47 | } 48 | 49 | const fetchContractMessage = () => contract.methods.message().call().then(setMessage) 50 | 51 | return ( 52 | userMetadata ? ( 53 | <> 54 | 55 | 56 | 57 | 58 | ) : 59 | ); 60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/components/Info.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | import { useHistory } from "react-router"; 3 | 4 | export default function Info({ user, magic, handleChangeNetwork, balance }) { 5 | const history = useHistory(); 6 | 7 | const logout = useCallback(() => { 8 | magic.user.logout().then(() => { 9 | history.push("/login"); 10 | }) 11 | }, [history]); 12 | 13 | return ( 14 | <> 15 |
16 |

Current user: {user.email}

17 | 18 |
19 | 20 |
21 |

Network

22 |
23 | 27 |
28 |

Public Address

29 |
{user.publicAddress}
30 |

Balance

31 |
{balance.toString().substring(0, 6)} {magic.network === 'matic' ? 'MATIC' : 'ETH'}
32 | 33 | 34 |
35 | 36 | ) 37 | } -------------------------------------------------------------------------------- /src/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 |

Loading...

7 |
8 | ); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/components/Login.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from "react"; 2 | import { useHistory } from "react-router"; 3 | import { magicEthereum } from "../magic"; 4 | 5 | export default function Login() { 6 | const [email, setEmail] = useState(""); 7 | const [isLoggingIn, setIsLoggingIn] = useState(false); 8 | const history = useHistory(); 9 | 10 | /** 11 | * Perform login action via Magic's passwordless flow. Upon successuful 12 | * completion of the login flow, a user is redirected to the homepage. 13 | */ 14 | const login = useCallback(async () => { 15 | setIsLoggingIn(true); 16 | 17 | try { 18 | await magicEthereum.auth.loginWithMagicLink({ 19 | email, 20 | redirectURI: new URL("/callback", window.location.origin).href, 21 | }); 22 | history.push("/"); 23 | } catch { 24 | setIsLoggingIn(false); 25 | } 26 | }, [email]); 27 | 28 | /** 29 | * Saves the value of our email input into component state. 30 | */ 31 | const handleInputOnChange = useCallback(event => { 32 | setEmail(event.target.value); 33 | }, []); 34 | 35 | return ( 36 |
37 |

Please sign up or login

38 | 46 | 47 |
48 | ); 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/components/SendTransaction.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from "react"; 2 | 3 | export default function SendTransaction({ web3, network, publicAddress, fetchBalance }) { 4 | const [toAddress, setToAddress] = useState(''); 5 | const [amount, setAmount] = useState(''); 6 | const [disabled, setDisabled] = useState(false); 7 | const [txnHash, setTxnHash] = useState(); 8 | 9 | const sendTxBtnRef = useRef(); 10 | 11 | const sendTransaction = async () => { 12 | if (!toAddress || !amount) return; 13 | disableForm() 14 | const receipt = await web3.eth.sendTransaction({ 15 | from: publicAddress, 16 | to: toAddress, 17 | value: web3.utils.toWei(amount) 18 | }); 19 | setTxnHash(receipt.transactionHash); 20 | enableForm() 21 | } 22 | 23 | // Disable input form while the transaction is being confirmed 24 | const disableForm = () => { 25 | setTxnHash(); 26 | setDisabled(true); 27 | sendTxBtnRef.current.innerText = 'Submitted...'; 28 | } 29 | 30 | // Re-enable input form once the transaction is confirmed 31 | const enableForm = () => { 32 | setDisabled(false); 33 | setToAddress(''); 34 | setAmount(''); 35 | fetchBalance(publicAddress); 36 | sendTxBtnRef.current.innerText = 'Send Transaction'; 37 | } 38 | 39 | 40 | return ( 41 |
42 |

Send Transaction

43 | setToAddress(e.target.value)} className="full-width" placeholder="To Address" /> 44 | setAmount(e.target.value)} className="full-width" placeholder="Amount" /> 45 | 46 | { 47 | txnHash && 48 | 53 | } 54 |
55 | ) 56 | } -------------------------------------------------------------------------------- /src/contract/HelloWorld.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity >=0.7.0 <0.9.0; 4 | 5 | contract HelloWorld { 6 | string public message = "first message"; 7 | 8 | function update(string memory newMessage) public { 9 | message = newMessage; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/contract/abi.js: -------------------------------------------------------------------------------- 1 | export const abi = [ 2 | { 3 | "inputs": [], 4 | "name": "message", 5 | "outputs": [ 6 | { 7 | "internalType": "string", 8 | "name": "", 9 | "type": "string" 10 | } 11 | ], 12 | "stateMutability": "view", 13 | "type": "function" 14 | }, 15 | { 16 | "inputs": [ 17 | { 18 | "internalType": "string", 19 | "name": "newMessage", 20 | "type": "string" 21 | } 22 | ], 23 | "name": "update", 24 | "outputs": [], 25 | "stateMutability": "nonpayable", 26 | "type": "function" 27 | } 28 | ] -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "react-dom"; 3 | import App from "./components/App"; 4 | 5 | import "./styles.css"; 6 | 7 | render(, document.getElementById("root")); 8 | -------------------------------------------------------------------------------- /src/magic.js: -------------------------------------------------------------------------------- 1 | import { Magic } from 'magic-sdk'; 2 | import Web3 from 'web3'; 3 | 4 | /** 5 | * Configure Polygon Connection 6 | */ 7 | const polygonNodeOptions = { 8 | rpcUrl: 'https://rpc-mumbai.matic.today', 9 | chainId: 80001, 10 | }; 11 | 12 | export const magicMatic = new Magic( 13 | process.env.REACT_APP_MAGIC_PUBLISHABLE_KEY, 14 | { 15 | network: polygonNodeOptions, 16 | }, 17 | ); 18 | magicMatic.network = 'matic'; 19 | 20 | export const maticWeb3 = new Web3(magicMatic.rpcProvider); 21 | 22 | // Connect to Ethereum (Goerli Testnet) 23 | export const magicEthereum = new Magic( 24 | process.env.REACT_APP_MAGIC_PUBLISHABLE_KEY, 25 | { 26 | network: 'goerli', 27 | }, 28 | ); 29 | magicEthereum.network = 'ethereum'; 30 | 31 | export const ethWeb3 = new Web3(magicEthereum.rpcProvider); 32 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | align-self: center; 3 | justify-self: center; 4 | } 5 | 6 | #root { 7 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 8 | Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 9 | height: 100vh; 10 | display: grid; 11 | font-size: 18px; 12 | } 13 | 14 | .container { 15 | min-width: 300px; 16 | background-color: #eee; 17 | text-align: center; 18 | padding: 27px 18px; 19 | margin-bottom: 27px; 20 | } 21 | 22 | h1 { 23 | margin: 0; 24 | padding-bottom: 18px; 25 | font-size: 18px; 26 | } 27 | 28 | .info { 29 | max-width: 21ch; 30 | margin: 0 auto; 31 | margin-bottom: 18px; 32 | background-color: #ddd; 33 | padding: 12px 24px; 34 | word-wrap: break-word; 35 | font-family: "Lucida Console", Monaco, monospace; 36 | font-size: 15px; 37 | } 38 | 39 | a, a:visited { 40 | color: black; 41 | text-decoration: none; 42 | } 43 | 44 | a:hover { 45 | text-decoration: underline; 46 | } 47 | 48 | input, 49 | button { 50 | padding: 9px; 51 | font-size: 18px; 52 | margin-bottom: 9px; 53 | } 54 | 55 | input.full-width { 56 | display: block; 57 | margin: 0 auto; 58 | margin-bottom: 9px; 59 | text-align: center; 60 | } 61 | 62 | select, option { 63 | background-color: #dddddd; 64 | color: black; 65 | border: none; 66 | } --------------------------------------------------------------------------------