├── .dockerignore ├── .env.example ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── user-story.md ├── .gitignore ├── .graphqlconfig.yml ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── database ├── datamodel.graphql └── prisma.yml ├── docker-compose.yaml ├── package.json ├── resources └── logo.png ├── scripts └── wait-for.sh ├── src ├── context.ts ├── generated │ ├── prisma.graphql │ └── prisma.ts ├── index.ts ├── phone.ts ├── redis.ts ├── resolvers │ ├── BigNumber.ts │ ├── EthereumAddress.ts │ ├── EthereumAddressString.ts │ ├── EthereumBlock.ts │ ├── EthereumContractMethod.ts │ ├── EthereumGenericContract.ts │ ├── EthereumIdentityContract.ts │ ├── EthereumLog.ts │ ├── EthereumTokenContract.ts │ ├── EthereumTransaction.ts │ ├── EthereumValue.ts │ ├── HexValue.ts │ ├── Mutation.ts │ ├── PhoneNumber.ts │ ├── Query.ts │ └── index.ts ├── schema.graphql ├── tokens.ts ├── usernames.ts └── web3 │ ├── abis │ ├── erc20.ts │ ├── erc721.ts │ └── erc725.ts │ ├── address.ts │ ├── client.ts │ ├── contracts.ts │ ├── ens │ ├── abi.ts │ └── resolve.ts │ ├── etherscan.ts │ ├── loaders.ts │ └── standards.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .git 4 | .env 5 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Infura 2 | INFURA_API_KEY=00000000000000000000000000000000 3 | 4 | # Twilio 5 | TWILIO_ACCOUNT_SID=0000000000000000000000000000000000 6 | TWILIO_API_KEY=00000000000000000000000000000000 7 | TWILIO_FROM_NUMBER=+12345678900 8 | 9 | # Redis 10 | REDIS_URL=redis://localhost:6379/0 11 | 12 | # Prisma 13 | PRISMA_ENDPOINT=http://localhost:4466 14 | PRISMA_MANAGEMENT_API_SECRET=not-a-secret 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/user-story.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: User story 3 | about: Template for user stories 4 | 5 | --- 6 | 7 | **As a** Multi user 8 | **I want to** 9 | **So that I** 10 | 11 | --- 12 | 13 | TODO: Add notes here. 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # mono 2 | *.log 3 | etc/openvpn 4 | pkg 5 | src/dash/protobufs/*.proto 6 | src/reports/**/report.html 7 | src/reports/**/report.pdf 8 | src/vendor 9 | usr/local 10 | tmp 11 | .vimlocal 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | # Python 17 | .ipynb_checkpoints 18 | *.ipynb 19 | __pycache__/ 20 | *.py[cod] 21 | *$py.class 22 | .python-version 23 | .venv/ 24 | 25 | # Java 26 | *.jar 27 | 28 | # Go 29 | bin/golint 30 | bin/protoc-gen-go 31 | bazel-* 32 | 33 | # JavaScript 34 | node_modules 35 | 36 | # TypeScript 37 | dist 38 | .env 39 | -------------------------------------------------------------------------------- /.graphqlconfig.yml: -------------------------------------------------------------------------------- 1 | projects: 2 | app: 3 | schemaPath: src/schema.graphql 4 | extensions: 5 | endpoints: 6 | default: http://localhost:4000 7 | database: 8 | schemaPath: src/generated/prisma.graphql 9 | extensions: 10 | prisma: database/prisma.yml 11 | codegen: 12 | - generator: prisma-binding 13 | language: typescript 14 | output: 15 | binding: src/generated/prisma.ts 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "9" 4 | script: 5 | - yarn run check 6 | - yarn run lint 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:9-alpine 2 | 3 | RUN apk add --no-cache g++ git make python-dev 4 | 5 | COPY package.json yarn.lock ./ 6 | RUN yarn install && \ 7 | apk del g++ git make python-dev 8 | 9 | COPY tsconfig.json ./ 10 | COPY src src 11 | 12 | RUN `yarn bin`/tsc -p . 13 | 14 | COPY .graphqlconfig.yml .graphqlconfig.yml 15 | COPY database database 16 | COPY src/schema.graphql dist 17 | COPY src/generated/prisma.graphql dist/generated/prisma.graphql 18 | 19 | RUN echo "cd /database && /node_modules/.bin/prisma deploy" > deploy.sh && chmod +x deploy.sh 20 | 21 | ENV NODE_ENV production 22 | ENV PORT 4000 23 | CMD node /dist/index.js 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Distributed Systems, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

logo

2 | 3 |

