├── .npmignore ├── src ├── index.ts ├── assets │ └── app-icon.png ├── types │ └── index.ts ├── cli │ └── index.ts └── buidler │ └── index.ts ├── public └── logo.png ├── .prettierignore ├── .gitignore ├── .vscode ├── extensions.json ├── settings.json └── launch.json ├── tsconfig.module.json ├── .github ├── CONTRIBUTING.md ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE.md ├── .editorconfig ├── LICENSE ├── .eslintrc.json ├── .cspell.json ├── .circleci └── config.yml ├── tsconfig.json ├── README.md └── package.json /.npmignore: -------------------------------------------------------------------------------- 1 | public/ 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './buidler'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cawfree/create-react-native-dapp/HEAD/public/logo.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # package.json is formatted by package managers, so we ignore it here 2 | package.json -------------------------------------------------------------------------------- /src/assets/app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cawfree/create-react-native-dapp/HEAD/src/assets/app-icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | .nyc_output 3 | build 4 | node_modules 5 | test 6 | src/**.js 7 | coverage 8 | *.log 9 | package-lock.json 10 | my-react-dapp/ 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "eamodio.gitlens", 6 | "streetsidesoftware.code-spell-checker", 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.module.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "outDir": "build/module", 6 | "module": "esnext" 7 | }, 8 | "exclude": [ 9 | "node_modules/**" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.userWords": [], // only use words from .cspell.json 3 | "cSpell.enabled": true, 4 | "editor.formatOnSave": true, 5 | "typescript.tsdk": "node_modules/typescript/lib", 6 | "typescript.enablePromptUseWorkspaceTsdk": true 7 | } 8 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Example Contributing Guidelines 2 | 3 | This is an example of GitHub's contributing guidelines file. Check out GitHub's [CONTRIBUTING.md help center article](https://help.github.com/articles/setting-guidelines-for-repository-contributors/) for more information. 4 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) 2 | 3 | - **What is the current behavior?** (You can also link to an open issue here) 4 | 5 | - **What is the new behavior (if this is a feature change)?** 6 | 7 | - **Other information**: 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 80 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - **I'm submitting a ...** 2 | [ ] bug report 3 | [ ] feature request 4 | [ ] question about the decisions made in the repository 5 | [ ] question about how to use this project 6 | 7 | - **Summary** 8 | 9 | - **Other information** (e.g. detailed explanation, stack traces, related issues, suggestions how to fix, links for us to have context, eg. StackOverflow, personal fork, etc.) 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | // To debug, make sure a *.spec.ts file is active in the editor, then run a configuration 5 | { 6 | "type": "node", 7 | "request": "launch", 8 | "name": "Debug Active Spec", 9 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ava", 10 | "runtimeArgs": ["debug", "--break", "--serial", "${file}"], 11 | "port": 9229, 12 | "outputCapture": "std", 13 | "skipFiles": ["/**/*.js"], 14 | "preLaunchTask": "npm: build" 15 | // "smartStep": true 16 | }, 17 | { 18 | // Use this one if you're already running `yarn watch` 19 | "type": "node", 20 | "request": "launch", 21 | "name": "Debug Active Spec (no build)", 22 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ava", 23 | "runtimeArgs": ["debug", "--break", "--serial", "${file}"], 24 | "port": 9229, 25 | "outputCapture": "std", 26 | "skipFiles": ["/**/*.js"] 27 | // "smartStep": true 28 | }] 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alex Thomas (@cawfree) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { "project": "./tsconfig.json" }, 5 | "env": { "es6": true }, 6 | "ignorePatterns": ["node_modules", "build", "coverage"], 7 | "plugins": ["import", "eslint-comments", "functional"], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:eslint-comments/recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:import/typescript", 13 | "plugin:functional/lite", 14 | "prettier", 15 | "prettier/@typescript-eslint" 16 | ], 17 | "globals": { "BigInt": true, "console": true, "WebAssembly": true }, 18 | "rules": { 19 | "@typescript-eslint/explicit-module-boundary-types": "off", 20 | "eslint-comments/disable-enable-pair": [ 21 | "error", 22 | { "allowWholeFile": true } 23 | ], 24 | "eslint-comments/no-unused-disable": "error", 25 | "import/order": [ 26 | "error", 27 | { "newlines-between": "always", "alphabetize": { "order": "asc" } } 28 | ], 29 | "sort-imports": [ 30 | "error", 31 | { "ignoreDeclarationSort": true, "ignoreCase": true } 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export enum CreationStatus { 2 | SUCCESS, 3 | FAILURE, 4 | } 5 | 6 | export type createParams = { 7 | readonly name: string; 8 | //readonly bundleIdentifier: string; 9 | //readonly packageName: string; 10 | readonly uriScheme: string; 11 | }; 12 | 13 | export type HardhatAccount = { 14 | readonly privateKey: string; 15 | readonly balance: string; 16 | }; 17 | 18 | export type HardhatOptions = { 19 | readonly hardhat: string; 20 | readonly hardhatConfig: string; 21 | readonly hardhatAccounts: readonly HardhatAccount[]; 22 | }; 23 | 24 | export type createContextOptions = createParams & { 25 | readonly yarn: boolean; 26 | readonly hardhat: HardhatOptions; 27 | }; 28 | 29 | export type createContext = createContextOptions & { 30 | readonly projectDir: string; 31 | readonly scriptsDir: string; 32 | readonly srcDir: string; 33 | readonly testsDir: string; 34 | }; 35 | 36 | export type createResult = createContext & { 37 | readonly status: CreationStatus; 38 | readonly message: string; 39 | }; 40 | 41 | export type EnvVariable = readonly [name: string, type: string, value: string]; 42 | export type EnvVariables = readonly EnvVariable[]; 43 | -------------------------------------------------------------------------------- /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1", 3 | "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json", 4 | "language": "en", 5 | "words": [ 6 | "asyncstorage", 7 | "bitauth", 8 | "bitjson", 9 | "buidl", 10 | "buidler", 11 | "bytecode", 12 | "cawfree", 13 | "cimg", 14 | "circleci", 15 | "codecov", 16 | "commitlint", 17 | "commitlintrc", 18 | "dependabot", 19 | "editorconfig", 20 | "esnext", 21 | "execa", 22 | "exponentiate", 23 | "gitkeep", 24 | "globby", 25 | "jvmargs", 26 | "libauth", 27 | "mkdir", 28 | "nodeify", 29 | "nomiclabs", 30 | "precommit", 31 | "predeploy", 32 | "prettierignore", 33 | "randombytes", 34 | "sandboxed", 35 | "securerandom", 36 | "SPDX", 37 | "transpiled", 38 | "typedoc", 39 | "unflatten", 40 | "Unflattened", 41 | "unimodules", 42 | "untracked", 43 | "walletconnect" 44 | ], 45 | "flagWords": [], 46 | "ignorePaths": [ 47 | "package.json", 48 | "package-lock.json", 49 | "yarn.lock", 50 | "tsconfig.json", 51 | "node_modules/**" 52 | ] 53 | } -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # https://circleci.com/docs/2.0/language-javascript/ 2 | version: 2 3 | jobs: 4 | 'node-10': 5 | docker: 6 | - image: circleci/node:10 7 | steps: 8 | - checkout 9 | # Download and cache dependencies 10 | - restore_cache: 11 | keys: 12 | - v1-dependencies-{{ checksum "package.json" }} 13 | # fallback to using the latest cache if no exact match is found 14 | - v1-dependencies- 15 | - run: npm install 16 | - save_cache: 17 | paths: 18 | - node_modules 19 | key: v1-dependencies-{{ checksum "package.json" }} 20 | - run: npm test 21 | - run: npm run cov:send 22 | - run: npm run cov:check 23 | 'node-12': 24 | docker: 25 | - image: circleci/node:12 26 | steps: 27 | - checkout 28 | - restore_cache: 29 | keys: 30 | - v1-dependencies-{{ checksum "package.json" }} 31 | - v1-dependencies- 32 | - run: npm install 33 | - save_cache: 34 | paths: 35 | - node_modules 36 | key: v1-dependencies-{{ checksum "package.json" }} 37 | - run: npm test 38 | - run: npm run cov:send 39 | - run: npm run cov:check 40 | 'node-latest': 41 | docker: 42 | - image: circleci/node:latest 43 | steps: 44 | - checkout 45 | - restore_cache: 46 | keys: 47 | - v1-dependencies-{{ checksum "package.json" }} 48 | - v1-dependencies- 49 | - run: npm install 50 | - save_cache: 51 | paths: 52 | - node_modules 53 | key: v1-dependencies-{{ checksum "package.json" }} 54 | - run: npm test 55 | - run: npm run cov:send 56 | - run: npm run cov:check 57 | 58 | workflows: 59 | version: 2 60 | build: 61 | jobs: 62 | - 'node-10' 63 | - 'node-12' 64 | - 'node-latest' 65 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es2017", 5 | "outDir": "build/main", 6 | "rootDir": "src", 7 | "moduleResolution": "node", 8 | "module": "commonjs", 9 | "declaration": true, 10 | "inlineSourceMap": true, 11 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 12 | "resolveJsonModule": true /* Include modules imported with .json extension. */, 13 | 14 | "strict": true /* Enable all strict type-checking options. */, 15 | 16 | /* Strict Type-Checking Options */ 17 | // "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 18 | // "strictNullChecks": true /* Enable strict null checks. */, 19 | // "strictFunctionTypes": true /* Enable strict checking of function types. */, 20 | // "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 21 | // "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 22 | // "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 23 | 24 | /* Additional Checks */ 25 | "noUnusedLocals": true /* Report errors on unused locals. */, 26 | "noUnusedParameters": true /* Report errors on unused parameters. */, 27 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 28 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 29 | 30 | /* Debugging Options */ 31 | "traceResolution": false /* Report module resolution log messages. */, 32 | "listEmittedFiles": false /* Print names of generated files part of the compilation. */, 33 | "listFiles": false /* Print names of files part of the compilation. */, 34 | "pretty": true /* Stylize errors and messages using color and context. */, 35 | 36 | /* Experimental Options */ 37 | // "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, 38 | // "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */, 39 | 40 | "lib": ["es2017"], 41 | "types": ["node"], 42 | "typeRoots": ["node_modules/@types", "src/types"] 43 | }, 44 | "include": ["src/**/*.ts"], 45 | "exclude": ["node_modules/**"], 46 | "compileOnSave": false 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `npx create-react-native-dapp` 2 | 3 |

