Skip to content

Commit 435b592

Browse files
authored
Implement mutations in preparation for self-sovereign identity contract creation (#40)
Implements #39
1 parent 99311f5 commit 435b592

File tree

7 files changed

+312
-50
lines changed

7 files changed

+312
-50
lines changed

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
"eth-ens-namehash": "^2.0.8",
3030
"graphql-type-json": "^0.2.1",
3131
"graphql-yoga": "1.14.6",
32+
"jsonwebtoken": "^8.3.0",
3233
"libphonenumber-js": "^1.2.15",
34+
"moment": "^2.22.2",
3335
"node-fetch": "^2.1.2",
3436
"prisma-binding": "^2.0.2",
3537
"qs": "^6.5.2",

src/phone.ts

+39
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import {
33
format as formatPhoneNumber,
44
parse as parsePhoneNumber
55
} from "libphonenumber-js";
6+
import * as moment from "moment";
67
import * as fetch from "node-fetch";
78
import * as qs from "qs";
89
import { DEFAULT_REDIS_CLIENT } from "./redis";
10+
import { generateToken, verifyToken } from "./tokens";
911

1012
const TWILIO_ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID;
1113
const TWILIO_API_KEY = process.env.TWILIO_API_KEY;
@@ -108,3 +110,40 @@ export function generatePhoneNumberHash(phoneNumber: string): string {
108110
hash.update(normalizePhoneNumber(phoneNumber) + GLOBAL_PHONE_NUMBER_SALT);
109111
return hash.digest("hex").substring(0, TRUNCATE_BYTES);
110112
}
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<string> {
142+
const { hashedPhoneNumber } = await verifyToken(phoneNumberToken);
143+
144+
if (!hashedPhoneNumber) {
145+
throw new Error("invalid phone number token");
146+
}
147+
148+
return hashedPhoneNumber;
149+
}

src/resolvers/Mutation.ts

+142-36
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import { Context } from "../context";
22
import {
33
checkPhoneNumberVerificationCode,
44
generatePhoneNumberHash,
5-
startPhoneNumberVerification
5+
generatePhoneNumberToken,
6+
startPhoneNumberVerification,
7+
validatePhoneNumberToken
68
} from "../phone";
9+
import { normalizeUsername } from "../usernames";
710
import { web3 } from "../web3/client";
811

912
export const Mutation = {
@@ -12,69 +15,128 @@ export const Mutation = {
1215
{ input }: { input: { phoneNumber: string } },
1316
ctx: Context
1417
) {
15-
await startPhoneNumberVerification(input.phoneNumber);
16-
return { ok: true };
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+
}
1727
},
18-
async updatePhoneNumber(
28+
async checkPhoneNumberVerification(
1929
parent,
2030
{
2131
input
2232
}: {
2333
input: {
2434
phoneNumber: string;
2535
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;
2684
address: string;
2785
};
2886
},
2987
ctx: Context,
3088
info
3189
) {
32-
await checkPhoneNumberVerificationCode(
33-
input.phoneNumber,
34-
input.verificationCode
35-
);
90+
try {
91+
const hashedPhoneNumber = await validatePhoneNumberToken(
92+
input.phoneNumberToken
93+
);
3694

37-
const hashedPhoneNumber = generatePhoneNumberHash(input.phoneNumber);
38-
return {
39-
phoneNumber: ctx.db.mutation.upsertPhoneNumber(
40-
{
41-
create: { hashedPhoneNumber, address: input.address },
42-
update: { address: input.address },
43-
where: { hashedPhoneNumber }
44-
},
45-
// a bit of a hack since I'm not sure what to do with info here
46-
`{
47-
hashedPhoneNumber
48-
address
49-
createdAt
50-
updatedAt
51-
}`
52-
)
53-
};
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+
}
54115
},
55116
async deletePhoneNumber(
56117
parent,
57118
{
58119
input
59120
}: {
60121
input: {
61-
phoneNumber: string;
62-
verificationCode: string;
122+
phoneNumberToken: string;
63123
};
64124
},
65125
ctx: Context
66126
) {
67-
await checkPhoneNumberVerificationCode(
68-
input.phoneNumber,
69-
input.verificationCode
70-
);
127+
try {
128+
const hashedPhoneNumber = await validatePhoneNumberToken(
129+
input.phoneNumberToken
130+
);
71131

72-
const hashedPhoneNumber = generatePhoneNumberHash(input.phoneNumber);
73-
await ctx.db.mutation.deletePhoneNumber({
74-
where: { hashedPhoneNumber }
75-
});
132+
await ctx.db.mutation.deletePhoneNumber({
133+
where: { hashedPhoneNumber }
134+
});
76135

77-
return { ok: true };
136+
return { ok: true };
137+
} catch (err) {
138+
return { message: err.message, ok: false };
139+
}
78140
},
79141
async sendRawEthereumTransaction(parent, { input }, ctx) {
80142
const hash = await web3[input.network].eth.sendRawTransaction(input);
@@ -84,5 +146,49 @@ export const Mutation = {
84146
network: input.network
85147
})
86148
};
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+
}
87193
}
88194
};

0 commit comments

Comments
 (0)