├── .eslintrc
├── .gitignore
├── .prettierignore
├── .prettierrc
├── App.tsx
├── README.md
├── app.json
├── assets
├── adaptive-icon.png
├── favicon.png
├── icon.png
└── splash.png
├── babel.config.js
├── package-lock.json
├── package.json
├── polyfills.ts
├── src
├── clients
│ ├── public.ts
│ └── walletConnect.ts
└── pages
│ └── HomePage.tsx
└── tsconfig.json
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@callstack",
3 | "rules": {
4 | "promise/always-return": "off"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 |
11 | # Native
12 | *.orig.*
13 | *.jks
14 | *.p8
15 | *.p12
16 | *.key
17 | *.mobileprovision
18 |
19 | # Metro
20 | .metro-health-check*
21 |
22 | # debug
23 | npm-debug.*
24 | yarn-debug.*
25 | yarn-error.*
26 |
27 | # macOS
28 | .DS_Store
29 | *.pem
30 |
31 | # local env files
32 | .env*.local
33 |
34 | # typescript
35 | *.tsbuildinfo
36 |
37 | # JetBrains
38 | .idea
39 |
40 | # ENV
41 | .env
42 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 |
11 | # Native
12 | *.orig.*
13 | *.jks
14 | *.p8
15 | *.p12
16 | *.key
17 | *.mobileprovision
18 |
19 | # Metro
20 | .metro-health-check*
21 |
22 | # debug
23 | npm-debug.*
24 | yarn-debug.*
25 | yarn-error.*
26 |
27 | # macOS
28 | .DS_Store
29 | *.pem
30 |
31 | # local env files
32 | .env*.local
33 |
34 | # typescript
35 | *.tsbuildinfo
36 |
37 | # JetBrains
38 | .idea
39 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "bracketSameLine": true,
4 | "bracketSpacing": true,
5 | "singleQuote": true,
6 | "trailingComma": "all"
7 | }
8 |
--------------------------------------------------------------------------------
/App.tsx:
--------------------------------------------------------------------------------
1 | import './polyfills';
2 | import React from 'react';
3 | import { StatusBar } from 'expo-status-bar';
4 | import { StyleSheet, View } from 'react-native';
5 | import { WalletConnectModal } from '@walletconnect/modal-react-native';
6 | import HomePage from './src/pages/HomePage';
7 | import { providerMetadata } from './src/clients/walletConnect';
8 |
9 | export default function App() {
10 | return (
11 |
12 |
13 |
14 |
18 |
19 |
20 |
21 | );
22 | }
23 |
24 | const styles = StyleSheet.create({
25 | container: {
26 | flex: 1,
27 | marginHorizontal: 20,
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # web3-react-native-dapp-viem
2 |
3 | This repository showcases how to build a modern web3 dApp with React Native, [viem](https://viem.sh) and [WalletConnectModal](https://docs.walletconnect.com/advanced/walletconnectmodal/about)
4 |
5 | ## Requirements
6 |
7 | - [Expo environment setup](https://docs.expo.dev/get-started/installation/#requirements) (Node.js, Git, Watchman)
8 | - A [Wallet Connect Cloud](https://cloud.walletconnect.com/sign-in) project ID
9 | - Expo Go app installed in your smartphone
10 | - One or more web3 wallets installed in your smartphone (e.g. MetaMask, Rainbow Wallet, Trust Wallet, etc)
11 |
12 | ## How to run
13 |
14 | - In `App.tsx` fill in `projectId="..."` with your Wallet Connect Cloud project ID
15 | - `npm start`
16 | - Open Expo Go app in your smartphone
17 | - If your smartphone is in the same network as your computer, the local dev server should appear as the first option. If it doesn't, use the app to scan the QR Code presented in the terminal
18 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "react-native-viem",
4 | "slug": "react-native-viem",
5 | "scheme": "rnviem",
6 | "version": "1.0.0",
7 | "orientation": "portrait",
8 | "icon": "./assets/icon.png",
9 | "userInterfaceStyle": "light",
10 | "splash": {
11 | "image": "./assets/splash.png",
12 | "resizeMode": "contain",
13 | "backgroundColor": "#ffffff"
14 | },
15 | "assetBundlePatterns": ["**/*"],
16 | "ios": {
17 | "supportsTablet": true,
18 | "infoPlist": {
19 | "LSApplicationQueriesSchemes": [
20 | "metamask",
21 | "trust",
22 | "safe",
23 | "rainbow",
24 | "uniswap"
25 | ]
26 | }
27 | },
28 | "android": {
29 | "adaptiveIcon": {
30 | "foregroundImage": "./assets/adaptive-icon.png",
31 | "backgroundColor": "#ffffff"
32 | }
33 | },
34 | "web": {
35 | "favicon": "./assets/favicon.png"
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/callstack-internal/web3-react-native-dapp-viem/17ddf52a4362bf8b8d01c04aea4d0ceb45e77ac5/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/callstack-internal/web3-react-native-dapp-viem/17ddf52a4362bf8b8d01c04aea4d0ceb45e77ac5/assets/favicon.png
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/callstack-internal/web3-react-native-dapp-viem/17ddf52a4362bf8b8d01c04aea4d0ceb45e77ac5/assets/icon.png
--------------------------------------------------------------------------------
/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/callstack-internal/web3-react-native-dapp-viem/17ddf52a4362bf8b8d01c04aea4d0ceb45e77ac5/assets/splash.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-viem",
3 | "version": "1.0.0",
4 | "main": "node_modules/expo/AppEntry.js",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo start --android",
8 | "ios": "expo start --ios",
9 | "web": "expo start --web"
10 | },
11 | "dependencies": {
12 | "@react-native-async-storage/async-storage": "1.18.2",
13 | "@react-native-community/netinfo": "9.3.10",
14 | "@walletconnect/modal-react-native": "^1.0.0-rc.11",
15 | "expo": "~49.0.13",
16 | "expo-status-bar": "~1.6.0",
17 | "fastestsmallesttextencoderdecoder": "^1.0.22",
18 | "react": "18.2.0",
19 | "react-native": "0.72.5",
20 | "react-native-get-random-values": "^1.9.0",
21 | "react-native-modal": "^13.0.1",
22 | "react-native-svg": "13.9.0",
23 | "viem": "^1.16.2"
24 | },
25 | "devDependencies": {
26 | "@babel/core": "^7.20.0",
27 | "@callstack/eslint-config": "^14.0.0",
28 | "@types/react": "~18.2.14",
29 | "eslint": "^8.51.0",
30 | "eslint-config-prettier": "^9.0.0",
31 | "prettier": "^3.0.3",
32 | "typescript": "^5.2.2"
33 | },
34 | "private": true,
35 | "expo": {
36 | "install": {
37 | "exclude": [
38 | "react-native-get-random-values"
39 | ]
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/polyfills.ts:
--------------------------------------------------------------------------------
1 | import 'fastestsmallesttextencoderdecoder';
2 |
--------------------------------------------------------------------------------
/src/clients/public.ts:
--------------------------------------------------------------------------------
1 | import { createPublicClient, http } from 'viem';
2 | import { mainnet } from 'viem/chains';
3 |
4 | export const publicClient = createPublicClient({
5 | chain: mainnet,
6 | transport: http(),
7 | });
8 |
--------------------------------------------------------------------------------
/src/clients/walletConnect.ts:
--------------------------------------------------------------------------------
1 | export const providerMetadata = {
2 | name: 'Example dApp',
3 | description: 'Modern Example dApp from Callstack',
4 | url: 'https://callstack.com/',
5 | icons: ['https://example.com/'],
6 | redirect: {
7 | native: 'rnviem://',
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/src/pages/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo, useState } from 'react';
2 | import { Button, StyleSheet, Text, View } from 'react-native';
3 | import { Address, Chain, createWalletClient, custom, formatEther } from 'viem';
4 | import { mainnet, sepolia } from 'viem/chains';
5 | import {
6 | useWalletConnectModal,
7 | IProvider,
8 | } from '@walletconnect/modal-react-native';
9 | import { publicClient } from '../clients/public';
10 |
11 | export const CHAINS = [mainnet, sepolia];
12 |
13 | export default function HomePage() {
14 | const {
15 | open,
16 | isConnected,
17 | provider,
18 | address: wcAddress,
19 | } = useWalletConnectModal();
20 | const address = wcAddress as Address | undefined;
21 |
22 | const walletClient = useMemo(
23 | () =>
24 | createWalletClient({
25 | chain: mainnet,
26 | transport: custom({
27 | async request({ method, params }) {
28 | return await provider?.request({ method, params });
29 | },
30 | }),
31 | }),
32 | [provider],
33 | );
34 |
35 | const [blockNumber, setBlockNumber] = useState(0n);
36 | const [gasPrice, setGasPrice] = useState(0n);
37 | const [chain, setChain] = useState(CHAINS[0]);
38 | const [balance, setBalance] = useState(0n);
39 | const [signature, setSignature] = useState<`0x${string}`>();
40 |
41 | const onSignMessage = async () => {
42 | if (address) {
43 | const signature = await walletClient.signMessage({
44 | account: address,
45 | message: 'Sign this message to prove you are the owner of this wallet',
46 | });
47 |
48 | setSignature(signature);
49 | }
50 | };
51 |
52 | useEffect(() => {
53 | const getNetworkData = async () => {
54 | const [blockNumber, gasPrice] = await Promise.all([
55 | publicClient.getBlockNumber(),
56 | publicClient.getGasPrice(),
57 | ]);
58 |
59 | setBlockNumber(blockNumber);
60 | setGasPrice(gasPrice);
61 | };
62 |
63 | void getNetworkData();
64 | }, []);
65 |
66 | useEffect(() => {
67 | const onChainChangedEvent = (chainId: string) => {
68 | const chain =
69 | CHAINS.find(chain => chain.id === Number(chainId)) ?? CHAINS[0];
70 | setChain(chain);
71 | };
72 |
73 | const onConnectEvent = async ({
74 | session,
75 | }: {
76 | session: IProvider['session'];
77 | }) => {
78 | const chainId = session?.namespaces.eip155.chains?.[0].replace(
79 | 'eip155:',
80 | '',
81 | );
82 |
83 | if (chainId) {
84 | onChainChangedEvent(chainId);
85 | }
86 |
87 | if (address) {
88 | const balance = await publicClient.getBalance({
89 | address,
90 | });
91 | setBalance(balance);
92 | }
93 | };
94 |
95 | // @ts-ignore
96 | provider?.on('connect', onConnectEvent);
97 | provider?.on('chainChanged', onChainChangedEvent);
98 |
99 | return () => {
100 | provider?.removeListener('chainChanged', onChainChangedEvent);
101 | provider?.removeListener('connect', onConnectEvent);
102 | };
103 | }, [address, provider, walletClient]);
104 |
105 | return (
106 |
107 | Block number: {String(blockNumber)}
108 | Gas price: {formatEther(gasPrice)} ETH
109 |
110 | {isConnected && (
111 |
112 | Address: {address}
113 | Balance: {formatEther(balance)} ETH
114 | Connected to: {chain.name}
115 |
116 | {signature && (
117 |
118 | Signature: {signature}
119 |
120 | )}
121 |
122 | )}
123 |
124 |
125 | {isConnected ? (
126 | <>
127 |
128 |
138 |
139 | );
140 | }
141 |
142 | const styles = StyleSheet.create({
143 | container: {
144 | flex: 1,
145 | justifyContent: 'center',
146 | },
147 | block: {
148 | marginTop: 32,
149 | },
150 | });
151 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true
4 | },
5 | "extends": "expo/tsconfig.base"
6 | }
7 |
--------------------------------------------------------------------------------