4 | 5 |

6 | 7 | ![https://img.shields.io/badge/strictly-typescript-blue](https://img.shields.io/badge/types-typescript-blue) 8 | ![https://img.shields.io/badge/platforms-android%2Cios%2Cweb-brightgreen](https://img.shields.io/badge/platforms-android%2Cios%2Cweb-brightgreen) 9 | ![https://img.shields.io/badge/powered--by-hardhat-orange](https://img.shields.io/badge/powered--by-hardhat-orange) 10 | [![https://img.shields.io/badge/chat-on%20discord-red](https://img.shields.io/badge/chat-on%20discord-red)](https://discord.gg/PeqrwpCDwc) 11 | 12 | [`create-react-native-dapp`](https://github.com/cawfree/create-react-native-dapp) is an `npx` utility to help quickly bootstrap [**React Native ⚛️**](https://reactnative.dev) applications with access to the [**Ethereum**](https://ethereum.org) Blockchain. 13 | 14 | Our goal is to help create a sustainable open source ecosystem for [`Web3`](https://github.com/ethereum/web3.js/) in React Native by providing a dependable common runtime which we can _buidl_ upon and extend **together**. 15 | 16 | Watch us shill `create-react-native-dapp` [here](https://www.youtube.com/watch?v=Y1_37f9cseQ). 17 | 18 | ### 🔥 Features 19 | 20 | - 🚀 **Bootstrapped by Expo.** 21 | - Easily take advantage of Expo's high quality, well-supported and well-documented library architecture. 22 | - Supports Android, iOS and the Web. 23 | - 👷 **Served with Hardhat.** 24 | - Your generated app comes with a simple example contract which you can deploy, test and interact with directly. 25 | - 👛 **Powered by WalletConnect**. 26 | - Connect to secure wallets such as [**Rainbow** 🌈](https://github.com/rainbow-me/rainbow) out of the box! 27 | - 🏗️ **It's typed, and pretty.** 28 | - It comes pre-configured with TypeScript to help write applications that _scale_. 29 | - It's integrated with [**prettier**](https://prettier.io/) and [**husky**](https://github.com/typicode/husky) to ensure coding standards are enforced right from the beginning. 30 | - 😉 **And it's ready to go.** 31 | - Built applications come pre-packaged with `.env` support using [`react-native-dotenv`](https://github.com/goatandsheep/react-native-dotenv) and companion tests for your [contracts](https://ethereum.org/en/learn/). 32 | - Projects are initialized using deep linking so external navigation is a breeze. 33 | 34 | ## To get started, 35 | 36 | First, [please make sure](https://forums.expo.io/t/newly-created-app-crashes-on-ios-sim/45566) you've logged into [`expo-cli`](https://docs.expo.io/workflow/expo-cli/). 37 | 38 | ``` 39 | npm install -g expo-cli 40 | npx create-react-native-dapp 41 | ``` 42 | 43 | This will walk you through the creation of your Ethereum-enabled application, which works on [**Android**](https://reactnative.dev), [**iOS**](https://reactnative.dev) and the [**Web**](https://github.com/necolas/react-native-web). 44 | 45 | To start the app, you can: 46 | 47 | ``` 48 | cd my-react-dapp 49 | yarn ios # android, web 50 | ``` 51 | 52 | ## ✌️ License 53 | 54 | [**MIT**](./LICENSE) 55 | -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import chalk from 'chalk'; 4 | import prompts from 'prompts'; 5 | 6 | import { create } from '../buidler'; 7 | import { CreationStatus } from '../types'; 8 | 9 | const defaultPadding = 5; 10 | 11 | const line = (message: string, padding: number = defaultPadding) => 12 | console.log([...Array(padding)].map(() => ' ').join(''), message); 13 | 14 | const gfx = ` 15 | MMMMMMMMMMMMMMMMMMMMMMMXkkKMMMMMMMMMMMMMMMMMMMMMMM 16 | MMMMMMMMMMMMMMMMMMMMMWKd;':OWMMMMMMMMMMMMMMMMMMMMM 17 | MMMMMMMMMMMMMMMMMMMMW0dl;..,kNMMMMMMMMMMMMMMMMMMMM 18 | MMMMMMMMMMMMMMMMMMMNOdol;...,dNMMMMMMMMMMMMMMMMMMM 19 | MMMMMMMMMMMMMMMMMMXkdool,....'lXMMMMMMMMMMMMMMMMMM 20 | MMMMMMMMMMMMMMMMWXxoc:;,.......c0WMMMMMMMMMMMMMMMM 21 | MMMMMMMMMMMMMMMMKl;,'.... ..:0MMMMMMMMMMMMMMMM 22 | MMMMMMMMMMMMMMMMKxl:'.... .,:oKMMMMMMMMMMMMMMMM 23 | MMMMMMMMMMMMMMMMWXOkxoc,...;cclo0WMMMMMMMMMMMMMMMM 24 | MMMMMMMMMMMMMMMMMWXkdxkxoclc;,lKWMMMMMMMMMMMMMMMMM 25 | MMMMMMMMMMMMMMMMMMMN0doo:'..;xNMMMMMMMMMMMMMMMMMMM 26 | MMMMMMMMMMMMMMMMMMMMWKko;.'c0WMMMMMMMMMMMMMMMMMMMM 27 | MMMMMMMMMMMMMMMMMMMMMMNOl:dXMMMMMMMMMMMMMMMMMMMMMM 28 | MMMMMMMMMMMMMMMMMMMMMMMWXXNMMMMMMMMMMMMMMMMMMMMMMM 29 | `.trim(); 30 | 31 | //function validateBundleId(value: string): boolean { 32 | // return /^[a-zA-Z][a-zA-Z0-9\-.]+$/.test(value); 33 | //} 34 | // 35 | //function validatePackage(value: string): boolean { 36 | // return /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(value); 37 | //} 38 | 39 | function validateUriScheme(value: string): boolean { 40 | return /^[a-z0-9]+$/.test(value); 41 | } 42 | 43 | (async () => { 44 | console.clear(); 45 | console.log(); 46 | gfx.split('\n').map((e) => line(e, 1)); 47 | console.log(); 48 | 49 | line(chalk.green.bold`Ξ Welcome to create-react-native-dapp! Ξ`); 50 | line( 51 | `Your next Ethereum application starts ${chalk.bold`here`}.`, 52 | defaultPadding - 1 53 | ); 54 | 55 | console.log(); 56 | 57 | const { 58 | name, 59 | //bundleIdentifier, 60 | //packageName, 61 | uriScheme, 62 | } = await prompts([ 63 | { 64 | type: 'text', 65 | name: 'name', 66 | message: 'What is your app named?', 67 | initial: 'my-react-dapp', 68 | validate: (value) => { 69 | if (typeof value !== 'string') { 70 | return `Expected string, encountered ${typeof value}.`; 71 | } else if (!value.length) { 72 | return 'Name cannot be empty.'; 73 | } else if (!value.match(/^[a-z0-9-]+$/i)) { 74 | return 'Name must be alphanumeric and contain no special characters other than a hyphen.'; 75 | } else if (/^\d/.test(value)) { 76 | return 'Name cannot begin with a number.'; 77 | } else if (/^-/.test(value)) { 78 | return 'Name cannot begin with a hyphen.'; 79 | } 80 | return true; 81 | }, 82 | }, 83 | //{ 84 | // type: 'text', 85 | // name: 'bundleIdentifier', 86 | // message: 'What is the iOS bundle identifier?', 87 | // initial: 'com.myreactdapp', 88 | // validate: (value) => { 89 | // if (!validateBundleId(value)) { 90 | // return `Only alphanumeric characters, '.', '-', and '_' are allowed, and each '.' must be followed by a letter.`; 91 | // } 92 | // return true; 93 | // }, 94 | //}, 95 | //{ 96 | // type: 'text', 97 | // name: 'packageName', 98 | // message: 'What is the Android package name?', 99 | // // eslint-disable-next-line @typescript-eslint/ban-ts-comment 100 | // // @ts-ignore 101 | // initial: (last) => { 102 | // if (!validatePackage(last)) { 103 | // return undefined; 104 | // } 105 | // return last; 106 | // }, 107 | // validate: (value) => { 108 | // if (!validatePackage(value)) { 109 | // return `Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter.`; 110 | // } 111 | // return true; 112 | // }, 113 | //}, 114 | { 115 | type: 'text', 116 | name: 'uriScheme', 117 | message: 'What is the URI scheme?', 118 | initial: 'myreactdapp', 119 | validate: (value) => { 120 | if (!validateUriScheme(value)) { 121 | return `Only lowercase alphanumeric characters are allowed.`; 122 | } 123 | return true; 124 | }, 125 | }, 126 | ]); 127 | 128 | const { status, message } = await create({ 129 | name, 130 | //bundleIdentifier, 131 | //packageName, 132 | uriScheme, 133 | }); 134 | 135 | if (status === CreationStatus.FAILURE) { 136 | // eslint-disable-next-line functional/no-throw-statement 137 | throw new Error(message); 138 | } 139 | console.log(message); 140 | console.log(); 141 | })(); 142 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-react-native-dapp", 3 | "version": "1.3.0", 4 | "private": false, 5 | "description": "Your next Ethereum application starts here. ⚛️ 💪 🦄", 6 | "main": "build/main/index.js", 7 | "typings": "build/main/index.d.ts", 8 | "module": "build/module/index.js", 9 | "repository": "https://github.com/cawfree/create-react-native-dapp", 10 | "license": "MIT", 11 | "author": "Alex Thomas (@cawfree) ", 12 | "contributors": [ 13 | { 14 | "name": "@cawfree", 15 | "url": "https://github.com/cawfree" 16 | } 17 | ], 18 | "keywords": [ 19 | "react", 20 | "react-native", 21 | "dapp", 22 | "ethereum", 23 | "web3", 24 | "starter", 25 | "react-native-web", 26 | "blockchain" 27 | ], 28 | "scripts": { 29 | "audit": "npx yarn-audit-fix", 30 | "build": "run-p build:* && cp -rf src/assets build/main/ && sudo chmod +x build/main/cli/index.js", 31 | "build:main": "tsc -p tsconfig.json", 32 | "build:module": "tsc -p tsconfig.module.json", 33 | "fix": "run-s fix:*", 34 | "fix:prettier": "prettier \"src/**/*.ts\" --write", 35 | "fix:lint": "eslint src --ext .ts --fix", 36 | "test": "run-s build test:*", 37 | "test:lint": "eslint src --ext .ts", 38 | "test:prettier": "prettier \"src/**/*.ts\" --list-different", 39 | "test:spelling": "cspell \"{README.md,.github/*.md,src/**/*.ts}\"", 40 | "test:unit": "nyc --silent ava", 41 | "check-cli": "run-s test diff-integration-tests check-integration-tests", 42 | "check-integration-tests": "run-s check-integration-test:*", 43 | "diff-integration-tests": "mkdir -p diff && rm -rf diff/test && cp -r test diff/test && rm -rf diff/test/test-*/.git && cd diff && git init --quiet && git add -A && git commit --quiet --no-verify --allow-empty -m 'WIP' && echo '\\n\\nCommitted most recent integration test output in the \"diff\" directory. Review the changes with \"cd diff && git diff HEAD\" or your preferred git diff viewer.'", 44 | "watch:build": "tsc -p tsconfig.json -w", 45 | "watch:test": "nyc --silent ava --watch", 46 | "cov": "run-s build test:unit cov:html cov:lcov && open-cli coverage/index.html", 47 | "cov:html": "nyc report --reporter=html", 48 | "cov:lcov": "nyc report --reporter=lcov", 49 | "cov:send": "run-s cov:lcov && codecov", 50 | "cov:check": "nyc report && nyc check-coverage --lines 100 --functions 100 --branches 100", 51 | "doc": "run-s doc:html && open-cli build/docs/index.html", 52 | "doc:html": "typedoc src/ --exclude **/*.spec.ts --target ES6 --mode file --out build/docs", 53 | "doc:json": "typedoc src/ --exclude **/*.spec.ts --target ES6 --mode file --json build/docs/typedoc.json", 54 | "doc:publish": "gh-pages -m \"[ci skip] Updates\" -d build/docs", 55 | "version": "standard-version", 56 | "reset-hard": "git clean -dfx && git reset --hard && yarn", 57 | "prepare-release": "run-s reset-hard test cov:check doc:html version doc:publish" 58 | }, 59 | "bin": { 60 | "create-react-native-dapp": "./build/main/cli/index.js" 61 | }, 62 | "engines": { 63 | "node": ">=12" 64 | }, 65 | "dependencies": { 66 | "chalk": "^4.1.0", 67 | "ethers": "^5.0.24", 68 | "flat": "^5.0.2", 69 | "prompts": "^2.4.0" 70 | }, 71 | "devDependencies": { 72 | "@ava/typescript": "^1.1.1", 73 | "@istanbuljs/nyc-config-typescript": "^1.0.1", 74 | "@types/flat": "^5.0.1", 75 | "@types/node": "^14.14.16", 76 | "@types/prompts": "^2.0.9", 77 | "@typescript-eslint/eslint-plugin": "^4.0.1", 78 | "@typescript-eslint/parser": "^4.0.1", 79 | "ava": "^3.12.1", 80 | "codecov": "^3.5.0", 81 | "cspell": "^4.1.0", 82 | "cz-conventional-changelog": "^3.3.0", 83 | "eslint": "^7.8.0", 84 | "eslint-config-prettier": "^6.11.0", 85 | "eslint-plugin-eslint-comments": "^3.2.0", 86 | "eslint-plugin-functional": "^3.0.2", 87 | "eslint-plugin-import": "^2.22.0", 88 | "gh-pages": "^3.1.0", 89 | "npm-run-all": "^4.1.5", 90 | "nyc": "^15.1.0", 91 | "open-cli": "^6.0.1", 92 | "prettier": "^2.1.1", 93 | "standard-version": "^9.0.0", 94 | "ts-node": "^9.0.0", 95 | "typedoc": "^0.19.0", 96 | "typescript": "^4.0.2" 97 | }, 98 | "files": [ 99 | "build/main", 100 | "build/module", 101 | "!**/*.spec.*", 102 | "!**/*.json", 103 | "CHANGELOG.md", 104 | "LICENSE", 105 | "README.md" 106 | ], 107 | "ava": { 108 | "failFast": true, 109 | "timeout": "60s", 110 | "typescript": { 111 | "rewritePaths": { 112 | "src/": "build/main/" 113 | } 114 | }, 115 | "files": [ 116 | "!build/module/**" 117 | ] 118 | }, 119 | "config": { 120 | "commitizen": { 121 | "path": "cz-conventional-changelog" 122 | } 123 | }, 124 | "prettier": { 125 | "singleQuote": true 126 | }, 127 | "nyc": { 128 | "extends": "@istanbuljs/nyc-config-typescript", 129 | "exclude": [ 130 | "**/*.spec.js" 131 | ] 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/buidler/index.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | 5 | import chalk from 'chalk'; 6 | import { ethers } from 'ethers'; 7 | import { flatten, unflatten } from 'flat'; 8 | 9 | import { 10 | createContext, 11 | createParams, 12 | createResult, 13 | CreationStatus, 14 | EnvVariable, 15 | EnvVariables, 16 | HardhatOptions, 17 | } from '../types'; 18 | 19 | const port = 8545; 20 | 21 | // eslint-disable-next-line @typescript-eslint/ban-types 22 | const prettyStringify = (obj: object): string => JSON.stringify(obj, null, 2); 23 | 24 | const injectFlattenedJsonToFile = ( 25 | file: string, 26 | // eslint-disable-next-line @typescript-eslint/ban-types 27 | options: object, 28 | // eslint-disable-next-line @typescript-eslint/ban-types 29 | maybeUnflattened?: object 30 | ) => { 31 | !fs.existsSync(file) && fs.writeFileSync(file, JSON.stringify({})); 32 | fs.writeFileSync( 33 | file, 34 | prettyStringify({ 35 | ...unflatten({ 36 | ...(flatten( 37 | JSON.parse(fs.readFileSync(file, 'utf-8')) 38 | // eslint-disable-next-line @typescript-eslint/ban-types 39 | ) as object), 40 | ...options, 41 | }), 42 | ...(typeof maybeUnflattened === 'object' ? maybeUnflattened : {}), 43 | }) 44 | ); 45 | }; 46 | 47 | const createBaseProject = ({ name }: createParams) => 48 | execSync(`npx create-react-native-app ${name} -t with-typescript`, { 49 | stdio: 'inherit', 50 | }); 51 | 52 | //const ejectExpoProject = (ctx: createContext) => { 53 | // const { 54 | // bundleIdentifier, packageName, uriScheme, 55 | // } = ctx; 56 | // const { projectDir } = ctx; 57 | // 58 | // injectFlattenedJsonToFile(path.resolve(projectDir, 'app.json'), { 59 | // 'expo.ios.bundleIdentifier': bundleIdentifier, 60 | // 'expo.android.package': packageName, 61 | // 'expo.scheme': uriScheme, 62 | // 'expo.icon': 'assets/image/app-icon.png', 63 | // 'expo.splash.image': 'assets/image/app-icon.png', 64 | // 'expo.splash.resizeMode': 'contain', 65 | // 'expo.splash.backgroundColor': '#222222', 66 | // }); 67 | // 68 | // execSync(`expo eject --non-interactive`, { 69 | // stdio: 'inherit', 70 | // cwd: `${projectDir}`, 71 | // }); 72 | // 73 | // const gradle = path.resolve(projectDir, 'android', 'gradle.properties'); 74 | // fs.writeFileSync( 75 | // gradle, 76 | // ` 77 | //${fs.readFileSync(gradle, 'utf-8')} 78 | // 79 | //# 4GB Heap Size 80 | //org.gradle.jvmargs=-Xmx4608m 81 | // `.trim(), 82 | // ); 83 | //}; 84 | 85 | const setAppIcon = (ctx: createContext) => { 86 | const { projectDir } = ctx; 87 | const assetsDir = path.resolve(projectDir, 'assets'); 88 | 89 | !fs.existsSync(assetsDir) && fs.mkdirSync(assetsDir); 90 | 91 | ['image', 'video', 'json', 'raw'].map((type: string) => { 92 | const dir = path.resolve(assetsDir, type); 93 | const gitkeep = path.resolve(dir, '.gitkeep'); 94 | !fs.existsSync(dir) && fs.mkdirSync(dir); 95 | fs.writeFileSync(gitkeep, ''); 96 | }); 97 | 98 | const appIcon = path.resolve(assetsDir, 'image', 'app-icon.png'); 99 | 100 | fs.copyFileSync(require.resolve('../assets/app-icon.png'), appIcon); 101 | 102 | const assetDeclarations = path.resolve(assetsDir, 'index.d.ts'); 103 | fs.writeFileSync( 104 | assetDeclarations, 105 | ` 106 | import { ImageSourcePropType } from 'react-native'; 107 | 108 | declare module '*.png' { 109 | export default ImageSourcePropType; 110 | } 111 | 112 | declare module '*.jpg' { 113 | export default ImageSourcePropType; 114 | } 115 | 116 | declare module '*.jpeg' { 117 | export default ImageSourcePropType; 118 | } 119 | 120 | declare module '*.gif' { 121 | export default ImageSourcePropType; 122 | } 123 | 124 | declare module '*.mp4' { 125 | export default unknown; 126 | } 127 | `.trim(), 128 | ); 129 | 130 | }; 131 | 132 | const createFileThunk = (root: string) => (f: readonly string[]): string => { 133 | return path.resolve(root, ...f); 134 | }; 135 | 136 | const hardhatOptions = async ( 137 | projectFile: (f: readonly string[]) => string, 138 | scriptFile: (f: readonly string[]) => string 139 | ): Promise => { 140 | const hardhatAccounts = await Promise.all( 141 | [...Array(10)].map(async () => { 142 | const { privateKey } = await ethers.Wallet.createRandom(); 143 | return { privateKey, balance: '1000000000000000000000' }; // 1000 ETH 144 | }) 145 | ); 146 | return { 147 | hardhat: scriptFile(['hardhat.ts']), 148 | hardhatConfig: projectFile(['hardhat.config.js']), 149 | hardhatAccounts, 150 | } as HardhatOptions; 151 | }; 152 | 153 | const createBaseContext = async ( 154 | params: createParams 155 | ): Promise => { 156 | const { name } = params; 157 | const projectDir = path.resolve(name); 158 | const scriptsDir = path.resolve(projectDir, 'scripts'); 159 | const testsDir = path.resolve(projectDir, '__tests__'); 160 | const projectFile = createFileThunk(projectDir); 161 | const scriptFile = createFileThunk(scriptsDir); 162 | const srcDir = path.resolve(projectDir, 'frontend'); 163 | return Object.freeze({ 164 | ...params, 165 | yarn: fs.existsSync(projectFile(['yarn.lock'])), 166 | hardhat: await hardhatOptions(projectFile, scriptFile), 167 | projectDir, 168 | scriptsDir, 169 | testsDir, 170 | srcDir, 171 | }); 172 | }; 173 | 174 | // TODO: Find a nice version. 175 | const shimProcessVersion = 'v9.40'; 176 | 177 | const injectShims = (ctx: createContext) => { 178 | const { projectDir } = ctx; 179 | fs.writeFileSync( 180 | path.resolve(projectDir, 'index.js'), 181 | ` 182 | /* eslint-disable eslint-comments/no-unlimited-disable */ 183 | /* eslint-disable */ 184 | 185 | // This file has been auto-generated by Ξ create-react-native-dapp Ξ. 186 | // Feel free to modify it, but please take care to maintain the exact 187 | // procedure listed between /* dapp-begin */ and /* dapp-end */, as 188 | // this will help persist a known template for future migrations. 189 | 190 | /* dapp-begin */ 191 | const { Platform, LogBox } = require('react-native'); 192 | 193 | if (Platform.OS !== 'web') { 194 | require('react-native-get-random-values'); 195 | LogBox.ignoreLogs( 196 | [ 197 | "Warning: The provided value 'ms-stream' is not a valid 'responseType'.", 198 | "Warning: The provided value 'moz-chunked-arraybuffer' is not a valid 'responseType'.", 199 | ], 200 | ); 201 | } 202 | 203 | if (typeof Buffer === 'undefined') { 204 | global.Buffer = require('buffer').Buffer; 205 | } 206 | 207 | global.btoa = global.btoa || require('base-64').encode; 208 | global.atob = global.atob || require('base-64').decode; 209 | 210 | process.version = '${shimProcessVersion}'; 211 | 212 | const { registerRootComponent, scheme } = require('expo'); 213 | const { default: App } = require('./frontend/App'); 214 | 215 | const { default: AsyncStorage } = require('@react-native-async-storage/async-storage'); 216 | const { withWalletConnect } = require('@walletconnect/react-native-dapp'); 217 | 218 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 219 | // It also ensures that whether you load the app in the Expo client or in a native build, 220 | // the environment is set up appropriately 221 | registerRootComponent(withWalletConnect(App, { 222 | redirectUrl: Platform.OS === 'web' ? window.location.origin : \`\${scheme}://\`, 223 | storageOptions: { 224 | asyncStorage: AsyncStorage, 225 | }, 226 | })); 227 | /* dapp-end */ 228 | `.trim() 229 | ); 230 | }; 231 | 232 | const createScripts = (ctx: createContext) => { 233 | const { scriptsDir } = ctx; 234 | !fs.existsSync(scriptsDir) && fs.mkdirSync(scriptsDir); 235 | const postinstall = path.resolve(scriptsDir, 'postinstall.ts'); 236 | const android = path.resolve(scriptsDir, 'android.ts'); 237 | const ios = path.resolve(scriptsDir, 'ios.ts'); 238 | const web = path.resolve(scriptsDir, 'web.ts'); 239 | fs.writeFileSync( 240 | postinstall, 241 | ` 242 | import 'dotenv/config'; 243 | import * as child_process from 'child_process'; 244 | 245 | // Uncommment to rename the application binaries on postinstall. 246 | //const {APP_DISPLAY_NAME} = process.env; 247 | //child_process.execSync( 248 | // \`npx react-native-rename \${APP_DISPLAY_NAME}\`, 249 | // {stdio: 'inherit'} 250 | //); 251 | 252 | // Uncommment to regenerate the application icon on postinstall. 253 | //child_process.execSync( 254 | // 'npx app-icon generate -i assets/image/app-icon.png --platforms=android,ios', 255 | // {stdio: 'inherit'} 256 | //); 257 | 258 | child_process.execSync('npx patch-package', { stdio: 'inherit' }); 259 | 260 | // Uncomment to reinstall pods on postinstall. 261 | // import {macos} from 'platform-detect'; 262 | // if (macos) { 263 | // child_process.execSync('npx pod-install', { stdio: 'inherit' }); 264 | // } 265 | 266 | `.trim() 267 | ); 268 | 269 | fs.writeFileSync( 270 | android, 271 | ` 272 | import 'dotenv/config'; 273 | import * as child_process from 'child_process'; 274 | 275 | import * as appRootPath from 'app-root-path'; 276 | import * as chokidar from 'chokidar'; 277 | 278 | const opts: child_process.ExecSyncOptions = { cwd: \`\${appRootPath}\`, stdio: 'inherit' }; 279 | 280 | chokidar.watch('contracts').on('all', () => { 281 | child_process.execSync('npx hardhat compile', opts); 282 | }); 283 | 284 | child_process.execSync('npx kill-port ${port}', opts); 285 | child_process.execSync('adb reverse tcp:${port} tcp:${port}', opts); 286 | 287 | child_process.execSync('npx hardhat node --hostname 0.0.0.0 & expo run:android &', opts); 288 | `.trim(), 289 | ); 290 | fs.writeFileSync( 291 | ios, 292 | ` 293 | import 'dotenv/config'; 294 | import * as child_process from 'child_process'; 295 | 296 | import * as appRootPath from 'app-root-path'; 297 | import * as chokidar from 'chokidar'; 298 | 299 | const opts: child_process.ExecSyncOptions = { cwd: \`\${appRootPath}\`, stdio: 'inherit' }; 300 | 301 | chokidar.watch('contracts').on('all', () => { 302 | child_process.execSync('npx hardhat compile', opts); 303 | }); 304 | 305 | child_process.execSync('npx kill-port ${port}', opts); 306 | child_process.execSync('npx hardhat node --hostname 0.0.0.0 & expo run:ios &', opts); 307 | `.trim(), 308 | ); 309 | fs.writeFileSync( 310 | web, 311 | ` 312 | import 'dotenv/config'; 313 | import * as child_process from 'child_process'; 314 | 315 | import * as appRootPath from 'app-root-path'; 316 | import * as chokidar from 'chokidar'; 317 | 318 | const opts: child_process.ExecSyncOptions = { cwd: \`\${appRootPath}\`, stdio: 'inherit' }; 319 | 320 | chokidar.watch('contracts').on('all', () => { 321 | child_process.execSync('npx hardhat compile', opts); 322 | }); 323 | 324 | child_process.execSync('npx kill-port 8545', opts); 325 | child_process.execSync('expo web & npx hardhat node --hostname 0.0.0.0 &', opts); 326 | `.trim(), 327 | ); 328 | }; 329 | 330 | const getAllEnvVariables = (ctx: createContext): EnvVariables => { 331 | const { hardhat: { hardhatAccounts } } = ctx; 332 | return [ 333 | ['APP_DISPLAY_NAME', 'string', `${ctx.name}`], 334 | ['HARDHAT_PORT', 'string', `${port}`], 335 | ['HARDHAT_PRIVATE_KEY', 'string', hardhatAccounts[0].privateKey], 336 | ]; 337 | }; 338 | 339 | const shouldPrepareTypeRoots = (ctx: createContext) => { 340 | const stringsToRender = getAllEnvVariables(ctx).map( 341 | ([name, type]: EnvVariable) => ` export const ${name}: ${type};` 342 | ); 343 | return fs.writeFileSync( 344 | path.resolve(ctx.projectDir, 'index.d.ts'), 345 | ` 346 | declare module '@env' { 347 | ${stringsToRender.join('\n')} 348 | } 349 | `.trim() 350 | ); 351 | }; 352 | 353 | const shouldPrepareSpelling = (ctx: createContext) => fs.writeFileSync( 354 | path.resolve(ctx.projectDir, '.cspell.json'), 355 | prettyStringify({ 356 | words: ["bytecode", "dapp"], 357 | }), 358 | ); 359 | 360 | const shouldPrepareTsc = (ctx: createContext) => { 361 | fs.writeFileSync( 362 | path.resolve(ctx.projectDir, 'tsconfig.json'), 363 | prettyStringify({ 364 | compilerOptions: { 365 | allowSyntheticDefaultImports: true, 366 | jsx: 'react-native', 367 | lib: ['dom', 'esnext'], 368 | moduleResolution: 'node', 369 | noEmit: true, 370 | skipLibCheck: true, 371 | resolveJsonModule: true, 372 | typeRoots: ['index.d.ts'], 373 | types: ['node', 'jest'], 374 | }, 375 | include: ['**/*.ts', '**/*.tsx'], 376 | exclude: [ 377 | 'node_modules', 378 | 'babel.config.js', 379 | 'metro.config.js', 380 | 'jest.config.js', 381 | '**/*.test.tsx', 382 | '**/*.test.ts', 383 | '**/*.spec.tsx', 384 | '**/*.spec.ts', 385 | ], 386 | }) 387 | ); 388 | }; 389 | 390 | const preparePackage = (ctx: createContext) => 391 | injectFlattenedJsonToFile( 392 | path.resolve(ctx.projectDir, 'package.json'), 393 | { 394 | homepage: 'https://cawfree.github.io/create-react-native-dapp', 395 | license: 'MIT', 396 | contributors: [ 397 | { 398 | name: '@cawfree', 399 | url: "https://github.com/cawfree" 400 | }, 401 | ], 402 | keywords: [ 403 | 'react', 404 | 'react-native', 405 | 'blockchain', 406 | 'dapp', 407 | 'ethereum', 408 | 'web3', 409 | 'starter', 410 | 'react-native-web', 411 | ], 412 | // scripts 413 | 'scripts.audit': `${ctx.yarn ? '' : 'npm_config_yes=true '}npx yarn-audit-fix`, 414 | 'scripts.postinstall': 'node_modules/.bin/ts-node scripts/postinstall', 415 | 'scripts.test': 'npx hardhat test && jest', 416 | 'scripts.android': 'node_modules/.bin/ts-node scripts/android', 417 | 'scripts.ios': 'node_modules/.bin/ts-node scripts/ios', 418 | 'scripts.web': 'node_modules/.bin/ts-node scripts/web', 419 | 'scripts.web:deploy': 'expo build:web && gh-pages -d web-build', 420 | // dependencies 421 | 'dependencies.@react-native-async-storage/async-storage': '1.13.4', 422 | 'dependencies.@walletconnect/react-native-dapp': '1.6.1', 423 | 'dependencies.react-native-svg': '9.6.4', 424 | 'dependencies.base-64': '1.0.0', 425 | 'dependencies.buffer': '6.0.3', 426 | 'dependencies.node-libs-browser': '2.2.1', 427 | 'dependencies.path-browserify': '0.0.0', 428 | 'dependencies.react-native-crypto': '2.2.0', 429 | 'dependencies.react-native-dotenv': '2.4.3', 430 | 'dependencies.react-native-localhost': '1.0.0', 431 | 'dependencies.react-native-get-random-values': '1.5.0', 432 | 'dependencies.react-native-stream': '0.1.9', 433 | 'dependencies.web3': '1.3.1', 434 | 'devDependencies.@babel/core': '7.15.5', 435 | 'devDependencies.@babel/plugin-proposal-private-property-in-object': '7.15.4', 436 | 'devDependencies.@babel/preset-env': '7.15.6', 437 | 'devDependencies.@babel/preset-typescript': '7.15.0', 438 | 'devDependencies.app-root-path': '3.0.0', 439 | 'devDependencies.babel-jest': '27.2.3', 440 | 'devDependencies.chokidar': '3.5.1', 441 | 'devDependencies.commitizen': '4.2.3', 442 | 'devDependencies.cz-conventional-changelog': '^3.2.0', 443 | 'devDependencies.dotenv': '8.2.0', 444 | 'devDependencies.enzyme': '3.11.0', 445 | 'devDependencies.enzyme-adapter-react-16': '1.15.6', 446 | 'devDependencies.husky': '4.3.8', 447 | 'devDependencies.prettier': '2.2.1', 448 | 'devDependencies.platform-detect': '3.0.1', 449 | 'devDependencies.@typescript-eslint/eslint-plugin': '^4.0.1', 450 | 'devDependencies.@typescript-eslint/parser': '^4.0.1', 451 | 'devDependencies.@openzeppelin/contracts': '^4.3.0', 452 | 'devDependencies.eslint': '^7.8.0', 453 | 'devDependencies.eslint-config-prettier': '^6.11.0', 454 | 'devDependencies.eslint-plugin-eslint-comments': '^3.2.0', 455 | 'devDependencies.eslint-plugin-functional': '^3.0.2', 456 | 'devDependencies.eslint-plugin-import': '^2.22.0', 457 | 'devDependencies.eslint-plugin-react': '7.22.0', 458 | 'devDependencies.eslint-plugin-react-native': '3.10.0', 459 | 'devDependencies.lint-staged': '10.5.3', 460 | 'devDependencies.@types/node': '14.14.22', 461 | "devDependencies.@types/jest": '^26.0.20', 462 | 'devDependencies.hardhat': '2.0.6', 463 | 'devDependencies.@nomiclabs/hardhat-ethers': '^2.0.1', 464 | 'devDependencies.@nomiclabs/hardhat-waffle': '^2.0.1', 465 | 'devDependencies.chai': '^4.2.0', 466 | 'devDependencies.ethereum-waffle': '^3.2.1', 467 | 'devDependencies.gh-pages': '^3.2.3', 468 | 'devDependencies.jest': '26.6.3', 469 | 'devDependencies.react-test-renderer': '17.0.1', 470 | 'devDependencies.ts-node': '9.1.1', 471 | // react-native 472 | 'react-native.stream': 'react-native-stream', 473 | 'react-native.crypto': 'react-native-crypto', 474 | 'react-native.path': 'path-browserify', 475 | 'react-native.process': 'node-libs-browser/mock/process', 476 | // jest 477 | 'jest.preset': 'react-native', 478 | 'jest.testMatch': ["**/__tests__/frontend/**/*.[jt]s?(x)"], 479 | 'jest.transformIgnorePatterns': ['node_modules/(?!@ngrx|(?!deck.gl)|ng-dynamic)'] 480 | }, 481 | { 482 | config: { 483 | commitizen: { 484 | path: './node_modules/cz-conventional-changelog' 485 | } 486 | }, 487 | husky: { 488 | hooks: { 489 | 'prepare-commit-msg': 'exec < /dev/tty && git cz --hook', 490 | 'pre-commit': 'lint-staged && tsc', 491 | 'pre-push': 'test' 492 | } 493 | }, 494 | 'lint-staged': { 495 | '*.{ts,tsx,js,jsx}': "eslint --fix --ext '.ts,.tsx,.js,.jsx' -c .eslintrc.json", 496 | }, 497 | } 498 | ); 499 | 500 | const shouldPrepareMetro = (ctx: createContext) => 501 | fs.writeFileSync( 502 | path.resolve(ctx.projectDir, 'metro.config.js'), 503 | ` 504 | const extraNodeModules = require('node-libs-browser'); 505 | 506 | module.exports = { 507 | resolver: { 508 | extraNodeModules, 509 | }, 510 | transformer: { 511 | assetPlugins: ['expo-asset/tools/hashAssetFiles'], 512 | }, 513 | }; 514 | `.trim() 515 | ); 516 | 517 | const shouldPrepareBabel = (ctx: createContext) => 518 | fs.writeFileSync( 519 | path.resolve(ctx.projectDir, 'babel.config.js'), 520 | ` 521 | module.exports = function(api) { 522 | api.cache(true); 523 | return { 524 | presets: [ 525 | 'babel-preset-expo', 526 | ['@babel/preset-env', {targets: {node: 'current'}}], 527 | '@babel/preset-typescript', 528 | ], 529 | plugins: [ 530 | ['@babel/plugin-proposal-private-property-in-object', {loose: true}], 531 | ['module:react-native-dotenv'], 532 | ], 533 | }; 534 | }; 535 | `.trim() 536 | ); 537 | 538 | const shouldPrepareEslint = (ctx: createContext) => 539 | fs.writeFileSync( 540 | path.resolve(ctx.projectDir, '.eslintrc.json'), 541 | prettyStringify({ 542 | root: true, 543 | parser: '@typescript-eslint/parser', 544 | env: { es6: true }, 545 | ignorePatterns: [ 546 | 'node_modules', 547 | 'build', 548 | 'coverage', 549 | 'babel.config.js', 550 | 'metro.config.js', 551 | 'hardhat.config.js', 552 | '__tests__/contracts', 553 | ], 554 | plugins: ['import', 'eslint-comments', 'functional', 'react', 'react-native'], 555 | extends: [ 556 | 'eslint:recommended', 557 | 'plugin:eslint-comments/recommended', 558 | 'plugin:@typescript-eslint/recommended', 559 | 'plugin:import/typescript', 560 | 'plugin:functional/lite', 561 | 'prettier', 562 | 'prettier/@typescript-eslint', 563 | ], 564 | globals: { 565 | // TODO: Enable support in RN for BigInteger. 566 | //BigInt: true, 567 | console: true, 568 | __DEV__: true, 569 | }, 570 | rules: { 571 | '@typescript-eslint/explicit-module-boundary-types': 'off', 572 | 'eslint-comments/disable-enable-pair': [ 573 | 'error', 574 | { allowWholeFile: true }, 575 | ], 576 | 'eslint-comments/no-unused-disable': 'error', 577 | 'import/order': [ 578 | 'error', 579 | { 'newlines-between': 'always', alphabetize: { order: 'asc' } }, 580 | ], 581 | 'sort-imports': [ 582 | 'error', 583 | { ignoreDeclarationSort: true, ignoreCase: true }, 584 | ], 585 | 'sort-keys': [ 586 | 'error', 587 | 'asc', 588 | { 589 | 'caseSensitive': true, 590 | 'natural': false, 591 | 'minKeys': 2, 592 | }, 593 | ], 594 | 'react-native/no-unused-styles': 2, 595 | 'react-native/split-platform-components': 2, 596 | 'react-native/no-inline-styles': 2, 597 | 'react-native/no-color-literals': 2, 598 | 'react-native/no-raw-text': 2, 599 | 'react-native/no-single-element-style-arrays': 2, 600 | }, 601 | parserOptions: { 602 | ecmaFeatures: { 603 | jsx: true, 604 | }, 605 | }, 606 | }) 607 | ); 608 | 609 | const shouldWriteEnv = (ctx: createContext) => { 610 | const lines = getAllEnvVariables(ctx).map( 611 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 612 | ([name, _type, value]) => `${name}=${value}` 613 | ); 614 | const env = path.resolve(ctx.projectDir, '.env'); 615 | const example = path.resolve(ctx.projectDir, '.env.example'); 616 | fs.writeFileSync(env, `${lines.join('\n')}\n`); 617 | fs.copyFileSync(env, example); 618 | }; 619 | 620 | const shouldInstall = (ctx: createContext) => 621 | execSync( 622 | `${ctx.yarn ? 'yarn' : 'npm i'}`, 623 | { 624 | stdio: 'inherit', 625 | cwd: `${ctx.projectDir}`, 626 | } 627 | ); 628 | 629 | const getExampleContract = () => 630 | ` 631 | // SPDX-License-Identifier: MIT 632 | pragma solidity >=0.4.22 <0.9.0; 633 | 634 | import "hardhat/console.sol"; 635 | 636 | contract Hello { 637 | string defaultSuffix; 638 | constructor() { 639 | defaultSuffix = '!'; 640 | } 641 | function sayHello(string memory name) public view returns(string memory) { 642 | console.log("Saying hello to %s!", msg.sender); 643 | return string(abi.encodePacked("Welcome to ", name, defaultSuffix)); 644 | } 645 | } 646 | `.trim(); 647 | 648 | const shouldPrepareExample = (ctx: createContext) => { 649 | const { 650 | projectDir, 651 | testsDir, 652 | srcDir, 653 | hardhat: { 654 | hardhatConfig, 655 | hardhatAccounts, 656 | }, 657 | } = ctx; 658 | 659 | const contracts = path.resolve(projectDir, 'contracts'); 660 | const patches = path.resolve(projectDir, 'patches'); 661 | 662 | !fs.existsSync(contracts) && fs.mkdirSync(contracts); 663 | !fs.existsSync(patches) && fs.mkdirSync(patches); 664 | !fs.existsSync(testsDir) && fs.mkdirSync(testsDir); 665 | 666 | const contractsTestDir = path.resolve(testsDir, 'contracts'); 667 | const frontendTestDir = path.resolve(testsDir, 'frontend'); 668 | 669 | fs.mkdirSync(contractsTestDir); 670 | fs.mkdirSync(frontendTestDir); 671 | 672 | fs.writeFileSync(path.resolve(contractsTestDir, '.gitkeep'), ''); 673 | fs.writeFileSync(path.resolve(frontendTestDir, '.gitkeep'), ''); 674 | 675 | const contractTest = path.resolve(contractsTestDir, 'Hello.test.js'); 676 | const frontendTest = path.resolve(frontendTestDir, 'App.test.tsx'); 677 | 678 | fs.writeFileSync( 679 | contractTest, 680 | ` 681 | const { expect } = require('chai'); 682 | 683 | describe("Hello", function() { 684 | it("Should return the default greeting", async function() { 685 | const Hello = await ethers.getContractFactory("Hello"); 686 | const hello = await Hello.deploy(); 687 | 688 | await hello.deployed(); 689 | 690 | expect(await hello.sayHello("React Native")).to.equal("Welcome to React Native!"); 691 | expect(await hello.sayHello("Web3")).to.equal("Welcome to Web3!"); 692 | }); 693 | }); 694 | ` 695 | ); 696 | 697 | fs.writeFileSync( 698 | frontendTest, 699 | ` 700 | import Enzyme from 'enzyme'; 701 | import Adapter from 'enzyme-adapter-react-16'; 702 | import React from 'react'; 703 | 704 | import App from '../../frontend/App'; 705 | 706 | Enzyme.configure({ adapter: new Adapter() }); 707 | 708 | test('renders correctly', () => { 709 | const wrapper = Enzyme.shallow(); 710 | 711 | expect(wrapper.find({ testID: 'tid-message'}).contains('Loading...')).toBe(true); 712 | }); 713 | `.trim(), 714 | ); 715 | 716 | const contract = path.resolve(contracts, 'Hello.sol'); 717 | fs.writeFileSync(contract, getExampleContract()); 718 | 719 | fs.writeFileSync( 720 | hardhatConfig, 721 | ` 722 | /** 723 | * @type import('hardhat/config').HardhatUserConfig 724 | */ 725 | require("@nomiclabs/hardhat-waffle"); 726 | require("dotenv/config"); 727 | 728 | const { HARDHAT_PORT } = process.env; 729 | 730 | module.exports = { 731 | solidity: "0.7.3", 732 | networks: { 733 | localhost: { url: \`http://127.0.0.1:\${HARDHAT_PORT}\` }, 734 | hardhat: { 735 | accounts: ${JSON.stringify(hardhatAccounts)} 736 | }, 737 | }, 738 | paths: { 739 | sources: './contracts', 740 | tests: './__tests__/contracts', 741 | cache: './cache', 742 | artifacts: './artifacts', 743 | }, 744 | }; 745 | `.trim() 746 | ); 747 | 748 | !fs.existsSync(srcDir) && fs.mkdirSync(srcDir); 749 | 750 | fs.writeFileSync( 751 | path.resolve(srcDir, 'App.tsx'), 752 | ` 753 | import { HARDHAT_PORT, HARDHAT_PRIVATE_KEY } from '@env'; 754 | import { useWalletConnect } from '@walletconnect/react-native-dapp'; 755 | import React from 'react'; 756 | import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; 757 | import localhost from 'react-native-localhost'; 758 | import Web3 from 'web3'; 759 | 760 | import Hello from '../artifacts/contracts/Hello.sol/Hello.json'; 761 | 762 | const styles = StyleSheet.create({ 763 | center: { alignItems: 'center', justifyContent: 'center' }, 764 | // eslint-disable-next-line react-native/no-color-literals 765 | white: { backgroundColor: 'white' }, 766 | }); 767 | 768 | const shouldDeployContract = async (web3, abi, data, from: string) => { 769 | const deployment = new web3.eth.Contract(abi).deploy({ data }); 770 | const gas = await deployment.estimateGas(); 771 | const { 772 | options: { address: contractAddress }, 773 | } = await deployment.send({ from, gas }); 774 | return new web3.eth.Contract(abi, contractAddress); 775 | }; 776 | 777 | export default function App(): JSX.Element { 778 | const connector = useWalletConnect(); 779 | const [message, setMessage] = React.useState('Loading...'); 780 | const web3 = React.useMemo( 781 | () => new Web3(new Web3.providers.HttpProvider(\`http://\${localhost}:\${HARDHAT_PORT}\`)), 782 | [HARDHAT_PORT] 783 | ); 784 | React.useEffect(() => { 785 | void (async () => { 786 | const { address } = await web3.eth.accounts.privateKeyToAccount(HARDHAT_PRIVATE_KEY); 787 | const contract = await shouldDeployContract( 788 | web3, 789 | Hello.abi, 790 | Hello.bytecode, 791 | address 792 | ); 793 | setMessage(await contract.methods.sayHello('React Native').call()); 794 | })(); 795 | }, [web3, shouldDeployContract, setMessage, HARDHAT_PRIVATE_KEY]); 796 | const connectWallet = React.useCallback(() => { 797 | return connector.connect(); 798 | }, [connector]); 799 | const signTransaction = React.useCallback(async () => { 800 | try { 801 | await connector.signTransaction({ 802 | data: '0x', 803 | from: '0xbc28Ea04101F03aA7a94C1379bc3AB32E65e62d3', 804 | gas: '0x9c40', 805 | gasPrice: '0x02540be400', 806 | nonce: '0x0114', 807 | to: '0x89D24A7b4cCB1b6fAA2625Fe562bDd9A23260359', 808 | value: '0x00', 809 | }); 810 | } catch (e) { 811 | console.error(e); 812 | } 813 | }, [connector]); 814 | const killSession = React.useCallback(() => { 815 | return connector.killSession(); 816 | }, [connector]); 817 | return ( 818 | 819 | {message} 820 | {!connector.connected && ( 821 | 822 | Connect a Wallet 823 | 824 | )} 825 | {!!connector.connected && ( 826 | <> 827 | 828 | Sign a Transaction 829 | 830 | 831 | Kill Session 832 | 833 | 834 | )} 835 | 836 | ); 837 | } 838 | `.trim() 839 | ); 840 | 841 | const orig = path.resolve(projectDir, 'App.tsx'); 842 | fs.existsSync(orig) && fs.unlinkSync(orig); 843 | 844 | execSync(`npx hardhat compile`, { cwd: `${projectDir}`, stdio: 'inherit' }); 845 | }; 846 | 847 | const getHardhatGitIgnore = (): string | null => { 848 | return ` 849 | # Hardhat 850 | artifacts/ 851 | cache/ 852 | `.trim(); 853 | }; 854 | 855 | const shouldPrepareGitignore = (ctx: createContext) => { 856 | const { projectDir } = ctx; 857 | const lines = [getHardhatGitIgnore()].filter((e) => !!e) as readonly string[]; 858 | const gitignore = path.resolve(projectDir, '.gitignore'); 859 | fs.writeFileSync( 860 | gitignore, 861 | ` 862 | ${fs.readFileSync(gitignore, 'utf-8')} 863 | # Environment Variables (Store safe defaults in .env.example!) 864 | .env 865 | 866 | # Jest 867 | .snap 868 | 869 | # Package Managers 870 | ${ctx.yarn ? 'package-lock.json' : 'yarn.lock'} 871 | 872 | ${lines.join('\n\n')} 873 | 874 | `.trim() 875 | ); 876 | }; 877 | 878 | const getScriptCommandString = (ctx: createContext, str: string) => 879 | chalk.white.bold`${ctx.yarn ? 'yarn' : 'npm run-script'} ${str}`; 880 | 881 | export const getSuccessMessage = (ctx: createContext): string => { 882 | return ` 883 | ${chalk.green`✔`} Successfully integrated Web3 into React Native! 884 | 885 | To compile and run your project in development, execute one of the following commands: 886 | - ${getScriptCommandString(ctx, `ios`)} 887 | - ${getScriptCommandString(ctx, `android`)} 888 | - ${getScriptCommandString(ctx, `web`)} 889 | 890 | `.trim(); 891 | }; 892 | 893 | export const create = async (params: createParams): Promise => { 894 | createBaseProject(params); 895 | 896 | const ctx = await createBaseContext(params); 897 | 898 | if (!fs.existsSync(ctx.projectDir)) { 899 | return Object.freeze({ 900 | ...ctx, 901 | status: CreationStatus.FAILURE, 902 | message: `Failed to resolve project directory.`, 903 | }); 904 | } 905 | 906 | setAppIcon(ctx); 907 | //ejectExpoProject(ctx); 908 | injectShims(ctx); 909 | createScripts(ctx); 910 | preparePackage(ctx); 911 | shouldPrepareMetro(ctx); 912 | shouldPrepareBabel(ctx); 913 | shouldPrepareEslint(ctx); 914 | shouldPrepareTypeRoots(ctx); 915 | shouldPrepareSpelling(ctx); 916 | shouldPrepareTsc(ctx); 917 | shouldPrepareGitignore(ctx); 918 | shouldWriteEnv(ctx); 919 | shouldInstall(ctx); 920 | shouldPrepareExample(ctx); 921 | 922 | return Object.freeze({ 923 | ...ctx, 924 | status: CreationStatus.SUCCESS, 925 | message: getSuccessMessage(ctx), 926 | }); 927 | }; 928 | --------------------------------------------------------------------------------