TravisCI build status GraphQL enabled 4 | 5 | **Cleargraph** is a multi-purpose GraphQL runtime for decentralized applications (dApps) and wallets. Its API is infrastructure that makes it easy for application developers to create products and services that take advantage of the Ethereum blockchain without sacrificing usability, flexibility, or security. Cleargraph is the essential missing link between conventional application development and on-chain smart contracts. [**Read the announcement.**](https://medium.com/dsys/build-the-next-big-decentralized-application-with-cleargraph-4b1f34d2675) 6 | 7 | ## Usage 8 | 9 | ```sh 10 | git clone git@github.com:dsys/cleargraph.git 11 | cd cleargraph 12 | yarn install 13 | yarn start # listening on localhost:4000 14 | ``` 15 | 16 | ## Development 17 | 18 | Contributions to Cleargraph are welcome. Our issues are maintained on GitHub and pull requests are appreciated. We'd love help with feature additions, documentation improvements, and more thorough tests. 19 | 20 | ```sh 21 | yarn dev 22 | ``` 23 | 24 | ## License 25 | 26 | Apache 2.0 27 | -------------------------------------------------------------------------------- /database/datamodel.graphql: -------------------------------------------------------------------------------- 1 | type PhoneNumber { 2 | hashedPhoneNumber: String! @unique 3 | address: String! 4 | createdAt: DateTime! 5 | updatedAt: DateTime! 6 | } 7 | -------------------------------------------------------------------------------- /database/prisma.yml: -------------------------------------------------------------------------------- 1 | # The endpoint of your Prisma API (deployed to a Prisma Sandbox). 2 | # endpoint: http://localhost:4466 3 | endpoint: ${env:PRISMA_ENDPOINT} 4 | 5 | # The file containing the definition of your data model. 6 | datamodel: datamodel.graphql 7 | 8 | # Seed your service with initial data based on `seed.graphql`. 9 | seed: 10 | import: seed.graphql 11 | 12 | # Download the GraphQL schema of the Prisma API into 13 | # `src/generated/prisma.graphql` (as specfied in `.graphqlconfig.yml`). 14 | # Then generate the corresponding TypeScript definitions into 15 | # `src/generated/prisma.ts` (also specfied in `.graphqlconfig.yml`) 16 | # with `graphql codegen` . 17 | # hooks: 18 | # post-deploy: 19 | # - yarn run codegen 20 | 21 | # If specified, the `secret` must be used to generate a JWT which is attached 22 | # to the `Authorization` header of HTTP requests made against the Prisma API. 23 | # Info: https://www.prisma.io/docs/reference/prisma-api/concepts-utee3eiquo#authentication 24 | secret: ${env:PRISMA_SECRET} 25 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | redis: 4 | container_name: redis 5 | image: redis:latest 6 | ports: 7 | - 6379:6379 8 | command: 9 | - redis-server 10 | - --appendonly 11 | - 'yes' 12 | volumes: 13 | - ./tmp/volumes/redis:/data 14 | 15 | prisma: 16 | image: prismagraphql/prisma:1.9 17 | ports: 18 | - "4466:4466" 19 | environment: 20 | PRISMA_CONFIG: | 21 | managementApiSecret: not-a-secret 22 | port: 4466 23 | databases: 24 | default: 25 | connector: postgres 26 | host: postgres 27 | port: 5432 28 | user: root 29 | password: prisma 30 | migrations: true 31 | managementSchema: management 32 | database: prisma 33 | links: 34 | - postgres 35 | 36 | postgres: 37 | image: postgres:latest 38 | ports: 39 | - "5432:5432" 40 | environment: 41 | POSTGRES_USER: root 42 | POSTGRES_PASSWORD: prisma 43 | volumes: 44 | - ./tmp/volumes/postgres:/var/lib/postgresql/data 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dsys/cleargraph", 3 | "version": "0.1.0", 4 | "license": "Apache-2.0", 5 | "author": "Alex Kern ", 6 | "scripts": { 7 | "start": "nodemon -e ts,graphql -x ts-node -r dotenv/config src/index.ts", 8 | "debug": "nodemon -e ts,graphql -x ts-node -r dotenv/config --inspect src/index.ts", 9 | "dev": "npm-run-all --parallel dev:cleargraph dev:deps dev:prisma:watch dev:playground", 10 | "dev:cleargraph": "./scripts/wait-for.sh localhost:6379 -- ./scripts/wait-for.sh localhost:4466 -- yarn run start", 11 | "dev:deps": "docker-compose up redis prisma", 12 | "dev:prisma:watch": "./scripts/wait-for.sh localhost:4466 -- echo \"Waiting for Prisma to finish starting\" && sleep 10 && nodemon --watch database -e graphql,yml -x yarn run dev:prisma:deploy", 13 | "dev:prisma:deploy": "yarn prisma deploy && yarn run codegen", 14 | "dev:playground": "graphql playground", 15 | "codegen": "graphql get-schema --project database && graphql codegen", 16 | "lint": "npm-run-all --parallel lint:typescript lint:graphql", 17 | "lint:typescript": "tslint --fix --exclude src/generated/prisma.ts 'src/**/*.ts'", 18 | "lint:graphql": "prettier --write 'database/**/*.graphql' 'src/schema.graphql'", 19 | "check": "tsc --noEmit && tslint --exclude src/generated/prisma.ts 'src/**/*.ts' && prettier -l 'database/**/*.graphql' 'src/schema.graphql'", 20 | "precommit": "yarn run check", 21 | "docker:build": "docker build -t dsys/cleargraph:latest .", 22 | "docker:run": "docker run -it dsys/cleargraph:latest" 23 | }, 24 | "dependencies": { 25 | "apollo-engine": "^1.1.2", 26 | "bignumber.js": "^7.2.1", 27 | "bluebird": "^3.5.1", 28 | "dataloader": "^1.4.0", 29 | "eth-ens-namehash": "^2.0.8", 30 | "graphql-type-json": "^0.2.1", 31 | "graphql-yoga": "1.14.6", 32 | "jsonwebtoken": "^8.3.0", 33 | "libphonenumber-js": "^1.2.15", 34 | "moment": "^2.22.2", 35 | "node-fetch": "^2.1.2", 36 | "prisma-binding": "^2.0.2", 37 | "qs": "^6.5.2", 38 | "redis": "^2.8.0", 39 | "web3": "^1.0.0-beta.34" 40 | }, 41 | "devDependencies": { 42 | "dotenv": "^6.0.0", 43 | "graphql-cli": "2.16.0", 44 | "husky": "^0.14.3", 45 | "nodemon": "1.17.5", 46 | "npm-run-all": "4.1.3", 47 | "prettier": "^1.13.5", 48 | "prisma": "^1.9.0", 49 | "ts-node": "^6.1.0", 50 | "tslint": "^5.10.0", 51 | "tslint-config-prettier": "^1.13.0", 52 | "tslint-plugin-prettier": "^1.3.0", 53 | "typescript": "^2.9.1", 54 | "web3-typescript-typings": "^0.10.2" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsys/cleargraph/b53fa767f0295802799b4fb49e57f1a85b20f010/resources/logo.png -------------------------------------------------------------------------------- /scripts/wait-for.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TIMEOUT=15 4 | QUIET=0 5 | 6 | echoerr() { 7 | if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi 8 | } 9 | 10 | usage() { 11 | exitcode="$1" 12 | cat << USAGE >&2 13 | Usage: 14 | $cmdname host:port [-t timeout] [-- command args] 15 | -q | --quiet Do not output any status messages 16 | -t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout 17 | -- COMMAND ARGS Execute command with args after the test finishes 18 | USAGE 19 | exit "$exitcode" 20 | } 21 | 22 | wait_for() { 23 | for i in `seq $TIMEOUT` ; do 24 | nc -z "$HOST" "$PORT" > /dev/null 2>&1 25 | 26 | result=$? 27 | if [ $result -eq 0 ] ; then 28 | if [ $# -gt 0 ] ; then 29 | exec "$@" 30 | fi 31 | exit 0 32 | fi 33 | sleep 1 34 | done 35 | echo "Operation timed out" >&2 36 | exit 1 37 | } 38 | 39 | while [ $# -gt 0 ] 40 | do 41 | case "$1" in 42 | *:* ) 43 | HOST=$(printf "%s\n" "$1"| cut -d : -f 1) 44 | PORT=$(printf "%s\n" "$1"| cut -d : -f 2) 45 | shift 1 46 | ;; 47 | -q | --quiet) 48 | QUIET=1 49 | shift 1 50 | ;; 51 | -t) 52 | TIMEOUT="$2" 53 | if [ "$TIMEOUT" = "" ]; then break; fi 54 | shift 2 55 | ;; 56 | --timeout=*) 57 | TIMEOUT="${1#*=}" 58 | shift 1 59 | ;; 60 | --) 61 | shift 62 | break 63 | ;; 64 | --help) 65 | usage 0 66 | ;; 67 | *) 68 | echoerr "Unknown argument: $1" 69 | usage 1 70 | ;; 71 | esac 72 | done 73 | 74 | if [ "$HOST" = "" -o "$PORT" = "" ]; then 75 | echoerr "Error: you need to provide a host and port to test." 76 | usage 2 77 | fi 78 | 79 | wait_for "$@" 80 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from "./generated/prisma"; 2 | import { createWeb3Loaders } from "./web3/loaders"; 3 | 4 | export interface Context { 5 | db: Prisma; 6 | loaders: { 7 | web3: any; 8 | }; 9 | req: any; 10 | } 11 | 12 | export function createContext(req): Context { 13 | return { 14 | db: new Prisma({ 15 | debug: true, // log all GraphQL queries & mutations sent to the Prisma API 16 | endpoint: process.env.PRISMA_ENDPOINT, // the endpoint of the Prisma API (value set in `.env`) 17 | secret: process.env.PRISMA_SECRET // only needed if specified in `database/prisma.yml` (value set in `.env`) 18 | }), 19 | loaders: { web3: createWeb3Loaders() }, 20 | req 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/generated/prisma.graphql: -------------------------------------------------------------------------------- 1 | # source: http://localhost:4466 2 | # timestamp: Thu Jun 14 2018 17:06:22 GMT-0700 (PDT) 3 | 4 | type AggregatePhoneNumber { 5 | count: Int! 6 | } 7 | 8 | type BatchPayload { 9 | """ 10 | The number of nodes that have been affected by the Batch operation. 11 | """ 12 | count: Long! 13 | } 14 | 15 | scalar DateTime 16 | 17 | """ 18 | The `Long` scalar type represents non-fractional signed whole numeric values. 19 | Long can represent values between -(2^63) and 2^63 - 1. 20 | """ 21 | scalar Long 22 | 23 | type Mutation { 24 | createPhoneNumber(data: PhoneNumberCreateInput!): PhoneNumber! 25 | updatePhoneNumber( 26 | data: PhoneNumberUpdateInput! 27 | where: PhoneNumberWhereUniqueInput! 28 | ): PhoneNumber 29 | deletePhoneNumber(where: PhoneNumberWhereUniqueInput!): PhoneNumber 30 | upsertPhoneNumber( 31 | where: PhoneNumberWhereUniqueInput! 32 | create: PhoneNumberCreateInput! 33 | update: PhoneNumberUpdateInput! 34 | ): PhoneNumber! 35 | updateManyPhoneNumbers( 36 | data: PhoneNumberUpdateInput! 37 | where: PhoneNumberWhereInput 38 | ): BatchPayload! 39 | deleteManyPhoneNumbers(where: PhoneNumberWhereInput): BatchPayload! 40 | } 41 | 42 | enum MutationType { 43 | CREATED 44 | UPDATED 45 | DELETED 46 | } 47 | 48 | """ 49 | An object with an ID 50 | """ 51 | interface Node { 52 | """ 53 | The id of the object. 54 | """ 55 | id: ID! 56 | } 57 | 58 | """ 59 | Information about pagination in a connection. 60 | """ 61 | type PageInfo { 62 | """ 63 | When paginating forwards, are there more items? 64 | """ 65 | hasNextPage: Boolean! 66 | 67 | """ 68 | When paginating backwards, are there more items? 69 | """ 70 | hasPreviousPage: Boolean! 71 | 72 | """ 73 | When paginating backwards, the cursor to continue. 74 | """ 75 | startCursor: String 76 | 77 | """ 78 | When paginating forwards, the cursor to continue. 79 | """ 80 | endCursor: String 81 | } 82 | 83 | type PhoneNumber { 84 | hashedPhoneNumber: String! 85 | address: String! 86 | createdAt: DateTime! 87 | updatedAt: DateTime! 88 | } 89 | 90 | """ 91 | A connection to a list of items. 92 | """ 93 | type PhoneNumberConnection { 94 | """ 95 | Information to aid in pagination. 96 | """ 97 | pageInfo: PageInfo! 98 | 99 | """ 100 | A list of edges. 101 | """ 102 | edges: [PhoneNumberEdge]! 103 | aggregate: AggregatePhoneNumber! 104 | } 105 | 106 | input PhoneNumberCreateInput { 107 | hashedPhoneNumber: String! 108 | address: String! 109 | } 110 | 111 | """ 112 | An edge in a connection. 113 | """ 114 | type PhoneNumberEdge { 115 | """ 116 | The item at the end of the edge. 117 | """ 118 | node: PhoneNumber! 119 | 120 | """ 121 | A cursor for use in pagination. 122 | """ 123 | cursor: String! 124 | } 125 | 126 | enum PhoneNumberOrderByInput { 127 | hashedPhoneNumber_ASC 128 | hashedPhoneNumber_DESC 129 | address_ASC 130 | address_DESC 131 | createdAt_ASC 132 | createdAt_DESC 133 | updatedAt_ASC 134 | updatedAt_DESC 135 | id_ASC 136 | id_DESC 137 | } 138 | 139 | type PhoneNumberPreviousValues { 140 | hashedPhoneNumber: String! 141 | address: String! 142 | createdAt: DateTime! 143 | updatedAt: DateTime! 144 | } 145 | 146 | type PhoneNumberSubscriptionPayload { 147 | mutation: MutationType! 148 | node: PhoneNumber 149 | updatedFields: [String!] 150 | previousValues: PhoneNumberPreviousValues 151 | } 152 | 153 | input PhoneNumberSubscriptionWhereInput { 154 | """ 155 | Logical AND on all given filters. 156 | """ 157 | AND: [PhoneNumberSubscriptionWhereInput!] 158 | 159 | """ 160 | Logical OR on all given filters. 161 | """ 162 | OR: [PhoneNumberSubscriptionWhereInput!] 163 | 164 | """ 165 | Logical NOT on all given filters combined by AND. 166 | """ 167 | NOT: [PhoneNumberSubscriptionWhereInput!] 168 | 169 | """ 170 | The subscription event gets dispatched when it's listed in mutation_in 171 | """ 172 | mutation_in: [MutationType!] 173 | 174 | """ 175 | The subscription event gets only dispatched when one of the updated fields names is included in this list 176 | """ 177 | updatedFields_contains: String 178 | 179 | """ 180 | The subscription event gets only dispatched when all of the field names included in this list have been updated 181 | """ 182 | updatedFields_contains_every: [String!] 183 | 184 | """ 185 | The subscription event gets only dispatched when some of the field names included in this list have been updated 186 | """ 187 | updatedFields_contains_some: [String!] 188 | node: PhoneNumberWhereInput 189 | } 190 | 191 | input PhoneNumberUpdateInput { 192 | hashedPhoneNumber: String 193 | address: String 194 | } 195 | 196 | input PhoneNumberWhereInput { 197 | """ 198 | Logical AND on all given filters. 199 | """ 200 | AND: [PhoneNumberWhereInput!] 201 | 202 | """ 203 | Logical OR on all given filters. 204 | """ 205 | OR: [PhoneNumberWhereInput!] 206 | 207 | """ 208 | Logical NOT on all given filters combined by AND. 209 | """ 210 | NOT: [PhoneNumberWhereInput!] 211 | hashedPhoneNumber: String 212 | 213 | """ 214 | All values that are not equal to given value. 215 | """ 216 | hashedPhoneNumber_not: String 217 | 218 | """ 219 | All values that are contained in given list. 220 | """ 221 | hashedPhoneNumber_in: [String!] 222 | 223 | """ 224 | All values that are not contained in given list. 225 | """ 226 | hashedPhoneNumber_not_in: [String!] 227 | 228 | """ 229 | All values less than the given value. 230 | """ 231 | hashedPhoneNumber_lt: String 232 | 233 | """ 234 | All values less than or equal the given value. 235 | """ 236 | hashedPhoneNumber_lte: String 237 | 238 | """ 239 | All values greater than the given value. 240 | """ 241 | hashedPhoneNumber_gt: String 242 | 243 | """ 244 | All values greater than or equal the given value. 245 | """ 246 | hashedPhoneNumber_gte: String 247 | 248 | """ 249 | All values containing the given string. 250 | """ 251 | hashedPhoneNumber_contains: String 252 | 253 | """ 254 | All values not containing the given string. 255 | """ 256 | hashedPhoneNumber_not_contains: String 257 | 258 | """ 259 | All values starting with the given string. 260 | """ 261 | hashedPhoneNumber_starts_with: String 262 | 263 | """ 264 | All values not starting with the given string. 265 | """ 266 | hashedPhoneNumber_not_starts_with: String 267 | 268 | """ 269 | All values ending with the given string. 270 | """ 271 | hashedPhoneNumber_ends_with: String 272 | 273 | """ 274 | All values not ending with the given string. 275 | """ 276 | hashedPhoneNumber_not_ends_with: String 277 | address: String 278 | 279 | """ 280 | All values that are not equal to given value. 281 | """ 282 | address_not: String 283 | 284 | """ 285 | All values that are contained in given list. 286 | """ 287 | address_in: [String!] 288 | 289 | """ 290 | All values that are not contained in given list. 291 | """ 292 | address_not_in: [String!] 293 | 294 | """ 295 | All values less than the given value. 296 | """ 297 | address_lt: String 298 | 299 | """ 300 | All values less than or equal the given value. 301 | """ 302 | address_lte: String 303 | 304 | """ 305 | All values greater than the given value. 306 | """ 307 | address_gt: String 308 | 309 | """ 310 | All values greater than or equal the given value. 311 | """ 312 | address_gte: String 313 | 314 | """ 315 | All values containing the given string. 316 | """ 317 | address_contains: String 318 | 319 | """ 320 | All values not containing the given string. 321 | """ 322 | address_not_contains: String 323 | 324 | """ 325 | All values starting with the given string. 326 | """ 327 | address_starts_with: String 328 | 329 | """ 330 | All values not starting with the given string. 331 | """ 332 | address_not_starts_with: String 333 | 334 | """ 335 | All values ending with the given string. 336 | """ 337 | address_ends_with: String 338 | 339 | """ 340 | All values not ending with the given string. 341 | """ 342 | address_not_ends_with: String 343 | createdAt: DateTime 344 | 345 | """ 346 | All values that are not equal to given value. 347 | """ 348 | createdAt_not: DateTime 349 | 350 | """ 351 | All values that are contained in given list. 352 | """ 353 | createdAt_in: [DateTime!] 354 | 355 | """ 356 | All values that are not contained in given list. 357 | """ 358 | createdAt_not_in: [DateTime!] 359 | 360 | """ 361 | All values less than the given value. 362 | """ 363 | createdAt_lt: DateTime 364 | 365 | """ 366 | All values less than or equal the given value. 367 | """ 368 | createdAt_lte: DateTime 369 | 370 | """ 371 | All values greater than the given value. 372 | """ 373 | createdAt_gt: DateTime 374 | 375 | """ 376 | All values greater than or equal the given value. 377 | """ 378 | createdAt_gte: DateTime 379 | updatedAt: DateTime 380 | 381 | """ 382 | All values that are not equal to given value. 383 | """ 384 | updatedAt_not: DateTime 385 | 386 | """ 387 | All values that are contained in given list. 388 | """ 389 | updatedAt_in: [DateTime!] 390 | 391 | """ 392 | All values that are not contained in given list. 393 | """ 394 | updatedAt_not_in: [DateTime!] 395 | 396 | """ 397 | All values less than the given value. 398 | """ 399 | updatedAt_lt: DateTime 400 | 401 | """ 402 | All values less than or equal the given value. 403 | """ 404 | updatedAt_lte: DateTime 405 | 406 | """ 407 | All values greater than the given value. 408 | """ 409 | updatedAt_gt: DateTime 410 | 411 | """ 412 | All values greater than or equal the given value. 413 | """ 414 | updatedAt_gte: DateTime 415 | } 416 | 417 | input PhoneNumberWhereUniqueInput { 418 | hashedPhoneNumber: String 419 | } 420 | 421 | type Query { 422 | phoneNumbers( 423 | where: PhoneNumberWhereInput 424 | orderBy: PhoneNumberOrderByInput 425 | skip: Int 426 | after: String 427 | before: String 428 | first: Int 429 | last: Int 430 | ): [PhoneNumber]! 431 | phoneNumber(where: PhoneNumberWhereUniqueInput!): PhoneNumber 432 | phoneNumbersConnection( 433 | where: PhoneNumberWhereInput 434 | orderBy: PhoneNumberOrderByInput 435 | skip: Int 436 | after: String 437 | before: String 438 | first: Int 439 | last: Int 440 | ): PhoneNumberConnection! 441 | 442 | """ 443 | Fetches an object given its ID 444 | """ 445 | node( 446 | """ 447 | The ID of an object 448 | """ 449 | id: ID! 450 | ): Node 451 | } 452 | 453 | type Subscription { 454 | phoneNumber( 455 | where: PhoneNumberSubscriptionWhereInput 456 | ): PhoneNumberSubscriptionPayload 457 | } 458 | -------------------------------------------------------------------------------- /src/generated/prisma.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLResolveInfo, GraphQLSchema } from 'graphql' 2 | import { IResolvers } from 'graphql-tools/dist/Interfaces' 3 | import { Options } from 'graphql-binding' 4 | import { makePrismaBindingClass, BasePrismaOptions } from 'prisma-binding' 5 | 6 | export interface Query { 7 | phoneNumbers: (args: { where?: PhoneNumberWhereInput, orderBy?: PhoneNumberOrderByInput, skip?: Int, after?: String, before?: String, first?: Int, last?: Int }, info?: GraphQLResolveInfo | string, options?: Options) => Promise , 8 | phoneNumber: (args: { where: PhoneNumberWhereUniqueInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise , 9 | phoneNumbersConnection: (args: { where?: PhoneNumberWhereInput, orderBy?: PhoneNumberOrderByInput, skip?: Int, after?: String, before?: String, first?: Int, last?: Int }, info?: GraphQLResolveInfo | string, options?: Options) => Promise , 10 | node: (args: { id: ID_Output }, info?: GraphQLResolveInfo | string, options?: Options) => Promise 11 | } 12 | 13 | export interface Mutation { 14 | createPhoneNumber: (args: { data: PhoneNumberCreateInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise , 15 | updatePhoneNumber: (args: { data: PhoneNumberUpdateInput, where: PhoneNumberWhereUniqueInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise , 16 | deletePhoneNumber: (args: { where: PhoneNumberWhereUniqueInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise , 17 | upsertPhoneNumber: (args: { where: PhoneNumberWhereUniqueInput, create: PhoneNumberCreateInput, update: PhoneNumberUpdateInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise , 18 | updateManyPhoneNumbers: (args: { data: PhoneNumberUpdateInput, where?: PhoneNumberWhereInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise , 19 | deleteManyPhoneNumbers: (args: { where?: PhoneNumberWhereInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise 20 | } 21 | 22 | export interface Subscription { 23 | phoneNumber: (args: { where?: PhoneNumberSubscriptionWhereInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise> 24 | } 25 | 26 | export interface Exists { 27 | PhoneNumber: (where?: PhoneNumberWhereInput) => Promise 28 | } 29 | 30 | export interface Prisma { 31 | query: Query 32 | mutation: Mutation 33 | subscription: Subscription 34 | exists: Exists 35 | request: (query: string, variables?: {[key: string]: any}) => Promise 36 | delegate(operation: 'query' | 'mutation', fieldName: string, args: { 37 | [key: string]: any; 38 | }, infoOrQuery?: GraphQLResolveInfo | string, options?: Options): Promise; 39 | delegateSubscription(fieldName: string, args?: { 40 | [key: string]: any; 41 | }, infoOrQuery?: GraphQLResolveInfo | string, options?: Options): Promise>; 42 | getAbstractResolvers(filterSchema?: GraphQLSchema | string): IResolvers; 43 | } 44 | 45 | export interface BindingConstructor { 46 | new(options: BasePrismaOptions): T 47 | } 48 | /** 49 | * Type Defs 50 | */ 51 | 52 | const typeDefs = `type AggregatePhoneNumber { 53 | count: Int! 54 | } 55 | 56 | type BatchPayload { 57 | """The number of nodes that have been affected by the Batch operation.""" 58 | count: Long! 59 | } 60 | 61 | scalar DateTime 62 | 63 | """ 64 | The \`Long\` scalar type represents non-fractional signed whole numeric values. 65 | Long can represent values between -(2^63) and 2^63 - 1. 66 | """ 67 | scalar Long 68 | 69 | type Mutation { 70 | createPhoneNumber(data: PhoneNumberCreateInput!): PhoneNumber! 71 | updatePhoneNumber(data: PhoneNumberUpdateInput!, where: PhoneNumberWhereUniqueInput!): PhoneNumber 72 | deletePhoneNumber(where: PhoneNumberWhereUniqueInput!): PhoneNumber 73 | upsertPhoneNumber(where: PhoneNumberWhereUniqueInput!, create: PhoneNumberCreateInput!, update: PhoneNumberUpdateInput!): PhoneNumber! 74 | updateManyPhoneNumbers(data: PhoneNumberUpdateInput!, where: PhoneNumberWhereInput): BatchPayload! 75 | deleteManyPhoneNumbers(where: PhoneNumberWhereInput): BatchPayload! 76 | } 77 | 78 | enum MutationType { 79 | CREATED 80 | UPDATED 81 | DELETED 82 | } 83 | 84 | """An object with an ID""" 85 | interface Node { 86 | """The id of the object.""" 87 | id: ID! 88 | } 89 | 90 | """Information about pagination in a connection.""" 91 | type PageInfo { 92 | """When paginating forwards, are there more items?""" 93 | hasNextPage: Boolean! 94 | 95 | """When paginating backwards, are there more items?""" 96 | hasPreviousPage: Boolean! 97 | 98 | """When paginating backwards, the cursor to continue.""" 99 | startCursor: String 100 | 101 | """When paginating forwards, the cursor to continue.""" 102 | endCursor: String 103 | } 104 | 105 | type PhoneNumber { 106 | hashedPhoneNumber: String! 107 | address: String! 108 | createdAt: DateTime! 109 | updatedAt: DateTime! 110 | } 111 | 112 | """A connection to a list of items.""" 113 | type PhoneNumberConnection { 114 | """Information to aid in pagination.""" 115 | pageInfo: PageInfo! 116 | 117 | """A list of edges.""" 118 | edges: [PhoneNumberEdge]! 119 | aggregate: AggregatePhoneNumber! 120 | } 121 | 122 | input PhoneNumberCreateInput { 123 | hashedPhoneNumber: String! 124 | address: String! 125 | } 126 | 127 | """An edge in a connection.""" 128 | type PhoneNumberEdge { 129 | """The item at the end of the edge.""" 130 | node: PhoneNumber! 131 | 132 | """A cursor for use in pagination.""" 133 | cursor: String! 134 | } 135 | 136 | enum PhoneNumberOrderByInput { 137 | hashedPhoneNumber_ASC 138 | hashedPhoneNumber_DESC 139 | address_ASC 140 | address_DESC 141 | createdAt_ASC 142 | createdAt_DESC 143 | updatedAt_ASC 144 | updatedAt_DESC 145 | id_ASC 146 | id_DESC 147 | } 148 | 149 | type PhoneNumberPreviousValues { 150 | hashedPhoneNumber: String! 151 | address: String! 152 | createdAt: DateTime! 153 | updatedAt: DateTime! 154 | } 155 | 156 | type PhoneNumberSubscriptionPayload { 157 | mutation: MutationType! 158 | node: PhoneNumber 159 | updatedFields: [String!] 160 | previousValues: PhoneNumberPreviousValues 161 | } 162 | 163 | input PhoneNumberSubscriptionWhereInput { 164 | """Logical AND on all given filters.""" 165 | AND: [PhoneNumberSubscriptionWhereInput!] 166 | 167 | """Logical OR on all given filters.""" 168 | OR: [PhoneNumberSubscriptionWhereInput!] 169 | 170 | """Logical NOT on all given filters combined by AND.""" 171 | NOT: [PhoneNumberSubscriptionWhereInput!] 172 | 173 | """ 174 | The subscription event gets dispatched when it's listed in mutation_in 175 | """ 176 | mutation_in: [MutationType!] 177 | 178 | """ 179 | The subscription event gets only dispatched when one of the updated fields names is included in this list 180 | """ 181 | updatedFields_contains: String 182 | 183 | """ 184 | The subscription event gets only dispatched when all of the field names included in this list have been updated 185 | """ 186 | updatedFields_contains_every: [String!] 187 | 188 | """ 189 | The subscription event gets only dispatched when some of the field names included in this list have been updated 190 | """ 191 | updatedFields_contains_some: [String!] 192 | node: PhoneNumberWhereInput 193 | } 194 | 195 | input PhoneNumberUpdateInput { 196 | hashedPhoneNumber: String 197 | address: String 198 | } 199 | 200 | input PhoneNumberWhereInput { 201 | """Logical AND on all given filters.""" 202 | AND: [PhoneNumberWhereInput!] 203 | 204 | """Logical OR on all given filters.""" 205 | OR: [PhoneNumberWhereInput!] 206 | 207 | """Logical NOT on all given filters combined by AND.""" 208 | NOT: [PhoneNumberWhereInput!] 209 | hashedPhoneNumber: String 210 | 211 | """All values that are not equal to given value.""" 212 | hashedPhoneNumber_not: String 213 | 214 | """All values that are contained in given list.""" 215 | hashedPhoneNumber_in: [String!] 216 | 217 | """All values that are not contained in given list.""" 218 | hashedPhoneNumber_not_in: [String!] 219 | 220 | """All values less than the given value.""" 221 | hashedPhoneNumber_lt: String 222 | 223 | """All values less than or equal the given value.""" 224 | hashedPhoneNumber_lte: String 225 | 226 | """All values greater than the given value.""" 227 | hashedPhoneNumber_gt: String 228 | 229 | """All values greater than or equal the given value.""" 230 | hashedPhoneNumber_gte: String 231 | 232 | """All values containing the given string.""" 233 | hashedPhoneNumber_contains: String 234 | 235 | """All values not containing the given string.""" 236 | hashedPhoneNumber_not_contains: String 237 | 238 | """All values starting with the given string.""" 239 | hashedPhoneNumber_starts_with: String 240 | 241 | """All values not starting with the given string.""" 242 | hashedPhoneNumber_not_starts_with: String 243 | 244 | """All values ending with the given string.""" 245 | hashedPhoneNumber_ends_with: String 246 | 247 | """All values not ending with the given string.""" 248 | hashedPhoneNumber_not_ends_with: String 249 | address: String 250 | 251 | """All values that are not equal to given value.""" 252 | address_not: String 253 | 254 | """All values that are contained in given list.""" 255 | address_in: [String!] 256 | 257 | """All values that are not contained in given list.""" 258 | address_not_in: [String!] 259 | 260 | """All values less than the given value.""" 261 | address_lt: String 262 | 263 | """All values less than or equal the given value.""" 264 | address_lte: String 265 | 266 | """All values greater than the given value.""" 267 | address_gt: String 268 | 269 | """All values greater than or equal the given value.""" 270 | address_gte: String 271 | 272 | """All values containing the given string.""" 273 | address_contains: String 274 | 275 | """All values not containing the given string.""" 276 | address_not_contains: String 277 | 278 | """All values starting with the given string.""" 279 | address_starts_with: String 280 | 281 | """All values not starting with the given string.""" 282 | address_not_starts_with: String 283 | 284 | """All values ending with the given string.""" 285 | address_ends_with: String 286 | 287 | """All values not ending with the given string.""" 288 | address_not_ends_with: String 289 | createdAt: DateTime 290 | 291 | """All values that are not equal to given value.""" 292 | createdAt_not: DateTime 293 | 294 | """All values that are contained in given list.""" 295 | createdAt_in: [DateTime!] 296 | 297 | """All values that are not contained in given list.""" 298 | createdAt_not_in: [DateTime!] 299 | 300 | """All values less than the given value.""" 301 | createdAt_lt: DateTime 302 | 303 | """All values less than or equal the given value.""" 304 | createdAt_lte: DateTime 305 | 306 | """All values greater than the given value.""" 307 | createdAt_gt: DateTime 308 | 309 | """All values greater than or equal the given value.""" 310 | createdAt_gte: DateTime 311 | updatedAt: DateTime 312 | 313 | """All values that are not equal to given value.""" 314 | updatedAt_not: DateTime 315 | 316 | """All values that are contained in given list.""" 317 | updatedAt_in: [DateTime!] 318 | 319 | """All values that are not contained in given list.""" 320 | updatedAt_not_in: [DateTime!] 321 | 322 | """All values less than the given value.""" 323 | updatedAt_lt: DateTime 324 | 325 | """All values less than or equal the given value.""" 326 | updatedAt_lte: DateTime 327 | 328 | """All values greater than the given value.""" 329 | updatedAt_gt: DateTime 330 | 331 | """All values greater than or equal the given value.""" 332 | updatedAt_gte: DateTime 333 | } 334 | 335 | input PhoneNumberWhereUniqueInput { 336 | hashedPhoneNumber: String 337 | } 338 | 339 | type Query { 340 | phoneNumbers(where: PhoneNumberWhereInput, orderBy: PhoneNumberOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [PhoneNumber]! 341 | phoneNumber(where: PhoneNumberWhereUniqueInput!): PhoneNumber 342 | phoneNumbersConnection(where: PhoneNumberWhereInput, orderBy: PhoneNumberOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): PhoneNumberConnection! 343 | 344 | """Fetches an object given its ID""" 345 | node( 346 | """The ID of an object""" 347 | id: ID! 348 | ): Node 349 | } 350 | 351 | type Subscription { 352 | phoneNumber(where: PhoneNumberSubscriptionWhereInput): PhoneNumberSubscriptionPayload 353 | } 354 | ` 355 | 356 | export const Prisma = makePrismaBindingClass>({typeDefs}) 357 | 358 | /** 359 | * Types 360 | */ 361 | 362 | export type PhoneNumberOrderByInput = 'hashedPhoneNumber_ASC' | 363 | 'hashedPhoneNumber_DESC' | 364 | 'address_ASC' | 365 | 'address_DESC' | 366 | 'createdAt_ASC' | 367 | 'createdAt_DESC' | 368 | 'updatedAt_ASC' | 369 | 'updatedAt_DESC' | 370 | 'id_ASC' | 371 | 'id_DESC' 372 | 373 | export type MutationType = 'CREATED' | 374 | 'UPDATED' | 375 | 'DELETED' 376 | 377 | export interface PhoneNumberCreateInput { 378 | hashedPhoneNumber: String 379 | address: String 380 | } 381 | 382 | export interface PhoneNumberWhereUniqueInput { 383 | hashedPhoneNumber?: String 384 | } 385 | 386 | export interface PhoneNumberUpdateInput { 387 | hashedPhoneNumber?: String 388 | address?: String 389 | } 390 | 391 | export interface PhoneNumberSubscriptionWhereInput { 392 | AND?: PhoneNumberSubscriptionWhereInput[] | PhoneNumberSubscriptionWhereInput 393 | OR?: PhoneNumberSubscriptionWhereInput[] | PhoneNumberSubscriptionWhereInput 394 | NOT?: PhoneNumberSubscriptionWhereInput[] | PhoneNumberSubscriptionWhereInput 395 | mutation_in?: MutationType[] | MutationType 396 | updatedFields_contains?: String 397 | updatedFields_contains_every?: String[] | String 398 | updatedFields_contains_some?: String[] | String 399 | node?: PhoneNumberWhereInput 400 | } 401 | 402 | export interface PhoneNumberWhereInput { 403 | AND?: PhoneNumberWhereInput[] | PhoneNumberWhereInput 404 | OR?: PhoneNumberWhereInput[] | PhoneNumberWhereInput 405 | NOT?: PhoneNumberWhereInput[] | PhoneNumberWhereInput 406 | hashedPhoneNumber?: String 407 | hashedPhoneNumber_not?: String 408 | hashedPhoneNumber_in?: String[] | String 409 | hashedPhoneNumber_not_in?: String[] | String 410 | hashedPhoneNumber_lt?: String 411 | hashedPhoneNumber_lte?: String 412 | hashedPhoneNumber_gt?: String 413 | hashedPhoneNumber_gte?: String 414 | hashedPhoneNumber_contains?: String 415 | hashedPhoneNumber_not_contains?: String 416 | hashedPhoneNumber_starts_with?: String 417 | hashedPhoneNumber_not_starts_with?: String 418 | hashedPhoneNumber_ends_with?: String 419 | hashedPhoneNumber_not_ends_with?: String 420 | address?: String 421 | address_not?: String 422 | address_in?: String[] | String 423 | address_not_in?: String[] | String 424 | address_lt?: String 425 | address_lte?: String 426 | address_gt?: String 427 | address_gte?: String 428 | address_contains?: String 429 | address_not_contains?: String 430 | address_starts_with?: String 431 | address_not_starts_with?: String 432 | address_ends_with?: String 433 | address_not_ends_with?: String 434 | createdAt?: DateTime 435 | createdAt_not?: DateTime 436 | createdAt_in?: DateTime[] | DateTime 437 | createdAt_not_in?: DateTime[] | DateTime 438 | createdAt_lt?: DateTime 439 | createdAt_lte?: DateTime 440 | createdAt_gt?: DateTime 441 | createdAt_gte?: DateTime 442 | updatedAt?: DateTime 443 | updatedAt_not?: DateTime 444 | updatedAt_in?: DateTime[] | DateTime 445 | updatedAt_not_in?: DateTime[] | DateTime 446 | updatedAt_lt?: DateTime 447 | updatedAt_lte?: DateTime 448 | updatedAt_gt?: DateTime 449 | updatedAt_gte?: DateTime 450 | } 451 | 452 | /* 453 | * An object with an ID 454 | 455 | */ 456 | export interface Node { 457 | id: ID_Output 458 | } 459 | 460 | export interface AggregatePhoneNumber { 461 | count: Int 462 | } 463 | 464 | export interface PhoneNumber { 465 | hashedPhoneNumber: String 466 | address: String 467 | createdAt: DateTime 468 | updatedAt: DateTime 469 | } 470 | 471 | export interface PhoneNumberPreviousValues { 472 | hashedPhoneNumber: String 473 | address: String 474 | createdAt: DateTime 475 | updatedAt: DateTime 476 | } 477 | 478 | export interface PhoneNumberSubscriptionPayload { 479 | mutation: MutationType 480 | node?: PhoneNumber 481 | updatedFields?: String[] 482 | previousValues?: PhoneNumberPreviousValues 483 | } 484 | 485 | /* 486 | * An edge in a connection. 487 | 488 | */ 489 | export interface PhoneNumberEdge { 490 | node: PhoneNumber 491 | cursor: String 492 | } 493 | 494 | /* 495 | * A connection to a list of items. 496 | 497 | */ 498 | export interface PhoneNumberConnection { 499 | pageInfo: PageInfo 500 | edges: PhoneNumberEdge[] 501 | aggregate: AggregatePhoneNumber 502 | } 503 | 504 | /* 505 | * Information about pagination in a connection. 506 | 507 | */ 508 | export interface PageInfo { 509 | hasNextPage: Boolean 510 | hasPreviousPage: Boolean 511 | startCursor?: String 512 | endCursor?: String 513 | } 514 | 515 | export interface BatchPayload { 516 | count: Long 517 | } 518 | 519 | /* 520 | The `Boolean` scalar type represents `true` or `false`. 521 | */ 522 | export type Boolean = boolean 523 | 524 | /* 525 | The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. 526 | */ 527 | export type String = string 528 | 529 | /* 530 | The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID. 531 | */ 532 | export type ID_Input = string | number 533 | export type ID_Output = string 534 | 535 | export type DateTime = Date | string 536 | 537 | /* 538 | The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. 539 | */ 540 | export type Int = number 541 | 542 | /* 543 | The `Long` scalar type represents non-fractional signed whole numeric values. 544 | Long can represent values between -(2^63) and 2^63 - 1. 545 | */ 546 | export type Long = string -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { ApolloEngine } from "apollo-engine"; 2 | import { GraphQLServer } from "graphql-yoga"; 3 | import * as path from "path"; 4 | import { createContext } from "./context"; 5 | import * as resolvers from "./resolvers"; 6 | 7 | const graphQLServer = new GraphQLServer({ 8 | context: createContext, 9 | resolvers, 10 | typeDefs: path.resolve(__dirname, "schema.graphql") 11 | }); 12 | 13 | const port = parseInt(process.env.PORT, 10) || 4000; 14 | 15 | if (process.env.APOLLO_ENGINE_KEY) { 16 | const engine = new ApolloEngine({ 17 | apiKey: process.env.APOLLO_ENGINE_KEY 18 | }); 19 | 20 | const httpServer = graphQLServer.createHttpServer({ 21 | cacheControl: true, 22 | tracing: true 23 | }); 24 | 25 | engine.listen( 26 | { 27 | graphqlPaths: ["/"], 28 | httpServer, 29 | port 30 | }, 31 | () => 32 | // tslint:disable-next-line:no-console 33 | console.log(`Server with Apollo Engine is running on localhost:${port}`) 34 | ); 35 | } else { 36 | graphQLServer.start({ port }, () => 37 | // tslint:disable-next-line:no-console 38 | console.log(`Server is running on localhost:${port}`) 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/phone.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "crypto"; 2 | import { 3 | format as formatPhoneNumber, 4 | parse as parsePhoneNumber 5 | } from "libphonenumber-js"; 6 | import * as moment from "moment"; 7 | import * as fetch from "node-fetch"; 8 | import * as qs from "qs"; 9 | import { DEFAULT_REDIS_CLIENT } from "./redis"; 10 | import { generateToken, verifyToken } from "./tokens"; 11 | 12 | const TWILIO_ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID; 13 | const TWILIO_API_KEY = process.env.TWILIO_API_KEY; 14 | const SEND_MESSAGE_URL = `https://api.twilio.com/2010-04-01/Accounts/${TWILIO_ACCOUNT_SID}/Messages.json`; 15 | const TWILIO_AUTHORIZATION = 16 | "Basic " + 17 | Buffer.from(`${TWILIO_ACCOUNT_SID}:${TWILIO_API_KEY}`).toString("base64"); 18 | const TWILIO_FROM_NUMBER = process.env.TWILIO_FROM_NUMBER; 19 | const CODE_LENGTH = 6; 20 | const CODE_TIMEOUT = 60 * 30; // 30 mins in seconds 21 | const MAX_ATTEMPTS = 5; 22 | const REQUEST_TIMEOUT = 5 * 1000; 23 | const DEFAULT_COUNTRY_CODE = "US"; 24 | 25 | const GLOBAL_PHONE_NUMBER_SALT = process.env.GLOBAL_PHONE_NUMBER_SALT || ""; 26 | const TRUNCATE_BYTES = 16; 27 | 28 | export function normalizePhoneNumber(phoneNumber: string): string { 29 | const parsed = parsePhoneNumber(phoneNumber, DEFAULT_COUNTRY_CODE); 30 | return parsed.country ? formatPhoneNumber(parsed, "E.164") : null; 31 | } 32 | 33 | export function generateRandomVerificationCode( 34 | length: number = CODE_LENGTH 35 | ): string { 36 | return Math.round(Math.random() * Math.pow(10, length)) 37 | .toString() 38 | .padStart(CODE_LENGTH, "0"); 39 | } 40 | 41 | export function generateTextMessage(verificationCode: string): string { 42 | return `Your Cleargraph verification code is: ${verificationCode}`; 43 | } 44 | 45 | export async function startPhoneNumberVerification( 46 | rawPhoneNumber: string 47 | ): Promise { 48 | const phoneNumber = normalizePhoneNumber(rawPhoneNumber); 49 | const redisKey = `phoneVerificationCodes:${phoneNumber}`; 50 | const verificationCode = generateRandomVerificationCode(); 51 | await DEFAULT_REDIS_CLIENT.hmsetAsync(redisKey, { 52 | attemptsLeft: MAX_ATTEMPTS, 53 | verificationCode 54 | }); 55 | 56 | await DEFAULT_REDIS_CLIENT.expireAsync(redisKey, CODE_TIMEOUT); 57 | const body = generateTextMessage(verificationCode); 58 | 59 | const res = await fetch(SEND_MESSAGE_URL, { 60 | body: qs.stringify({ 61 | Body: body, 62 | From: TWILIO_FROM_NUMBER, 63 | To: phoneNumber 64 | }), 65 | headers: { 66 | Authorization: TWILIO_AUTHORIZATION, 67 | "Content-Type": "application/x-www-form-urlencoded" 68 | }, 69 | method: "POST", 70 | timeout: REQUEST_TIMEOUT 71 | }); 72 | 73 | const resBody = await res.json(); 74 | if (resBody.error_message) { 75 | throw new Error(resBody.error_message); 76 | } 77 | 78 | return; 79 | } 80 | 81 | export async function checkPhoneNumberVerificationCode( 82 | rawPhoneNumber: string, 83 | verificationCode: string 84 | ): Promise { 85 | const phoneNumber = normalizePhoneNumber(rawPhoneNumber); 86 | const redisKey = `phoneVerificationCodes:${phoneNumber}`; 87 | const data = await DEFAULT_REDIS_CLIENT.hgetallAsync(redisKey); 88 | if (!data) { 89 | throw new Error("verification code expired"); 90 | } 91 | 92 | const attemptsLeft = parseInt(data.attemptsLeft, 10); 93 | 94 | if (attemptsLeft === 0) { 95 | throw new Error("max attempts exceeded"); 96 | } 97 | 98 | if (verificationCode !== data.verificationCode) { 99 | await DEFAULT_REDIS_CLIENT.hincrbyAsync(redisKey, "attemptsLeft", -1); 100 | throw new Error("incorrect verification code"); 101 | } 102 | 103 | await DEFAULT_REDIS_CLIENT.delAsync([redisKey]); 104 | 105 | return; 106 | } 107 | 108 | export function generatePhoneNumberHash(phoneNumber: string): string { 109 | const hash = crypto.createHash("sha256"); 110 | hash.update(normalizePhoneNumber(phoneNumber) + GLOBAL_PHONE_NUMBER_SALT); 111 | return hash.digest("hex").substring(0, TRUNCATE_BYTES); 112 | } 113 | 114 | export async function generatePhoneNumberToken( 115 | phoneNumber: string 116 | ): Promise<{ 117 | hashedPhoneNumber: string; 118 | phoneNumberToken: string; 119 | phoneNumberTokenExpires: Date; 120 | }> { 121 | const hashedPhoneNumber = generatePhoneNumberHash(phoneNumber); 122 | 123 | const phoneNumberTokenExpires = moment() 124 | .add(1, "day") 125 | .toDate(); 126 | 127 | const phoneNumberToken = await generateToken( 128 | { hashedPhoneNumber }, 129 | phoneNumberTokenExpires 130 | ); 131 | 132 | return { 133 | hashedPhoneNumber, 134 | phoneNumberToken, 135 | phoneNumberTokenExpires 136 | }; 137 | } 138 | 139 | export async function validatePhoneNumberToken( 140 | phoneNumberToken: string 141 | ): Promise { 142 | const { hashedPhoneNumber } = await verifyToken(phoneNumberToken); 143 | 144 | if (!hashedPhoneNumber) { 145 | throw new Error("invalid phone number token"); 146 | } 147 | 148 | return hashedPhoneNumber; 149 | } 150 | -------------------------------------------------------------------------------- /src/redis.ts: -------------------------------------------------------------------------------- 1 | import * as bluebird from "bluebird"; 2 | import * as redis from "redis"; 3 | 4 | bluebird.promisifyAll(redis.RedisClient.prototype); 5 | bluebird.promisifyAll(redis.Multi.prototype); 6 | 7 | export const REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379/0"; 8 | export const DEFAULT_REDIS_CLIENT = redis.createClient(REDIS_URL); 9 | -------------------------------------------------------------------------------- /src/resolvers/BigNumber.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber as BigNumberValue } from "bignumber.js"; 2 | import { GraphQLScalarType } from "graphql"; 3 | import { GraphQLError } from "graphql/error"; 4 | import { Kind } from "graphql/language"; 5 | 6 | export function coerceBigNumber(value: string | number): BigNumberValue { 7 | return new BigNumberValue(value); 8 | } 9 | 10 | export const BigNumber = new GraphQLScalarType({ 11 | description: "An arbitrary precision value representing a number", 12 | name: "BigNumber", 13 | parseValue: coerceBigNumber, 14 | serialize(val) { 15 | return coerceBigNumber(val).toString(); 16 | }, 17 | parseLiteral(ast) { 18 | switch (ast.kind) { 19 | case Kind.STRING: 20 | case Kind.INT: 21 | case Kind.FLOAT: 22 | return coerceBigNumber(ast.value); 23 | 24 | default: 25 | throw new GraphQLError("invalid number value"); 26 | } 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /src/resolvers/EthereumAddress.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "../context"; 2 | import { web3, Web3Address } from "../web3/client"; 3 | import { fetchTransactions } from "../web3/etherscan"; 4 | 5 | export const EthereumAddress = { 6 | display(parent: Web3Address) { 7 | return parent.display || parent.address; 8 | }, 9 | hex(parent: Web3Address) { 10 | return parent.address; 11 | }, 12 | balance(parent: Web3Address, args, ctx: Context) { 13 | return ctx.loaders.web3.balance.load(parent); 14 | }, 15 | transactionCount(parent: Web3Address, args, ctx: Context) { 16 | return ctx.loaders.web3.transactionCount.load(parent); 17 | }, 18 | async transactions(parent: Web3Address, args, ctx: Context) { 19 | const txs = await fetchTransactions({ 20 | address: parent.address, 21 | network: parent.network, 22 | ...args 23 | }); 24 | 25 | return ctx.loaders.web3.transaction.loadMany( 26 | txs.map(hash => ({ 27 | hash, 28 | network: parent.network 29 | })) 30 | ); 31 | }, 32 | contract(parent: Web3Address, args, ctx: Context) { 33 | return ctx.loaders.web3.contract.load({ 34 | address: parent.address, 35 | interface: args.interface, 36 | network: parent.network 37 | }); 38 | }, 39 | tokenContract(parent, args, ctx: Context) { 40 | const iface = args.interface || {}; 41 | iface.standards = iface.standards || []; 42 | iface.standards.push("ERC_20"); 43 | iface.standards.push("ERC_721"); 44 | 45 | return ctx.loaders.web3.contract.load({ 46 | address: parent.address, 47 | interface: iface, 48 | network: parent.network || "MAINNET" 49 | }); 50 | }, 51 | identityContract(parent, args, ctx: Context) { 52 | const iface = args.interface || {}; 53 | iface.standards = iface.standards || []; 54 | iface.standards.push("ERC_725"); 55 | 56 | return ctx.loaders.web3.contract.load({ 57 | address: parent.address, 58 | interface: iface, 59 | network: parent.network || "MAINNET" 60 | }); 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /src/resolvers/EthereumAddressString.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType } from "graphql"; 2 | import { GraphQLError } from "graphql/error"; 3 | import { Kind } from "graphql/language"; 4 | 5 | export const HEX_REGEX = /^(0x)?([a-f0-9]+)$/; 6 | 7 | export function coerceEthereumAddressString(str: string): string | null { 8 | const norm = str.toLowerCase(); 9 | if (norm.match(HEX_REGEX)) { 10 | return norm.startsWith("0x") ? norm : "0x" + norm; 11 | } else if (norm.endsWith(".eth")) { 12 | return norm; 13 | } else { 14 | throw new GraphQLError("invalid Ethereum address"); 15 | } 16 | } 17 | 18 | export const EthereumAddressString = new GraphQLScalarType({ 19 | description: "A string representing an Ethereum address", 20 | name: "EthereumAddressString", 21 | parseValue: coerceEthereumAddressString, 22 | serialize: coerceEthereumAddressString, 23 | parseLiteral(ast) { 24 | switch (ast.kind) { 25 | case Kind.STRING: 26 | return coerceEthereumAddressString(ast.value); 27 | 28 | default: 29 | throw new GraphQLError("invalid Ethereum address"); 30 | } 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /src/resolvers/EthereumBlock.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "../context"; 2 | import { Web3Block } from "../web3/client"; 3 | 4 | export const EthereumBlock = { 5 | parent(parent: Web3Block, args, ctx: Context) { 6 | return ctx.loaders.web3.block.load({ 7 | hash: parent.parentHash, 8 | network: parent.network 9 | }); 10 | }, 11 | miner(parent: Web3Block, args, ctx: Context) { 12 | return ctx.loaders.web3.address.load({ 13 | address: parent.miner, 14 | network: parent.network 15 | }); 16 | }, 17 | transactions(parent: Web3Block, args, ctx: Context) { 18 | return ctx.loaders.web3.transaction.loadMany( 19 | parent.transactions.map(hash => ({ 20 | hash, 21 | network: parent.network 22 | })) 23 | ); 24 | }, 25 | transactionCount(parent: Web3Block, args, ctx: Context) { 26 | return ctx.loaders.web3.blockTransactionCount.load({ 27 | hash: parent.parentHash, 28 | network: parent.network 29 | }); 30 | }, 31 | uncles(parent: Web3Block, args, ctx: Context) { 32 | return ctx.loaders.web3.block.loadMany( 33 | parent.uncles.map(hash => ({ 34 | hash, 35 | network: parent.network 36 | })) 37 | ); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/resolvers/EthereumContractMethod.ts: -------------------------------------------------------------------------------- 1 | import { callMethodSafe } from "../web3/contracts"; 2 | 3 | export const EthereumContractMethod = { 4 | call(parent, args) { 5 | return callMethodSafe( 6 | parent.contract, 7 | parent.methodSignature, 8 | args.inputs || [] 9 | ); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/resolvers/EthereumGenericContract.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "../context"; 2 | 3 | export const EthereumGenericContract = { 4 | address(parent, args, ctx: Context) { 5 | return ctx.loaders.web3.address.load({ 6 | address: parent._address, 7 | network: parent.network 8 | }); 9 | }, 10 | method(parent, args: { signature: string }) { 11 | if (!(args.signature in parent.methods)) { 12 | return null; 13 | } 14 | 15 | return { contract: parent, methodSignature: args.signature }; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/resolvers/EthereumIdentityContract.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from "bignumber.js"; 2 | import { Context } from "../context"; 3 | import { callMethodSafe } from "../web3/contracts"; 4 | 5 | export const EthereumIdentityContract = { 6 | address(parent, args, ctx: Context) { 7 | return ctx.loaders.web3.address.load({ 8 | address: parent._address, 9 | network: parent.network 10 | }); 11 | }, 12 | method(parent, args: { signature: string }) { 13 | if (!(args.signature in parent.methods)) { 14 | return null; 15 | } 16 | 17 | return { contract: parent, methodSignature: args.signature }; 18 | }, 19 | async key(parent, args: { key: string }) { 20 | const result = await callMethodSafe(parent, "getKey", [args.key]); 21 | if (!result) { 22 | return null; 23 | } 24 | 25 | return { key: result[2], keyType: result[1], purposes: result[0] }; 26 | }, 27 | async keyByPurpose(parent, args: { purpose: string }) { 28 | const keys = await callMethodSafe(parent, "keysByPurpose", [args.purpose]); 29 | 30 | if (!keys) { 31 | return null; 32 | } 33 | 34 | return Promise.all( 35 | keys[0].map(async k => { 36 | const result = await callMethodSafe(parent, "getKey", [k]); 37 | return { key: result[2], keyType: result[1], purposes: result[0] }; 38 | }) 39 | ); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/resolvers/EthereumLog.ts: -------------------------------------------------------------------------------- 1 | import { EthereumNetwork, web3, Web3Log } from "../web3/client"; 2 | 3 | export const EthereumLog = { 4 | address(parent: Web3Log) { 5 | return { hash: parent.address, network: parent.network }; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/resolvers/EthereumTokenContract.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from "bignumber.js"; 2 | import { Context } from "../context"; 3 | import { callMethodSafe } from "../web3/contracts"; 4 | 5 | export const EthereumTokenContract = { 6 | address(parent, args, ctx: Context) { 7 | return ctx.loaders.web3.address.load({ 8 | address: parent._address, 9 | network: parent.network 10 | }); 11 | }, 12 | method(parent, args: { signature: string }) { 13 | if (!(args.signature in parent.methods)) { 14 | return null; 15 | } 16 | 17 | return { contract: parent, methodSignature: args.signature }; 18 | }, 19 | name(parent) { 20 | return callMethodSafe(parent, "name"); 21 | }, 22 | symbol(parent) { 23 | return callMethodSafe(parent, "symbol"); 24 | }, 25 | decimals(parent) { 26 | return callMethodSafe(parent, "decimals"); 27 | }, 28 | totalSupply(parent) { 29 | return callMethodSafe(parent, "totalSupply"); 30 | }, 31 | async owner(parent, args: { tokenId: string }, ctx: Context) { 32 | const ownerAddress = await callMethodSafe(parent, "ownerOf", [ 33 | args.tokenId 34 | ]); 35 | 36 | if (!ownerAddress) { 37 | return null; 38 | } 39 | 40 | return ctx.loaders.web3.address.load({ 41 | address: ownerAddress, 42 | network: parent.network 43 | }); 44 | }, 45 | async balance(parent, args: { owner: string }) { 46 | const rawBalance = await callMethodSafe(parent, "balanceOf", [args.owner]); 47 | const decimals = await callMethodSafe(parent, "decimals"); 48 | 49 | if (!decimals) { 50 | return rawBalance; 51 | } 52 | 53 | return new BigNumber(rawBalance).dividedBy( 54 | new BigNumber(10).exponentiatedBy(decimals) 55 | ); 56 | }, 57 | rawBalance(parent, args: { owner: string }) { 58 | return callMethodSafe(parent, "balanceOf", [args.owner]); 59 | }, 60 | allowance(parent, args: { owner: string; spender: string }) { 61 | return callMethodSafe(parent, "allowance", [args.owner, args.spender]); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/resolvers/EthereumTransaction.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "../context"; 2 | import { Web3Transaction } from "../web3/client"; 3 | 4 | export const EthereumTransaction = { 5 | block(parent: Web3Transaction, args, ctx: Context, info) { 6 | return ctx.loaders.web3.block.load({ 7 | hash: parent.blockHash, 8 | network: parent.network 9 | }); 10 | }, 11 | from(parent: Web3Transaction, args, ctx: Context, info) { 12 | if (!parent.from) { 13 | return null; 14 | } 15 | 16 | return ctx.loaders.web3.address.load({ 17 | address: parent.from, 18 | network: parent.network 19 | }); 20 | }, 21 | to(parent: Web3Transaction, args, ctx: Context, info) { 22 | if (!parent.to) { 23 | return null; 24 | } 25 | 26 | return ctx.loaders.web3.address.load({ 27 | address: parent.to, 28 | network: parent.network 29 | }); 30 | }, 31 | async gasUsed(parent: Web3Transaction, args, ctx: Context, info) { 32 | const receipt = await ctx.loaders.web3.transactionReceipt.load({ 33 | hash: parent.hash, 34 | network: parent.network 35 | }); 36 | 37 | return receipt ? receipt.gasUsed : null; 38 | }, 39 | async cumulativeGasUsed(parent: Web3Transaction, args, ctx: Context, info) { 40 | const receipt = await ctx.loaders.web3.transactionReceipt.load({ 41 | hash: parent.hash, 42 | network: parent.network 43 | }); 44 | 45 | return receipt ? receipt.cumulativeGasUsed : null; 46 | }, 47 | async contractAddress(parent: Web3Transaction, args, ctx: Context, info) { 48 | const receipt = await ctx.loaders.web3.transactionReceipt.load({ 49 | hash: parent.hash, 50 | network: parent.network 51 | }); 52 | 53 | return receipt && receipt.contractAddress 54 | ? ctx.loaders.web3.address.load({ 55 | address: receipt.contractAddress, 56 | network: parent.network 57 | }) 58 | : null; 59 | }, 60 | async status(parent: Web3Transaction, args, ctx: Context, info) { 61 | const receipt = await ctx.loaders.web3.transactionReceipt.load({ 62 | hash: parent.hash, 63 | network: parent.network 64 | }); 65 | 66 | return receipt ? receipt.status : null; 67 | }, 68 | async logs(parent: Web3Transaction, args, ctx: Context) { 69 | const receipt = await ctx.loaders.web3.transactionReceipt.load({ 70 | hash: parent.hash, 71 | network: parent.network 72 | }); 73 | 74 | return receipt ? receipt.logs : []; 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/resolvers/EthereumValue.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from "bignumber.js"; 2 | import { EthereumNetwork, web3 } from "../web3/client"; 3 | 4 | const fromWei = web3.MAINNET.utils.fromWei; 5 | 6 | // don't repeat yourse... ah screw it. 7 | export const EthereumValue = { 8 | display(v, args: { precision: number }) { 9 | const precision = args.precision || 3; 10 | const n = new BigNumber(v); 11 | if (n.isGreaterThan("50000000000000000")) { 12 | return n.dividedBy("1000000000000000000").toFixed(precision) + " ETH"; 13 | } else if (n.isGreaterThan("50000000000000")) { 14 | return n.dividedBy("1000000000000000").toFixed(precision) + " mETH"; 15 | } else if (n.isGreaterThan("50000000000")) { 16 | return n.dividedBy("1000000000000").toFixed(precision) + " nETH"; 17 | } else if (n.isGreaterThan("50000000")) { 18 | return n.dividedBy("1000000000").toFixed(precision) + " Gwei"; 19 | } else if (n.isGreaterThan("50000")) { 20 | return n.dividedBy("1000000").toFixed(precision) + " Kwei"; 21 | } else { 22 | return n.toString() + " wei"; 23 | } 24 | }, 25 | Gwei: v => fromWei(v, "Gwei"), 26 | Kwei: v => fromWei(v, "Kwei"), 27 | Mwei: v => fromWei(v, "Mwei"), 28 | babbage: v => fromWei(v, "babbage"), 29 | ether: v => fromWei(v, "ether"), 30 | femtoether: v => fromWei(v, "femtoether"), 31 | finney: v => fromWei(v, "finney"), 32 | gether: v => fromWei(v, "gether"), 33 | grand: v => fromWei(v, "grand"), 34 | gwei: v => fromWei(v, "gwei"), 35 | kether: v => fromWei(v, "kether"), 36 | kwei: v => fromWei(v, "kwei"), 37 | lovelace: v => fromWei(v, "lovelace"), 38 | mether: v => fromWei(v, "mether"), 39 | micro: v => fromWei(v, "micro"), 40 | microether: v => fromWei(v, "microether"), 41 | milli: v => fromWei(v, "milli"), 42 | milliether: v => fromWei(v, "milliether"), 43 | mwei: v => fromWei(v, "mwei"), 44 | nano: v => fromWei(v, "nano"), 45 | nanoether: v => fromWei(v, "nanoether"), 46 | picoether: v => fromWei(v, "picoether"), 47 | shannon: v => fromWei(v, "shannon"), 48 | szabo: v => fromWei(v, "szabo"), 49 | tether: v => fromWei(v, "tether"), 50 | wei: v => fromWei(v, "wei") 51 | }; 52 | -------------------------------------------------------------------------------- /src/resolvers/HexValue.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType } from "graphql"; 2 | import { GraphQLError } from "graphql/error"; 3 | import { Kind } from "graphql/language"; 4 | 5 | export const HEX_REGEX = /^(0x)?([a-f0-9]+)$/; 6 | 7 | export function hexValue(str: string, len: number | null): string | null { 8 | const norm = str.toLowerCase(); 9 | if (!norm.match(HEX_REGEX)) { 10 | throw new GraphQLError("invalid hex value"); 11 | } 12 | 13 | return norm.startsWith("0x") ? norm : "0x" + norm; 14 | } 15 | 16 | export function createHexValueGraphQLScalarType({ 17 | name, 18 | description, 19 | byteLength 20 | }: { 21 | name: string; 22 | description: string; 23 | byteLength: number; 24 | }): GraphQLScalarType { 25 | const coerceHexValue = (str: string): string | null => { 26 | const norm = str.toLowerCase(); 27 | const match = norm.match(HEX_REGEX); 28 | if (!match) { 29 | throw new GraphQLError("invalid hex value"); 30 | } 31 | 32 | const actualByteLength = match[2].length / 2; 33 | if (byteLength && actualByteLength !== byteLength) { 34 | throw new GraphQLError( 35 | `invalid hex value length (expected ${byteLength})` 36 | ); 37 | } 38 | 39 | return norm.startsWith("0x") ? norm : "0x" + norm; 40 | }; 41 | 42 | return new GraphQLScalarType({ 43 | description, 44 | name, 45 | parseValue: coerceHexValue, 46 | serialize: coerceHexValue, 47 | parseLiteral(ast) { 48 | switch (ast.kind) { 49 | case Kind.STRING: 50 | return coerceHexValue(ast.value); 51 | 52 | default: 53 | throw new GraphQLError("invalid hex value"); 54 | } 55 | } 56 | }); 57 | } 58 | 59 | export const HexValue = createHexValueGraphQLScalarType({ 60 | byteLength: 0, 61 | description: "A value encoded as a hexadecimal string", 62 | name: "HexValue" 63 | }); 64 | 65 | export const EthereumTransactionHashHexValue = createHexValueGraphQLScalarType({ 66 | byteLength: 32, 67 | description: 68 | "An Ethereum transaction hash encoded as a hexadecimal string (32 bytes)", 69 | name: "EthereumTransactionHashHexValue" 70 | }); 71 | 72 | export const EthereumBlockHashHexValue = createHexValueGraphQLScalarType({ 73 | byteLength: 32, 74 | description: 75 | "An Ethereum block hash encoded as a hexadecimal string (32 bytes)", 76 | name: "EthereumBlockHashHexValue" 77 | }); 78 | -------------------------------------------------------------------------------- /src/resolvers/Mutation.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "../context"; 2 | import { 3 | checkPhoneNumberVerificationCode, 4 | generatePhoneNumberHash, 5 | generatePhoneNumberToken, 6 | startPhoneNumberVerification, 7 | validatePhoneNumberToken 8 | } from "../phone"; 9 | import { normalizeUsername } from "../usernames"; 10 | import { web3 } from "../web3/client"; 11 | 12 | export const Mutation = { 13 | async startPhoneNumberVerification( 14 | parent, 15 | { input }: { input: { phoneNumber: string } }, 16 | ctx: Context 17 | ) { 18 | try { 19 | await startPhoneNumberVerification(input.phoneNumber); 20 | return { ok: true }; 21 | } catch (err) { 22 | return { 23 | message: err.message, 24 | ok: false 25 | }; 26 | } 27 | }, 28 | async checkPhoneNumberVerification( 29 | parent, 30 | { 31 | input 32 | }: { 33 | input: { 34 | phoneNumber: string; 35 | verificationCode: string; 36 | }; 37 | }, 38 | ctx: Context, 39 | info 40 | ) { 41 | try { 42 | await checkPhoneNumberVerificationCode( 43 | input.phoneNumber, 44 | input.verificationCode 45 | ); 46 | 47 | const { 48 | hashedPhoneNumber, 49 | phoneNumberToken, 50 | phoneNumberTokenExpires 51 | } = await generatePhoneNumberToken(input.phoneNumber); 52 | 53 | return { 54 | ok: true, 55 | phoneNumber: ctx.db.query.phoneNumber( 56 | { 57 | where: { hashedPhoneNumber } 58 | }, 59 | // a bit of a hack since I'm not sure what to do with info here 60 | `{ 61 | hashedPhoneNumber 62 | address 63 | createdAt 64 | updatedAt 65 | }` 66 | ), 67 | phoneNumberToken, 68 | phoneNumberTokenExpires 69 | }; 70 | } catch (err) { 71 | return { 72 | message: err.message, 73 | ok: false 74 | }; 75 | } 76 | }, 77 | async updatePhoneNumber( 78 | parent, 79 | { 80 | input 81 | }: { 82 | input: { 83 | phoneNumberToken: string; 84 | address: string; 85 | }; 86 | }, 87 | ctx: Context, 88 | info 89 | ) { 90 | try { 91 | const hashedPhoneNumber = await validatePhoneNumberToken( 92 | input.phoneNumberToken 93 | ); 94 | 95 | return { 96 | ok: true, 97 | phoneNumber: ctx.db.mutation.upsertPhoneNumber( 98 | { 99 | create: { hashedPhoneNumber, address: input.address }, 100 | update: { address: input.address }, 101 | where: { hashedPhoneNumber } 102 | }, 103 | // a bit of a hack since I'm not sure what to do with info here 104 | `{ 105 | hashedPhoneNumber 106 | address 107 | createdAt 108 | updatedAt 109 | }` 110 | ) 111 | }; 112 | } catch (err) { 113 | return { message: err.message, ok: false }; 114 | } 115 | }, 116 | async deletePhoneNumber( 117 | parent, 118 | { 119 | input 120 | }: { 121 | input: { 122 | phoneNumberToken: string; 123 | }; 124 | }, 125 | ctx: Context 126 | ) { 127 | try { 128 | const hashedPhoneNumber = await validatePhoneNumberToken( 129 | input.phoneNumberToken 130 | ); 131 | 132 | await ctx.db.mutation.deletePhoneNumber({ 133 | where: { hashedPhoneNumber } 134 | }); 135 | 136 | return { ok: true }; 137 | } catch (err) { 138 | return { message: err.message, ok: false }; 139 | } 140 | }, 141 | async sendRawEthereumTransaction(parent, { input }, ctx) { 142 | const hash = await web3[input.network].eth.sendRawTransaction(input); 143 | return { 144 | transaction: ctx.loaders.web3.transaction.load({ 145 | hash, 146 | network: input.network 147 | }) 148 | }; 149 | }, 150 | async checkUsernameAvailable(parent, { input }, ctx) { 151 | try { 152 | const username = await normalizeUsername(input.username); 153 | const address = await ctx.loaders.web3.address.load({ 154 | address: username, 155 | network: input.network || "MAINNET" 156 | }); 157 | 158 | if (address) { 159 | return { message: "username is taken", ok: false }; 160 | } 161 | 162 | return { ok: true }; 163 | } catch (err) { 164 | return { message: err.message, ok: false }; 165 | } 166 | }, 167 | async createIdentityContract(parent, { input }, ctx) { 168 | try { 169 | const username = await normalizeUsername(input.username); 170 | const address = await ctx.loaders.web3.address.load({ 171 | address: username, 172 | network: input.network || "MAINNET" 173 | }); 174 | 175 | if (address) { 176 | return { message: "username is taken", ok: false }; 177 | } 178 | 179 | const hashedPhoneNumber = await validatePhoneNumberToken( 180 | input.phoneNumberToken 181 | ); 182 | 183 | if (input.managerAddresses.length === 0) { 184 | return { message: "need at least one manager address", ok: false }; 185 | } 186 | 187 | // TODO: Create the identity contract and return its address. 188 | 189 | return { ok: true }; 190 | } catch (err) { 191 | return { message: err.message, ok: false }; 192 | } 193 | } 194 | }; 195 | -------------------------------------------------------------------------------- /src/resolvers/PhoneNumber.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "../context"; 2 | 3 | export const PhoneNumber = { 4 | ethereumAddress(parent, { network = "MAINNET" }, ctx: Context) { 5 | return ctx.loaders.web3.address.load({ hash: parent.address, network }); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/resolvers/Query.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from "bignumber.js"; 2 | import { Context } from "../context"; 3 | import { web3 } from "../web3/client"; 4 | 5 | export const Query = { 6 | phoneNumber(parent, { hashedPhoneNumber }, ctx: Context, info) { 7 | return ctx.db.query.phoneNumber({ 8 | where: { hashedPhoneNumber: hashedPhoneNumber.toLowerCase() } 9 | }); 10 | }, 11 | 12 | ethereumValue(parent, args: { value: string; unit: string }) { 13 | return web3.MAINNET.utils.toWei(args.value.toString(), args.unit || "wei"); 14 | }, 15 | 16 | ethereumGasPrice(parent, { network = "MAINNET" }) { 17 | return web3[network].eth.getGasPrice(); 18 | }, 19 | 20 | ethereumBlockNumber(parent, { network = "MAINNET" }) { 21 | return web3[network].eth.getBlockNumber(); 22 | }, 23 | 24 | ethereumAddress(parent, { address, network = "MAINNET" }, ctx: Context) { 25 | return ctx.loaders.web3.address.load({ address, network }); 26 | }, 27 | 28 | // tslint:disable-next-line:variable-name 29 | ethereumBlock(parent, { hash, number, network = "MAINNET" }, ctx: Context) { 30 | return ctx.loaders.web3.block.load({ hash, number, network }); 31 | }, 32 | 33 | ethereumTransaction(parent, { hash, network = "MAINNET" }, ctx: Context) { 34 | return ctx.loaders.web3.transaction.load({ hash, network }); 35 | }, 36 | 37 | ethereumContract(parent, args, ctx: Context) { 38 | return ctx.loaders.web3.contract.load({ 39 | address: args.address, 40 | interface: args.interface, 41 | network: args.network || "MAINNET" 42 | }); 43 | }, 44 | 45 | ethereumTokenContract(parent, args, ctx: Context) { 46 | const iface = args.interface || {}; 47 | iface.standards = iface.standards || []; 48 | iface.standards.push("ERC_20"); 49 | iface.standards.push("ERC_721"); 50 | 51 | return ctx.loaders.web3.contract.load({ 52 | address: args.address, 53 | interface: iface, 54 | network: args.network || "MAINNET" 55 | }); 56 | }, 57 | 58 | ethereumIdentityContract(parent, args, ctx: Context) { 59 | const iface = args.interface || {}; 60 | iface.standards = iface.standards || []; 61 | iface.standards.push("ERC_725"); 62 | 63 | return ctx.loaders.web3.contract.load({ 64 | address: args.address, 65 | interface: iface, 66 | network: args.network || "MAINNET" 67 | }); 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /src/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | import * as JSON from "graphql-type-json"; 2 | 3 | export { BigNumber } from "./BigNumber"; 4 | export { EthereumAddress } from "./EthereumAddress"; 5 | export { EthereumAddressString } from "./EthereumAddressString"; 6 | export { EthereumBlock } from "./EthereumBlock"; 7 | export { EthereumContractMethod } from "./EthereumContractMethod"; 8 | export { EthereumGenericContract } from "./EthereumGenericContract"; 9 | export { EthereumIdentityContract } from "./EthereumIdentityContract"; 10 | export { EthereumLog } from "./EthereumLog"; 11 | export { EthereumTokenContract } from "./EthereumTokenContract"; 12 | export { EthereumTransaction } from "./EthereumTransaction"; 13 | export { EthereumValue } from "./EthereumValue"; 14 | export { JSON }; 15 | export { Mutation } from "./Mutation"; 16 | export { PhoneNumber } from "./PhoneNumber"; 17 | export { Query } from "./Query"; 18 | export { 19 | HexValue, 20 | EthereumBlockHashHexValue, 21 | EthereumTransactionHashHexValue 22 | } from "./HexValue"; 23 | -------------------------------------------------------------------------------- /src/schema.graphql: -------------------------------------------------------------------------------- 1 | # import PhoneNumber from "./generated/prisma.graphql" 2 | 3 | scalar JSON 4 | scalar BigNumber 5 | scalar HexValue 6 | scalar EthereumAddressString 7 | scalar EthereumBlockHashHexValue 8 | scalar EthereumTransactionHashHexValue 9 | 10 | interface Ack { 11 | ok: Boolean 12 | message: String 13 | } 14 | 15 | enum ETHEREUM_NETWORK { 16 | MAINNET 17 | ROPSTEN 18 | KOVAN 19 | RINKEBY 20 | } 21 | 22 | enum ETHEREUM_UNIT { 23 | BABBAGE 24 | ETHER 25 | FEMTOETHER 26 | FINNEY 27 | GETHER 28 | GRAND 29 | GWEI 30 | KETHER 31 | KWEI 32 | LOVELACE 33 | METHER 34 | MICRO 35 | MICROETHER 36 | MILLI 37 | MILLIETHER 38 | NANO 39 | NANOETHER 40 | PICOETHER 41 | SHANNON 42 | SZABO 43 | TETHER 44 | WEI 45 | } 46 | 47 | enum ETHEREUM_CONTRACT_STANDARD { 48 | ERC_20 49 | ERC_721 50 | ERC_725 51 | } 52 | 53 | type Query { 54 | phoneNumber(hashedPhoneNumber: String!): PhoneNumber 55 | 56 | ethereumValue(value: BigNumber!, unit: ETHEREUM_UNIT): EthereumValue 57 | ethereumGasPrice(network: ETHEREUM_NETWORK): EthereumValue 58 | 59 | ethereumBlockNumber(network: ETHEREUM_NETWORK): Int 60 | 61 | ethereumBlock( 62 | hash: EthereumBlockHashHexValue 63 | number: Int 64 | network: ETHEREUM_NETWORK 65 | ): EthereumBlock 66 | 67 | ethereumTransaction( 68 | hash: EthereumTransactionHashHexValue! 69 | network: ETHEREUM_NETWORK 70 | ): EthereumTransaction 71 | 72 | ethereumAddress( 73 | address: EthereumAddressString! 74 | network: ETHEREUM_NETWORK 75 | ): EthereumAddress 76 | 77 | ethereumContract( 78 | address: EthereumAddressString! 79 | network: ETHEREUM_NETWORK 80 | interface: EthereumContractInterfaceInput! 81 | ): EthereumGenericContract 82 | 83 | ethereumTokenContract( 84 | address: EthereumAddressString! 85 | network: ETHEREUM_NETWORK 86 | interface: EthereumContractInterfaceInput 87 | ): EthereumTokenContract 88 | 89 | ethereumIdentityContract( 90 | address: EthereumAddressString! 91 | network: ETHEREUM_NETWORK 92 | interface: EthereumContractInterfaceInput 93 | ): EthereumIdentityContract 94 | } 95 | 96 | type Mutation { 97 | sendRawEthereumTransaction( 98 | input: SendRawEthereumTransactionInput! 99 | ): SendRawEthereumTransactionPayload 100 | 101 | startPhoneNumberVerification( 102 | input: StartPhoneNumberVerificationInput! 103 | ): StartPhoneNumberVerificationPayload 104 | 105 | checkPhoneNumberVerification( 106 | input: CheckPhoneNumberVerificationInput! 107 | ): CheckPhoneNumberVerificationPayload 108 | 109 | updatePhoneNumber(input: UpdatePhoneNumberInput!): UpdatePhoneNumberPayload 110 | deletePhoneNumber(input: DeletePhoneNumberInput!): DeletePhoneNumberPayload 111 | 112 | checkUsernameAvailable( 113 | input: CheckUsernameAvailableInput! 114 | ): CheckUsernameAvailablePayload 115 | 116 | createIdentityContract( 117 | input: CreateIdentityContractInput! 118 | ): CreateIdentityContractPayload 119 | } 120 | 121 | type EthereumAddress { 122 | network: ETHEREUM_NETWORK! 123 | display: String! 124 | hex: HexValue! 125 | 126 | balance: EthereumValue! 127 | 128 | transactionCount: Int! 129 | transactions( 130 | startBlock: Int 131 | endBlock: Int 132 | page: Int 133 | offset: Int 134 | ): [EthereumTransaction!] 135 | 136 | contract(interface: EthereumContractInterfaceInput!): EthereumGenericContract 137 | 138 | tokenContract( 139 | interface: EthereumContractInterfaceInput 140 | ): EthereumTokenContract 141 | 142 | identityContract( 143 | interface: EthereumContractInterfaceInput 144 | ): EthereumIdentityContract 145 | } 146 | 147 | type EthereumTransaction { 148 | network: ETHEREUM_NETWORK! 149 | hash: EthereumTransactionHashHexValue! 150 | nonce: Int 151 | block: EthereumBlock 152 | transactionIndex: Int 153 | from: EthereumAddress 154 | to: EthereumAddress 155 | value: EthereumValue 156 | gas: Int 157 | gasPrice: EthereumValue 158 | input: HexValue 159 | 160 | gasUsed: Int 161 | cumulativeGasUsed: Int 162 | contractAddress: EthereumAddress 163 | status: Boolean 164 | 165 | logs: [EthereumLog!]! 166 | } 167 | 168 | type EthereumBlock { 169 | network: ETHEREUM_NETWORK! 170 | hash: EthereumBlockHashHexValue! 171 | number: Int! 172 | parent: EthereumBlock 173 | nonce: HexValue 174 | sha3Uncles: String 175 | logsBloom: HexValue 176 | transactionsRoot: String 177 | stateRoot: String 178 | miner: EthereumAddress! 179 | difficulty: BigNumber 180 | totalDifficulty: BigNumber 181 | size: Int 182 | extraData: HexValue 183 | gasLimit: Int 184 | gasUsed: Int 185 | timestamp: Int 186 | transactions: [EthereumTransaction!]! 187 | transactionCount: Int! 188 | uncles: [EthereumBlock!]! 189 | } 190 | 191 | type EthereumLog { 192 | id: String 193 | address: EthereumAddress 194 | topics: [HexValue!]! 195 | data: HexValue 196 | logIndex: Int 197 | removed: Boolean 198 | } 199 | 200 | interface EthereumContract { 201 | address: EthereumAddress! 202 | method(signature: String!): EthereumContractMethod 203 | } 204 | 205 | type EthereumGenericContract implements EthereumContract { 206 | address: EthereumAddress! 207 | method(signature: String!): EthereumContractMethod 208 | } 209 | 210 | type EthereumTokenContract implements EthereumContract { 211 | address: EthereumAddress! 212 | method(signature: String!): EthereumContractMethod 213 | 214 | name: String 215 | symbol: String 216 | decimals: Int 217 | totalSupply: BigNumber 218 | balance(owner: EthereumAddressString!): BigNumber 219 | rawBalance(owner: EthereumAddressString!): BigNumber 220 | owner(tokenId: BigNumber!): EthereumAddress 221 | allowance( 222 | owner: EthereumAddressString! 223 | spender: EthereumAddressString! 224 | ): BigNumber 225 | } 226 | 227 | type EthereumContractMethod { 228 | call(inputs: [JSON]): JSON 229 | } 230 | 231 | type EthereumIdentityContract implements EthereumContract { 232 | address: EthereumAddress! 233 | method(signature: String!): EthereumContractMethod 234 | 235 | key(key: HexValue!): EthereumIdentityContractKey 236 | keyByPurpose(purpose: BigNumber!): [EthereumIdentityContractKey] 237 | } 238 | 239 | type EthereumIdentityContractKey { 240 | key: HexValue! 241 | keyType: BigNumber! 242 | purposes: [BigNumber!]! 243 | } 244 | 245 | type EthereumValue { 246 | display(precision: Int): String! 247 | 248 | Gwei: BigNumber! 249 | Kwei: BigNumber! 250 | Mwei: BigNumber! 251 | babbage: BigNumber! 252 | ether: BigNumber! 253 | femtoether: BigNumber! 254 | finney: BigNumber! 255 | gether: BigNumber! 256 | grand: BigNumber! 257 | gwei: BigNumber! 258 | kether: BigNumber! 259 | kwei: BigNumber! 260 | lovelace: BigNumber! 261 | mether: BigNumber! 262 | micro: BigNumber! 263 | microether: BigNumber! 264 | milli: BigNumber! 265 | milliether: BigNumber! 266 | mwei: BigNumber! 267 | nano: BigNumber! 268 | nanoether: BigNumber! 269 | picoether: BigNumber! 270 | shannon: BigNumber! 271 | szabo: BigNumber! 272 | tether: BigNumber! 273 | wei: BigNumber! 274 | } 275 | 276 | type PhoneNumber { 277 | hashedPhoneNumber: String! 278 | address: String! 279 | createdAt: DateTime! 280 | updatedAt: DateTime! 281 | 282 | ethereumAddress(network: ETHEREUM_NETWORK): EthereumAddress 283 | } 284 | 285 | input StartPhoneNumberVerificationInput { 286 | phoneNumber: String! 287 | } 288 | 289 | type StartPhoneNumberVerificationPayload implements Ack { 290 | ok: Boolean 291 | message: String 292 | } 293 | 294 | input CheckPhoneNumberVerificationInput { 295 | phoneNumber: String! 296 | verificationCode: String! 297 | } 298 | 299 | type CheckPhoneNumberVerificationPayload implements Ack { 300 | ok: Boolean 301 | message: String 302 | phoneNumber: PhoneNumber 303 | phoneNumberToken: String 304 | phoneNumberTokenExpires: DateTime 305 | } 306 | 307 | input CheckUsernameAvailableInput { 308 | username: String! 309 | network: ETHEREUM_NETWORK 310 | } 311 | 312 | type CheckUsernameAvailablePayload implements Ack { 313 | ok: Boolean 314 | message: String 315 | } 316 | 317 | input CreateIdentityContractInput { 318 | username: String! 319 | phoneNumberToken: String! 320 | managerAddresses: [EthereumAddressString!]! 321 | network: ETHEREUM_NETWORK 322 | passphraseRecoveryHash: String 323 | socialRecoveryAddresses: [EthereumAddressString!] 324 | } 325 | 326 | type CreateIdentityContractPayload implements Ack { 327 | ok: Boolean 328 | message: String 329 | identityContract: EthereumIdentityContract 330 | } 331 | 332 | input UpdatePhoneNumberInput { 333 | phoneNumberToken: String! 334 | address: EthereumAddressString! 335 | } 336 | 337 | type UpdatePhoneNumberPayload implements Ack { 338 | ok: Boolean 339 | message: String 340 | phoneNumber: PhoneNumber 341 | } 342 | 343 | input DeletePhoneNumberInput { 344 | phoneNumberToken: String! 345 | } 346 | 347 | type DeletePhoneNumberPayload implements Ack { 348 | ok: Boolean 349 | message: String 350 | } 351 | 352 | input SendRawEthereumTransactionInput { 353 | data: String! 354 | network: ETHEREUM_NETWORK 355 | } 356 | 357 | type SendRawEthereumTransactionPayload implements Ack { 358 | ok: Boolean 359 | message: String 360 | ethereumTransaction: EthereumTransaction 361 | } 362 | 363 | input EthereumContractInterfaceInput { 364 | standards: [ETHEREUM_CONTRACT_STANDARD!] 365 | inline: [EthereumContractInterfaceInlineInput!] 366 | } 367 | 368 | input EthereumContractInterfaceInlineInput { 369 | name: String! 370 | type: String 371 | inputs: [EthereumContractInterfaceInlineParameterInput!] 372 | outputs: [EthereumContractInterfaceInlineParameterInput!] 373 | payable: Boolean 374 | stateMutability: String 375 | constant: Boolean 376 | } 377 | 378 | input EthereumContractInterfaceInlineParameterInput { 379 | name: String! 380 | type: String! 381 | components: [EthereumContractInterfaceInlineParameterInput!] 382 | indexed: Boolean 383 | } 384 | -------------------------------------------------------------------------------- /src/tokens.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from "jsonwebtoken"; 2 | 3 | export const TOKEN_SECRET = process.env.TOKEN_SECRET || "nicetrynsa"; 4 | export const TOKEN_ALGORITHM = "HS512"; 5 | 6 | export async function generateToken( 7 | payload: object, 8 | expires: Date 9 | ): Promise { 10 | return jwt.sign({ ...payload, exp: expires.getTime() / 1000 }, TOKEN_SECRET, { 11 | algorithm: TOKEN_ALGORITHM 12 | }); 13 | } 14 | 15 | export async function verifyToken(token: string): Promise { 16 | return jwt.verify(token, TOKEN_SECRET, { algorithms: [TOKEN_ALGORITHM] }); 17 | } 18 | -------------------------------------------------------------------------------- /src/usernames.ts: -------------------------------------------------------------------------------- 1 | export const USERNAME_DOMAIN = process.env.USERNAME_DOMAINS || "multiapp.eth"; 2 | export const USERNAME_REGEXP = /^[a-zA-Z0-9]+$/; 3 | export const USERNAME_MIN_LENGTH = 3; 4 | export const USERNAME_MAX_LENGTH = 32; 5 | 6 | export function normalizeUsername(username: string): string { 7 | if (!username.endsWith("." + USERNAME_DOMAIN)) { 8 | throw new Error("unsupported username domain"); 9 | } 10 | 11 | const usernameParts = username.split("."); 12 | if (usernameParts.length !== 3) { 13 | throw new Error("invalid username"); 14 | } 15 | 16 | const n = usernameParts[0]; 17 | if (n.length > USERNAME_MAX_LENGTH) { 18 | throw new Error(`username too long (max ${USERNAME_MAX_LENGTH})`); 19 | } else if (n.length < USERNAME_MIN_LENGTH) { 20 | throw new Error(`username too short (min ${USERNAME_MIN_LENGTH})`); 21 | } 22 | 23 | if (!n.match(USERNAME_REGEXP)) { 24 | throw new Error("username contains invalid characters"); 25 | } 26 | 27 | return username.toLowerCase(); 28 | } 29 | -------------------------------------------------------------------------------- /src/web3/abis/erc20.ts: -------------------------------------------------------------------------------- 1 | export const ERC_20_ABI = [ 2 | { 3 | constant: false, 4 | inputs: [ 5 | { name: "spender", type: "address" }, 6 | { name: "value", type: "uint256" } 7 | ], 8 | name: "approve", 9 | outputs: [{ name: "", type: "bool" }], 10 | payable: false, 11 | type: "function" 12 | }, 13 | { 14 | constant: true, 15 | inputs: [], 16 | name: "name", 17 | outputs: [{ name: "", type: "string" }], 18 | payable: false, 19 | type: "function" 20 | }, 21 | { 22 | constant: true, 23 | inputs: [], 24 | name: "symbol", 25 | outputs: [{ name: "", type: "string" }], 26 | payable: false, 27 | type: "function" 28 | }, 29 | { 30 | constant: true, 31 | inputs: [], 32 | name: "decimals", 33 | outputs: [{ name: "", type: "uint8" }], 34 | payable: false, 35 | type: "function" 36 | }, 37 | { 38 | constant: true, 39 | inputs: [], 40 | name: "totalSupply", 41 | outputs: [{ name: "", type: "uint256" }], 42 | payable: false, 43 | type: "function" 44 | }, 45 | { 46 | constant: false, 47 | inputs: [ 48 | { name: "from", type: "address" }, 49 | { name: "to", type: "address" }, 50 | { name: "value", type: "uint256" } 51 | ], 52 | name: "transferFrom", 53 | outputs: [{ name: "", type: "bool" }], 54 | payable: false, 55 | type: "function" 56 | }, 57 | { 58 | constant: true, 59 | inputs: [{ name: "who", type: "address" }], 60 | name: "balanceOf", 61 | outputs: [{ name: "", type: "uint256" }], 62 | payable: false, 63 | type: "function" 64 | }, 65 | { 66 | constant: false, 67 | inputs: [ 68 | { name: "to", type: "address" }, 69 | { name: "value", type: "uint256" } 70 | ], 71 | name: "transfer", 72 | outputs: [{ name: "", type: "bool" }], 73 | payable: false, 74 | type: "function" 75 | }, 76 | { 77 | constant: false, 78 | inputs: [ 79 | { name: "spender", type: "address" }, 80 | { name: "value", type: "uint256" }, 81 | { name: "extraData", type: "bytes" } 82 | ], 83 | name: "approveAndCall", 84 | outputs: [{ name: "", type: "bool" }], 85 | payable: false, 86 | type: "function" 87 | }, 88 | { 89 | constant: true, 90 | inputs: [ 91 | { name: "owner", type: "address" }, 92 | { name: "spender", type: "address" } 93 | ], 94 | name: "allowance", 95 | outputs: [{ name: "", type: "uint256" }], 96 | payable: false, 97 | type: "function" 98 | }, 99 | { 100 | anonymous: false, 101 | inputs: [ 102 | { indexed: true, name: "owner", type: "address" }, 103 | { indexed: true, name: "spender", type: "address" }, 104 | { indexed: false, name: "value", type: "uint256" } 105 | ], 106 | name: "Approval", 107 | type: "event" 108 | }, 109 | { 110 | anonymous: false, 111 | inputs: [ 112 | { indexed: true, name: "from", type: "address" }, 113 | { indexed: true, name: "to", type: "address" }, 114 | { indexed: false, name: "value", type: "uint256" } 115 | ], 116 | name: "Transfer", 117 | type: "event" 118 | } 119 | ]; 120 | -------------------------------------------------------------------------------- /src/web3/abis/erc721.ts: -------------------------------------------------------------------------------- 1 | export const ERC_721_ABI = [ 2 | { 3 | constant: true, 4 | inputs: [ 5 | { 6 | name: "_tokenId", 7 | type: "uint256" 8 | } 9 | ], 10 | name: "getApproved", 11 | outputs: [ 12 | { 13 | name: "", 14 | type: "address" 15 | } 16 | ], 17 | payable: false, 18 | stateMutability: "view", 19 | type: "function" 20 | }, 21 | { 22 | constant: false, 23 | inputs: [ 24 | { 25 | name: "_approved", 26 | type: "address" 27 | }, 28 | { 29 | name: "_tokenId", 30 | type: "uint256" 31 | } 32 | ], 33 | name: "approve", 34 | outputs: [], 35 | payable: true, 36 | stateMutability: "payable", 37 | type: "function" 38 | }, 39 | { 40 | constant: false, 41 | inputs: [ 42 | { 43 | name: "_from", 44 | type: "address" 45 | }, 46 | { 47 | name: "_to", 48 | type: "address" 49 | }, 50 | { 51 | name: "_tokenId", 52 | type: "uint256" 53 | } 54 | ], 55 | name: "transferFrom", 56 | outputs: [], 57 | payable: true, 58 | stateMutability: "payable", 59 | type: "function" 60 | }, 61 | { 62 | constant: false, 63 | inputs: [ 64 | { 65 | name: "_from", 66 | type: "address" 67 | }, 68 | { 69 | name: "_to", 70 | type: "address" 71 | }, 72 | { 73 | name: "_tokenId", 74 | type: "uint256" 75 | } 76 | ], 77 | name: "safeTransferFrom", 78 | outputs: [], 79 | payable: true, 80 | stateMutability: "payable", 81 | type: "function" 82 | }, 83 | { 84 | constant: true, 85 | inputs: [ 86 | { 87 | name: "_tokenId", 88 | type: "uint256" 89 | } 90 | ], 91 | name: "ownerOf", 92 | outputs: [ 93 | { 94 | name: "", 95 | type: "address" 96 | } 97 | ], 98 | payable: false, 99 | stateMutability: "view", 100 | type: "function" 101 | }, 102 | { 103 | constant: true, 104 | inputs: [ 105 | { 106 | name: "_owner", 107 | type: "address" 108 | } 109 | ], 110 | name: "balanceOf", 111 | outputs: [ 112 | { 113 | name: "", 114 | type: "uint256" 115 | } 116 | ], 117 | payable: false, 118 | stateMutability: "view", 119 | type: "function" 120 | }, 121 | { 122 | constant: false, 123 | inputs: [ 124 | { 125 | name: "_operator", 126 | type: "address" 127 | }, 128 | { 129 | name: "_approved", 130 | type: "bool" 131 | } 132 | ], 133 | name: "setApprovalForAll", 134 | outputs: [], 135 | payable: false, 136 | stateMutability: "nonpayable", 137 | type: "function" 138 | }, 139 | { 140 | constant: false, 141 | inputs: [ 142 | { 143 | name: "_from", 144 | type: "address" 145 | }, 146 | { 147 | name: "_to", 148 | type: "address" 149 | }, 150 | { 151 | name: "_tokenId", 152 | type: "uint256" 153 | }, 154 | { 155 | name: "data", 156 | type: "bytes" 157 | } 158 | ], 159 | name: "safeTransferFrom", 160 | outputs: [], 161 | payable: true, 162 | stateMutability: "payable", 163 | type: "function" 164 | }, 165 | { 166 | constant: true, 167 | inputs: [ 168 | { 169 | name: "_owner", 170 | type: "address" 171 | }, 172 | { 173 | name: "_operator", 174 | type: "address" 175 | } 176 | ], 177 | name: "isApprovedForAll", 178 | outputs: [ 179 | { 180 | name: "", 181 | type: "bool" 182 | } 183 | ], 184 | payable: false, 185 | stateMutability: "view", 186 | type: "function" 187 | }, 188 | { 189 | anonymous: false, 190 | inputs: [ 191 | { 192 | indexed: true, 193 | name: "_from", 194 | type: "address" 195 | }, 196 | { 197 | indexed: true, 198 | name: "_to", 199 | type: "address" 200 | }, 201 | { 202 | indexed: true, 203 | name: "_tokenId", 204 | type: "uint256" 205 | } 206 | ], 207 | name: "Transfer", 208 | type: "event" 209 | }, 210 | { 211 | anonymous: false, 212 | inputs: [ 213 | { 214 | indexed: true, 215 | name: "_owner", 216 | type: "address" 217 | }, 218 | { 219 | indexed: true, 220 | name: "_approved", 221 | type: "address" 222 | }, 223 | { 224 | indexed: true, 225 | name: "_tokenId", 226 | type: "uint256" 227 | } 228 | ], 229 | name: "Approval", 230 | type: "event" 231 | }, 232 | { 233 | anonymous: false, 234 | inputs: [ 235 | { 236 | indexed: true, 237 | name: "_owner", 238 | type: "address" 239 | }, 240 | { 241 | indexed: true, 242 | name: "_operator", 243 | type: "address" 244 | }, 245 | { 246 | indexed: false, 247 | name: "_approved", 248 | type: "bool" 249 | } 250 | ], 251 | name: "ApprovalForAll", 252 | type: "event" 253 | } 254 | ]; 255 | -------------------------------------------------------------------------------- /src/web3/abis/erc725.ts: -------------------------------------------------------------------------------- 1 | export const ERC_725_ABI = [ 2 | { 3 | constant: true, 4 | inputs: [ 5 | { 6 | name: "_key", 7 | type: "bytes32" 8 | } 9 | ], 10 | name: "getKey", 11 | outputs: [ 12 | { 13 | name: "purposes", 14 | type: "uint256[]" 15 | }, 16 | { 17 | name: "keyType", 18 | type: "uint256" 19 | }, 20 | { 21 | name: "key", 22 | type: "bytes32" 23 | } 24 | ], 25 | payable: false, 26 | stateMutability: "view", 27 | type: "function" 28 | }, 29 | { 30 | constant: false, 31 | inputs: [ 32 | { 33 | name: "_key", 34 | type: "bytes32" 35 | }, 36 | { 37 | name: "_purpose", 38 | type: "uint256" 39 | }, 40 | { 41 | name: "_keyType", 42 | type: "uint256" 43 | } 44 | ], 45 | name: "addKey", 46 | outputs: [ 47 | { 48 | name: "success", 49 | type: "bool" 50 | } 51 | ], 52 | payable: false, 53 | stateMutability: "nonpayable", 54 | type: "function" 55 | }, 56 | { 57 | constant: false, 58 | inputs: [ 59 | { 60 | name: "_id", 61 | type: "uint256" 62 | }, 63 | { 64 | name: "_approve", 65 | type: "bool" 66 | } 67 | ], 68 | name: "approve", 69 | outputs: [ 70 | { 71 | name: "success", 72 | type: "bool" 73 | } 74 | ], 75 | payable: false, 76 | stateMutability: "nonpayable", 77 | type: "function" 78 | }, 79 | { 80 | constant: true, 81 | inputs: [ 82 | { 83 | name: "_purpose", 84 | type: "uint256" 85 | } 86 | ], 87 | name: "getKeysByPurpose", 88 | outputs: [ 89 | { 90 | name: "keys", 91 | type: "bytes32[]" 92 | } 93 | ], 94 | payable: false, 95 | stateMutability: "view", 96 | type: "function" 97 | }, 98 | { 99 | constant: false, 100 | inputs: [ 101 | { 102 | name: "_to", 103 | type: "address" 104 | }, 105 | { 106 | name: "_value", 107 | type: "uint256" 108 | }, 109 | { 110 | name: "_data", 111 | type: "bytes" 112 | } 113 | ], 114 | name: "execute", 115 | outputs: [ 116 | { 117 | name: "executionId", 118 | type: "uint256" 119 | } 120 | ], 121 | payable: false, 122 | stateMutability: "nonpayable", 123 | type: "function" 124 | }, 125 | { 126 | constant: true, 127 | inputs: [ 128 | { 129 | name: "_key", 130 | type: "bytes32" 131 | }, 132 | { 133 | name: "purpose", 134 | type: "uint256" 135 | } 136 | ], 137 | name: "keyHasPurpose", 138 | outputs: [ 139 | { 140 | name: "exists", 141 | type: "bool" 142 | } 143 | ], 144 | payable: false, 145 | stateMutability: "view", 146 | type: "function" 147 | }, 148 | { 149 | anonymous: false, 150 | inputs: [ 151 | { 152 | indexed: true, 153 | name: "key", 154 | type: "bytes32" 155 | }, 156 | { 157 | indexed: true, 158 | name: "purpose", 159 | type: "uint256" 160 | }, 161 | { 162 | indexed: true, 163 | name: "keyType", 164 | type: "uint256" 165 | } 166 | ], 167 | name: "KeyAdded", 168 | type: "event" 169 | }, 170 | { 171 | anonymous: false, 172 | inputs: [ 173 | { 174 | indexed: true, 175 | name: "key", 176 | type: "bytes32" 177 | }, 178 | { 179 | indexed: true, 180 | name: "purpose", 181 | type: "uint256" 182 | }, 183 | { 184 | indexed: true, 185 | name: "keyType", 186 | type: "uint256" 187 | } 188 | ], 189 | name: "KeyRemoved", 190 | type: "event" 191 | }, 192 | { 193 | anonymous: false, 194 | inputs: [ 195 | { 196 | indexed: true, 197 | name: "executionId", 198 | type: "uint256" 199 | }, 200 | { 201 | indexed: true, 202 | name: "to", 203 | type: "address" 204 | }, 205 | { 206 | indexed: true, 207 | name: "value", 208 | type: "uint256" 209 | }, 210 | { 211 | indexed: false, 212 | name: "data", 213 | type: "bytes" 214 | } 215 | ], 216 | name: "ExecutionRequested", 217 | type: "event" 218 | }, 219 | { 220 | anonymous: false, 221 | inputs: [ 222 | { 223 | indexed: true, 224 | name: "executionId", 225 | type: "uint256" 226 | }, 227 | { 228 | indexed: true, 229 | name: "to", 230 | type: "address" 231 | }, 232 | { 233 | indexed: true, 234 | name: "value", 235 | type: "uint256" 236 | }, 237 | { 238 | indexed: false, 239 | name: "data", 240 | type: "bytes" 241 | } 242 | ], 243 | name: "Executed", 244 | type: "event" 245 | }, 246 | { 247 | anonymous: false, 248 | inputs: [ 249 | { 250 | indexed: true, 251 | name: "executionId", 252 | type: "uint256" 253 | }, 254 | { 255 | indexed: false, 256 | name: "approved", 257 | type: "bool" 258 | } 259 | ], 260 | name: "Approved", 261 | type: "event" 262 | } 263 | ]; 264 | -------------------------------------------------------------------------------- /src/web3/address.ts: -------------------------------------------------------------------------------- 1 | import { EthereumNetwork, web3, Web3Address } from "./client"; 2 | import { resolveENSAddress } from "./ens/resolve"; 3 | 4 | export function isValidEthereumAddress(address: string): boolean { 5 | return web3.MAINNET.utils.isAddress(address); 6 | } 7 | 8 | export async function resolveEthereumAddress(req): Promise { 9 | if (!req.address) { 10 | return null; 11 | } 12 | 13 | const address = req.address.toLowerCase(); 14 | const network = req.network || EthereumNetwork.MAINNET; 15 | if (address.endsWith(".eth")) { 16 | return resolveENSAddress(network, address); 17 | } else if (isValidEthereumAddress(address)) { 18 | return { display: address, address, network }; 19 | } else { 20 | return null; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/web3/client.ts: -------------------------------------------------------------------------------- 1 | import * as DataLoader from "dataloader"; 2 | import * as namehash from "eth-ens-namehash"; 3 | import * as Web3 from "web3"; 4 | 5 | export enum EthereumNetwork { 6 | MAINNET, 7 | ROPSTEN, 8 | KOVAN, 9 | RINKEBY 10 | } 11 | 12 | export const web3: { [EthereumNetwork: string]: any } = {}; 13 | for (const network in EthereumNetwork) { 14 | if (!isNaN(Number(network))) { 15 | continue; 16 | } 17 | 18 | let providerURL = process.env[`WEB3_${network}`]; 19 | if (!providerURL) { 20 | providerURL = `https://${network}.infura.io/${process.env.INFURA_API_KEY}`; 21 | } 22 | const provider = new Web3.providers.HttpProvider(providerURL); 23 | 24 | web3[network] = new Web3(provider); 25 | } 26 | 27 | export interface Web3Address { 28 | display?: string; 29 | address: string; 30 | network: EthereumNetwork; 31 | } 32 | 33 | export interface Web3Block { 34 | parentHash: string; 35 | miner: string; 36 | network: EthereumNetwork; 37 | transactions: string[]; 38 | uncles: string[]; 39 | } 40 | 41 | export interface Web3Log { 42 | address: string; 43 | network: EthereumNetwork; 44 | topics: string[]; 45 | } 46 | 47 | export interface Web3Transaction { 48 | network: EthereumNetwork; 49 | hash: string; 50 | blockHash: string; 51 | from: string; 52 | to: string; 53 | } 54 | 55 | export interface Web3TransactionReceipt { 56 | network: EthereumNetwork; 57 | gasUsed: number; 58 | contractAddress?: string; 59 | cumulativeGasUsed: number; 60 | status: boolean; 61 | logs: Web3Log[]; 62 | } 63 | -------------------------------------------------------------------------------- /src/web3/contracts.ts: -------------------------------------------------------------------------------- 1 | export async function callMethodSafe(contract, signature, args = []) { 2 | try { 3 | if (!(signature in contract.methods)) { 4 | return null; 5 | } 6 | 7 | const result = await contract.methods[signature](...args).call(); 8 | return result; 9 | } catch (err) { 10 | return null; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/web3/ens/abi.ts: -------------------------------------------------------------------------------- 1 | export const registryInterface = [ 2 | { 3 | constant: true, 4 | inputs: [ 5 | { 6 | name: "node", 7 | type: "bytes32" 8 | } 9 | ], 10 | name: "resolver", 11 | outputs: [ 12 | { 13 | name: "", 14 | type: "address" 15 | } 16 | ], 17 | type: "function" 18 | }, 19 | { 20 | constant: true, 21 | inputs: [ 22 | { 23 | name: "node", 24 | type: "bytes32" 25 | } 26 | ], 27 | name: "owner", 28 | outputs: [ 29 | { 30 | name: "", 31 | type: "address" 32 | } 33 | ], 34 | type: "function" 35 | }, 36 | { 37 | constant: false, 38 | inputs: [ 39 | { 40 | name: "node", 41 | type: "bytes32" 42 | }, 43 | { 44 | name: "resolver", 45 | type: "address" 46 | } 47 | ], 48 | name: "setResolver", 49 | outputs: [], 50 | type: "function" 51 | }, 52 | { 53 | constant: false, 54 | inputs: [ 55 | { 56 | name: "node", 57 | type: "bytes32" 58 | }, 59 | { 60 | name: "label", 61 | type: "bytes32" 62 | }, 63 | { 64 | name: "owner", 65 | type: "address" 66 | } 67 | ], 68 | name: "setSubnodeOwner", 69 | outputs: [], 70 | type: "function" 71 | }, 72 | { 73 | constant: false, 74 | inputs: [ 75 | { 76 | name: "node", 77 | type: "bytes32" 78 | }, 79 | { 80 | name: "owner", 81 | type: "address" 82 | } 83 | ], 84 | name: "setOwner", 85 | outputs: [], 86 | type: "function" 87 | } 88 | ]; 89 | 90 | export const resolverInterface = [ 91 | { 92 | constant: true, 93 | inputs: [ 94 | { 95 | name: "node", 96 | type: "bytes32" 97 | } 98 | ], 99 | name: "addr", 100 | outputs: [ 101 | { 102 | name: "", 103 | type: "address" 104 | } 105 | ], 106 | type: "function" 107 | }, 108 | { 109 | constant: true, 110 | inputs: [ 111 | { 112 | name: "node", 113 | type: "bytes32" 114 | } 115 | ], 116 | name: "content", 117 | outputs: [ 118 | { 119 | name: "", 120 | type: "bytes32" 121 | } 122 | ], 123 | type: "function" 124 | }, 125 | { 126 | constant: true, 127 | inputs: [ 128 | { 129 | name: "node", 130 | type: "bytes32" 131 | } 132 | ], 133 | name: "name", 134 | outputs: [ 135 | { 136 | name: "", 137 | type: "string" 138 | } 139 | ], 140 | type: "function" 141 | }, 142 | { 143 | constant: true, 144 | inputs: [ 145 | { 146 | name: "node", 147 | type: "bytes32" 148 | }, 149 | { 150 | name: "kind", 151 | type: "bytes32" 152 | } 153 | ], 154 | name: "has", 155 | outputs: [ 156 | { 157 | name: "", 158 | type: "bool" 159 | } 160 | ], 161 | type: "function" 162 | }, 163 | { 164 | constant: false, 165 | inputs: [ 166 | { 167 | name: "node", 168 | type: "bytes32" 169 | }, 170 | { 171 | name: "addr", 172 | type: "address" 173 | } 174 | ], 175 | name: "setAddr", 176 | outputs: [], 177 | type: "function" 178 | }, 179 | { 180 | constant: false, 181 | inputs: [ 182 | { 183 | name: "node", 184 | type: "bytes32" 185 | }, 186 | { 187 | name: "hash", 188 | type: "bytes32" 189 | } 190 | ], 191 | name: "setContent", 192 | outputs: [], 193 | type: "function" 194 | }, 195 | { 196 | constant: false, 197 | inputs: [ 198 | { 199 | name: "node", 200 | type: "bytes32" 201 | }, 202 | { 203 | name: "name", 204 | type: "string" 205 | } 206 | ], 207 | name: "setName", 208 | outputs: [], 209 | type: "function" 210 | }, 211 | { 212 | constant: true, 213 | inputs: [ 214 | { 215 | name: "node", 216 | type: "bytes32" 217 | }, 218 | { 219 | name: "contentType", 220 | type: "uint256" 221 | } 222 | ], 223 | name: "ABI", 224 | outputs: [ 225 | { 226 | name: "", 227 | type: "uint256" 228 | }, 229 | { 230 | name: "", 231 | type: "bytes" 232 | } 233 | ], 234 | payable: false, 235 | type: "function" 236 | } 237 | ]; 238 | -------------------------------------------------------------------------------- /src/web3/ens/resolve.ts: -------------------------------------------------------------------------------- 1 | import * as namehash from "eth-ens-namehash"; 2 | import { EthereumNetwork, web3 } from "../client"; 3 | import { registryInterface, resolverInterface } from "./abi"; 4 | 5 | export const ENS_CONTRACT_ADDRESSES: { [EthereumNetwork: string]: string } = { 6 | MAINNET: "0x314159265dd8dbb310642f98f50c066173c1259b", 7 | RINKEBY: "0xe7410170f87102df0055eb195163a03b7f2bff4a", 8 | ROPSTEN: "0x112234455c3a32fd11230c42e7bccd4a84e02010" 9 | }; 10 | 11 | export const ens: { [EthereumNetwork: string]: any } = {}; 12 | for (const network in ENS_CONTRACT_ADDRESSES) { 13 | if (!isNaN(Number(network))) { 14 | continue; 15 | } 16 | ens[network] = new web3[network].eth.Contract( 17 | registryInterface, 18 | ENS_CONTRACT_ADDRESSES[network] 19 | ); 20 | } 21 | 22 | export async function resolveENSAddress(network, host) { 23 | const nh = namehash.hash(host); 24 | const registry = ens[network]; 25 | if (!registry) { 26 | return null; 27 | } 28 | 29 | const resolverAddr = await registry.methods.resolver(nh).call(); 30 | if (resolverAddr === "0x0000000000000000000000000000000000000000") { 31 | return null; 32 | } 33 | 34 | const resolverContract = new web3[network].eth.Contract( 35 | resolverInterface, 36 | resolverAddr 37 | ); 38 | 39 | const addr = await resolverContract.methods.addr(nh).call(); 40 | return { display: host, address: addr.toLowerCase(), network }; 41 | } 42 | -------------------------------------------------------------------------------- /src/web3/etherscan.ts: -------------------------------------------------------------------------------- 1 | import * as fetch from "node-fetch"; 2 | import * as qs from "qs"; 3 | import { EthereumNetwork } from "./client"; 4 | 5 | export const ETHERSCAN_API_BASE_URLS: { [EthereumNetwork: string]: string } = { 6 | KOVAN: "http://api-kovan.etherscan.io/api", 7 | MAINNET: "http://api.etherscan.io/api", 8 | RINKEBY: "http://api-rinkeby.etherscan.io/api", 9 | ROPSTEN: "http://api-ropsten.etherscan.io/api" 10 | }; 11 | 12 | const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; 13 | 14 | export async function fetchTransactions({ 15 | network, 16 | address, 17 | startBlock, 18 | endBlock, 19 | page, 20 | offset 21 | }: { 22 | network: EthereumNetwork; 23 | address: string; 24 | startBlock: number; 25 | endBlock: number; 26 | page: number; 27 | offset: number; 28 | }) { 29 | const baseURL = ETHERSCAN_API_BASE_URLS[network]; 30 | const q = qs.stringify({ 31 | action: "txlist", 32 | address, 33 | apikey: ETHERSCAN_API_KEY, 34 | endblock: endBlock, 35 | module: "account", 36 | offset, 37 | page, 38 | startblock: startBlock 39 | }); 40 | 41 | const res = await fetch(`${baseURL}?${q}`); 42 | if (!res.ok) { 43 | throw new Error("could not fetch transactions"); 44 | } 45 | 46 | const resBody = await res.json(); 47 | if (!resBody.result) { 48 | throw new Error("could not fetch transactions"); 49 | } 50 | 51 | return resBody.result.map(r => r.hash); 52 | } 53 | -------------------------------------------------------------------------------- /src/web3/loaders.ts: -------------------------------------------------------------------------------- 1 | import * as DataLoader from "dataloader"; 2 | import { resolveEthereumAddress } from "./address"; 3 | import { 4 | EthereumNetwork, 5 | web3, 6 | Web3Address, 7 | Web3Block, 8 | Web3Transaction, 9 | Web3TransactionReceipt 10 | } from "./client"; 11 | import { 12 | ETHEREUM_CONTRACT_STANDARD_ABIS, 13 | EthereumContractStandard 14 | } from "./standards"; 15 | 16 | export interface Web3HashRequest { 17 | hash: string; 18 | network: EthereumNetwork; 19 | } 20 | 21 | export interface Web3AddressRequest { 22 | address: string; 23 | network: EthereumNetwork; 24 | } 25 | 26 | export interface Web3BlockRequest { 27 | hash?: string; 28 | number?: number; 29 | network: EthereumNetwork; 30 | } 31 | 32 | export interface Web3ContractRequest { 33 | address: string; 34 | network: EthereumNetwork; 35 | interface: { 36 | standards: EthereumContractStandard[]; 37 | inline: any[]; 38 | }; 39 | } 40 | 41 | export function createWeb3Loaders() { 42 | return { 43 | address: new DataLoader(inputs => { 44 | return Promise.all(inputs.map(resolveEthereumAddress)); 45 | }), 46 | balance: new DataLoader(inputs => { 47 | return Promise.all( 48 | inputs.map(({ address, network }) => { 49 | return web3[network].eth.getBalance(address); 50 | }) 51 | ); 52 | }), 53 | block: new DataLoader(inputs => { 54 | return Promise.all( 55 | inputs.map(async i => { 56 | const block = await web3[i.network].eth.getBlock(i.hash || i.number); 57 | if (!block) { 58 | return null; 59 | } 60 | block.network = i.network; 61 | return block; 62 | }) 63 | ); 64 | }), 65 | blockTransactionCount: new DataLoader( 66 | inputs => { 67 | return Promise.all( 68 | inputs.map(async ({ hash, network }) => { 69 | return web3[network].eth.getBlockTransactionCount(hash); 70 | }) 71 | ); 72 | } 73 | ), 74 | contract: new DataLoader(async inputs => 75 | inputs.map(input => { 76 | try { 77 | let jsonInterface = input.interface.inline || []; 78 | if (input.interface.standards) { 79 | for (const standard of input.interface.standards) { 80 | jsonInterface = jsonInterface.concat( 81 | ETHEREUM_CONTRACT_STANDARD_ABIS[standard] 82 | ); 83 | } 84 | } 85 | 86 | const contract = new web3[input.network].eth.Contract( 87 | jsonInterface, 88 | input.address 89 | ); 90 | 91 | contract.network = input.network; 92 | return contract; 93 | } catch (err) { 94 | return new Error("invalid contract ABI"); 95 | } 96 | }) 97 | ), 98 | transaction: new DataLoader(inputs => { 99 | return Promise.all( 100 | inputs.map(async ({ hash, network }) => { 101 | const tx = await web3[network].eth.getTransaction(hash); 102 | if (!tx) { 103 | return null; 104 | } 105 | tx.network = network; 106 | return tx; 107 | }) 108 | ); 109 | }), 110 | transactionCount: new DataLoader( 111 | inputs => { 112 | return Promise.all( 113 | inputs.map(async ({ address, network }) => { 114 | return web3[network].eth.getTransactionCount(address); 115 | }) 116 | ); 117 | } 118 | ), 119 | transactionReceipt: new DataLoader( 120 | inputs => { 121 | return Promise.all( 122 | inputs.map(async ({ hash, network }) => { 123 | const receipt = await web3[network].eth.getTransactionReceipt(hash); 124 | if (!receipt) { 125 | return null; 126 | } 127 | receipt.network = network; 128 | return receipt; 129 | }) 130 | ); 131 | } 132 | ) 133 | }; 134 | } 135 | -------------------------------------------------------------------------------- /src/web3/standards.ts: -------------------------------------------------------------------------------- 1 | import { ERC_20_ABI } from "./abis/erc20"; 2 | import { ERC_721_ABI } from "./abis/erc721"; 3 | import { ERC_725_ABI } from "./abis/erc725"; 4 | 5 | export enum EthereumContractStandard { 6 | ERC_20, 7 | ERC_721, 8 | ERC_725 9 | } 10 | 11 | export const ETHEREUM_CONTRACT_STANDARD_ABIS = { 12 | ERC_20: ERC_20_ABI, 13 | ERC_721: ERC_721_ABI, 14 | ERC_725: ERC_725_ABI 15 | }; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "outDir": "dist", 8 | "lib": [ 9 | "esnext", 10 | "dom" 11 | ] 12 | }, 13 | "include": [ 14 | "./src/**/*.ts", 15 | "./node_modules/web3-typescript-typings/index.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended", 5 | "tslint-plugin-prettier", 6 | "tslint-config-prettier" 7 | ], 8 | "rules": { 9 | "prettier": true, 10 | "interface-name": false 11 | } 12 | } 13 | --------------------------------------------------------------------------------