├── .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 |

2 |
3 |
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 |
--------------------------------------------------------------------------------