on f3545aa help sign in
bridge/src/lib/useRoller.ts
504 lines 14 kB history raw
1import { useCallback, useEffect, useMemo, useState } from 'react';
2import * as azimuth from 'azimuth-js';
3import * as need from 'lib/need';
4import * as ob from 'urbit-ob';
5import { Just } from 'folktale/maybe';
7import { Invite } from 'lib/types/Invite';
8import { convertToInt } from './convertToInt';
9import { ensureHexPrefix } from 'form/formatters';
10import { getOutgoingPoints } from 'views/Points';
11import { isDevelopment, isRopsten } from './flags';
12import { isPlanet } from './utils/point';
13import { hasPoint, generateInviteWallet } from './utils/roller';
14import { ROLLER_HOSTS } from './constants';
15import { signTransactionHash } from './authToken';
16import { useNetwork } from 'store/network';
17import { usePointCache } from 'store/pointCache';
18import { usePointCursor } from 'store/pointCursor';
19import { useRollerStore } from 'store/roller';
20import { useWallet } from 'store/wallet';
22import {
23 deriveNetworkKeys,
24 CRYPTO_SUITE_VERSION,
25 deriveNetworkSeedFromUrbitWallet,
26} from 'lib/keys';
28import {
29 Config,
30 Ship,
31 L2Point,
32 Proxy,
33 RollerRPCAPI,
34 Options,
35 EthAddress,
36 UnspawnedPoints,
37 From,
38 Hash,
39} from '@urbit/roller-api';
41import {
42 configureKeys,
43 getTimeToNextBatch,
44 spawn,
45 transferPoint,
46} from './utils/roller';
48import {
49 getStoredInvites,
50 setPendingInvites,
51 setStoredInvites,
52} from 'store/storage/roller';
54const getProxyAndNonce = (
55 point: L2Point,
56 address: string
57): { proxy?: string; nonce?: number } => {
58 return point.ownership?.managementProxy?.address === address
59 ? { proxy: 'manage', nonce: point.ownership?.managementProxy.nonce }
60 : point.ownership?.owner?.address === address
61 ? { proxy: 'own', nonce: point.ownership?.owner.nonce }
62 : point.ownership?.spawnProxy?.address === address
63 ? { proxy: 'spawn', nonce: point.ownership?.spawn?.owner.nonce }
64 : point.ownership?.votingProxy?.address === address
65 ? { proxy: 'vote', nonce: point.ownership?.votingProxy?.owner.nonce }
66 : point.ownership?.transferProxy?.address === address
67 ? {
68 proxy: 'transfer',
69 nonce: point.transferProxy?.votingProxy?.owner.nonce,
70 }
71 : { proxy: undefined, nonce: undefined };
72};
74export default function useRoller() {
75 const { wallet, authToken, authMnemonic, urbitWallet }: any = useWallet();
76 const { pointCursor }: any = usePointCursor();
77 const { web3, contracts }: any = useNetwork();
78 const allPoints: any = usePointCache();
79 const controlledPoints = allPoints?.controlledPoints;
80 const getDetails = allPoints?.getDetails;
82 const {
83 nextBatchTime,
84 setNextBatchTime,
85 setNextRoll,
86 setPendingTransactions,
87 setInvites,
88 currentL2,
89 } = useRollerStore();
90 const [config, setConfig] = useState<Config | null>(null);
92 const options: Options = useMemo(() => {
93 const type = isRopsten || !isDevelopment ? 'https' : 'http';
94 const host = isRopsten
95 ? ROLLER_HOSTS.ROPSTEN
96 : isDevelopment
97 ? ROLLER_HOSTS.LOCAL
98 : ROLLER_HOSTS.MAINNET;
99 const port = isRopsten ? 443 : 8080;
100 const path = '/v1/roller';
102 return {
103 transport: {
104 type,
105 host,
106 port,
107 path,
108 },
109 };
110 }, []);
112 const api = useMemo(() => {
113 return new RollerRPCAPI(options);
114 }, [options]);
116 const fetchConfig = useCallback(async () => {
117 api
118 .getRollerConfig()
119 .then(response => {
120 setConfig(response);
121 setNextBatchTime(response.nextBatch);
122 })
123 .catch(err => {
124 // TODO: more elegant error handling
125 console.warn(
126 '[fetchConfig:failed] check the roller connection?\n',
127 err
128 );
129 });
130 }, [api]); // eslint-disable-line react-hooks/exhaustive-deps
132 const generateInviteCodes = useCallback(
133 async (numInvites: number) => {
134 const _point = need.point(pointCursor);
135 const _contracts = contracts.getOrElse(null);
136 const _web3 = web3.getOrElse(null);
137 const _wallet = wallet.getOrElse(null);
138 const _authToken = authToken.getOrElse(null);
139 const _details = getDetails(_point);
140 if (!_contracts || !_web3 || !_wallet || !_authToken || !_details) {
141 // not using need because we want a custom error
142 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
143 }
145 if (Just.hasInstance(authMnemonic)) {
146 throw new Error(
147 "Auth key Error: A management mnemonic can't create invite codes"
148 );
149 }
150 const planets: UnspawnedPoints = await api.getUnspawned(_point);
151 const starInfo = await api.getPoint(_point);
153 const tickets: {
154 ticket: string;
155 planet: number;
156 owner: string;
157 }[] = [];
158 const requests: Promise<string>[] = [];
160 // TODO: replace with "currentNonce" from store, that would reflect the latest nonce
161 // Bridge has used for the star _wallet.address
162 //
163 const { proxy, nonce } = getProxyAndNonce(starInfo, _wallet.address);
165 if (!(proxy === 'own' || proxy === 'spawn') || nonce === undefined)
166 throw new Error("Error: Address doesn't match proxy");
168 for (let i = 0; i < numInvites && planets[i]; i++) {
169 const planet = planets[i];
170 const nonceInc = i + nonce;
172 const { ticket, owner } = await generateInviteWallet(
173 planet,
174 _authToken
175 );
177 const spawnRequest = await spawn(
178 api,
179 _wallet,
180 _point,
181 proxy,
182 nonceInc,
183 planet
184 );
186 const configureKeysRequest = await configureKeys(
187 api,
188 _wallet,
189 planet,
190 proxy,
191 0,
192 _details,
193 authToken,
194 authMnemonic,
195 wallet,
196 urbitWallet
197 );
199 const transferPointRequest = await transferPoint(
200 api,
201 _wallet,
202 planet,
203 proxy,
204 1,
205 owner.keys.address
206 );
208 requests.push(spawnRequest, configureKeysRequest, transferPointRequest);
209 tickets.push({
210 ticket,
211 planet,
212 owner: owner.keys.address,
213 });
214 }
216 const hashes = await Promise.all(requests);
218 const pendingInvites: Invite[] = tickets.map((ticket, ind) => ({
219 ...ticket,
220 hash: hashes[ind * 3 + 2],
221 status: 'pending',
222 }));
224 setPendingInvites(_point, pendingInvites);
225 },
226 [
227 api,
228 authToken,
229 contracts,
230 pointCursor,
231 wallet,
232 web3,
233 getDetails,
234 authMnemonic,
235 urbitWallet,
236 ]
237 );
239 const getPoints = useCallback(
240 async (proxy: Proxy, address: EthAddress) => {
241 const points: Ship[] =
242 proxy === 'own'
243 ? await api.getOwnedPoints(address)
244 : proxy === 'manage'
245 ? await api.getManagerFor(address)
246 : proxy === 'vote'
247 ? await api.getVotingFor(address)
248 : proxy === 'transfer'
249 ? await api.getTransferringFor(address)
250 : proxy === 'spawn'
251 ? await api.getSpawningFor(address)
252 : [];
254 return points;
255 },
256 [api]
257 );
259 const getPendingTransactions = useCallback(async () => {
260 try {
261 const curPoint = Number(need.point(pointCursor));
262 const newPending = await api.getPendingByShip(curPoint);
263 setPendingTransactions(newPending);
265 // const allTransactions = await api.getHistory()
266 } catch (error) {
267 console.warn('ERROR GETTING PENDING', error);
268 }
269 }, [api, setPendingTransactions, pointCursor]);
271 const getInvites = useCallback(
272 async (isL2: boolean) => {
273 try {
274 const curPoint: number = Number(need.point(pointCursor));
275 const invites = getStoredInvites(curPoint);
276 const availableInvites = invites.available;
277 const pendingTransactions = await api.getPendingByShip(curPoint);
278 if (isDevelopment) {
279 console.log('PENDING', pendingTransactions);
280 }
281 setPendingTransactions(pendingTransactions);
283 const stillPending = invites.pending.filter(invite => {
284 const completed = !pendingTransactions.find(
285 p => `~${p?.rawTx?.tx?.data?.ship}` === ob.patp(invite.planet)
286 );
288 if (
289 completed &&
290 !availableInvites.find(({ planet }) => invite.planet === planet)
291 ) {
292 availableInvites.push({ ...invite, status: 'available' });
293 }
295 return !completed;
296 });
298 setStoredInvites(curPoint, {
299 available: availableInvites,
300 pending: stillPending,
301 });
302 setInvites(availableInvites);
304 const _authToken = authToken.getOrElse(null);
305 const _contracts = contracts.getOrElse(null);
307 if (_authToken && _contracts) {
308 let possibleMissingInvites: number[] = [];
309 let availablePoints: number[] = [];
310 if (isL2) {
311 possibleMissingInvites = await api.getSpawned(curPoint);
312 availablePoints = await api.getUnspawned(curPoint);
313 } else {
314 // TODO: should it be removed?
315 availablePoints = await azimuth.azimuth.getUnspawnedChildren(
316 _contracts,
317 curPoint
318 );
319 }
321 const outgoingPoints = getOutgoingPoints(
322 controlledPoints,
323 getDetails
324 );
326 possibleMissingInvites = possibleMissingInvites.concat(
327 outgoingPoints.filter(
328 (p: number) => isPlanet(p) && availablePoints.includes(p)
329 )
330 );
332 // Iterate over all spawned and controlled planets
333 // If the planet is not in available invites, generate the ticket and add it
334 if (isDevelopment) {
335 console.log('POSSIBLE MISSING', possibleMissingInvites);
336 }
337 // TODO: this can be verym slow, so best to allow the user to manually
338 // reveal the ticket, after checking if the invite is still "active"
339 // (i.e. if the address inferred from the derived inviteWallet corresponds to
340 // to the address the point has as owner)
341 //
342 for (let i = 0; i < possibleMissingInvites.length; i++) {
343 const planet = possibleMissingInvites[i];
344 if (!availableInvites.find(hasPoint(planet))) {
345 console.log('MISSING IN AVAILABLE', planet);
346 const { ticket, owner } = await generateInviteWallet(
347 planet,
348 _authToken
349 );
351 availableInvites.push({
352 ticket,
353 status: 'available',
354 planet,
355 hash: '',
356 owner: owner.keys.address,
357 });
358 }
359 }
361 setStoredInvites(curPoint, {
362 available: availableInvites.filter(({ planet }) =>
363 possibleMissingInvites.includes(planet)
364 ),
365 pending: stillPending,
366 });
367 setInvites(availableInvites);
368 }
369 } catch (error) {
370 console.warn('ERROR GETTING INVITES', error);
371 }
372 },
373 [
374 api,
375 pointCursor,
376 setInvites,
377 setPendingTransactions,
378 authToken,
379 contracts,
380 controlledPoints,
381 getDetails,
382 ]
383 );
385 // TODO: extract from #transferPoint
386 // interface ConfigureKeysParams {
388 // }
390 // const configureKeys = async () => {
392 // }
394 interface AcceptInviteParams {
395 point: Ship;
396 to: EthAddress;
397 fromWallet: any; // TODO: wallet type
398 toWallet: any; // TODO: wallet type
399 }
401 const acceptInvite = async ({
402 point,
403 to,
404 fromWallet,
405 toWallet,
406 }: AcceptInviteParams) => {
407 const azimuthPoint = await api.getPoint(point);
408 console.log('azimuthPoint', azimuthPoint);
409 const ownerAddress = azimuthPoint?.ownership?.owner?.address!;
411 const { nonce } = getProxyAndNonce(azimuthPoint, ownerAddress);
412 if (nonce === undefined) {
413 throw new Error('Nonce unavailable');
414 }
416 // 1. Update networking keys
417 // TODO: extract this to a useRoller#configureKeys function?
418 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
419 const nextRevision = networkRevision + 1;
420 const fromOwnerProxy: From = {
421 ship: point,
422 proxy: 'own',
423 };
425 const seed = await deriveNetworkSeedFromUrbitWallet(toWallet, nextRevision);
426 const keys = deriveNetworkKeys(seed.value);
427 const keysData = {
428 auth: ensureHexPrefix(keys.auth.public),
429 breach: false,
430 cryptoSuite: CRYPTO_SUITE_VERSION.toString(),
431 encrypt: ensureHexPrefix(keys.crypt.public),
432 };
434 const keysHash: Hash = await api.hashTransaction(
435 nonce,
436 fromOwnerProxy,
437 'configureKeys',
438 keysData
439 );
441 const keysSigningKey = fromWallet.privateKey;
442 await api.configureKeys(
443 signTransactionHash(keysHash, Buffer.from(keysSigningKey, 'hex')),
444 fromOwnerProxy,
445 ownerAddress,
446 keysData
447 );
449 // 2. Transfer point
450 const transferData = { address: to, reset: false };
451 const transferHash: Hash = await api.hashTransaction(
452 nonce + 1,
453 fromOwnerProxy,
454 'transferPoint',
455 transferData
456 );
458 const transferTxHash = await api.transferPoint(
459 signTransactionHash(transferHash, Buffer.from(keysSigningKey, 'hex')),
460 fromOwnerProxy,
461 ownerAddress,
462 transferData
463 );
465 return transferTxHash;
466 };
468 // On load, get initial config
469 useEffect(() => {
470 if (config) {
471 return;
472 }
474 fetchConfig();
475 }, [config, fetchConfig]);
477 useEffect(() => {
478 const interval = setInterval(() => {
479 const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
480 setNextRoll(nextRoll);
482 if (nextBatchTime <= new Date().getTime()) {
483 api.getRollerConfig().then(response => {
484 setNextBatchTime(response.nextBatch);
485 });
487 getInvites(currentL2);
488 }
489 }, 1000);
491 return () => clearInterval(interval);
492 }, [nextBatchTime, getPendingTransactions, currentL2]); // eslint-disable-line react-hooks/exhaustive-deps
494 return {
495 api,
496 acceptInvite,
497 config,
498 getPoints,
499 getInvites,
500 getPendingTransactions,
501 generateInviteCodes,
502 transferPoint,
503 };