on ca31bdf help sign in
bridge/src/lib/useRoller.ts
685 lines 18 kB history raw
1import { useCallback, useEffect, useMemo, useState } from 'react';
2import * as azimuth from 'azimuth-js';
3import * as need from 'lib/need';
4import SecureLS from 'secure-ls';
6import { Invite } from 'lib/types/Invite';
7import { convertToInt } from './convertToInt';
8import { isDevelopment, isRopsten } from './flags';
9import { generateInviteWallet, getPendingSpawns } from './utils/roller';
10import { ROLLER_HOSTS } from './constants';
12import { useNetwork } from 'store/network';
13import { usePointCursor } from 'store/pointCursor';
14import { useRollerStore } from 'store/roller';
15import { useWallet } from 'store/wallet';
16import { getStoredInvites, setStoredInvites } from 'store/storage/roller';
17import { usePointCache } from 'store/pointCache';
18import { getOutgoingPoints } from 'views/Points';
19import {
20 getSpawnProxy,
21 getManagerProxy,
22 getAddressProxy,
23 getTransferProxy,
24} from './utils/proxy';
25import { isPlanet } from './utils/point';
27import {
28 deriveNetworkSeedFromUrbitWallet,
29 attemptNetworkSeedDerivation,
30} from 'lib/keys';
32import {
33 Config,
34 Ship,
35 Proxy,
36 RollerRPCAPI,
37 Options,
38 EthAddress,
39 UnspawnedPoints,
40} from '@urbit/roller-api';
42import {
43 configureKeys,
44 getTimeToNextBatch,
45 spawn,
46 transferPointRequest,
47 registerProxyAddress,
48} from './utils/roller';
50const ONE_SECOND = 1000;
52const inviteTemplate = (
53 planet: number,
54 ticket = '',
55 owner = '',
56 hash = ''
57): Invite => ({
58 planet,
59 ticket,
60 owner,
61 hash,
62});
64export default function useRoller() {
65 const { wallet, authToken, authMnemonic, urbitWallet }: any = useWallet();
66 const { pointCursor }: any = usePointCursor();
67 const { web3, contracts }: any = useNetwork();
68 const allPoints: any = usePointCache();
69 const controlledPoints = allPoints?.controlledPoints;
70 const getDetails = allPoints?.getDetails;
72 const {
73 nextBatchTime,
74 currentL2,
75 invitePoints,
76 invites,
77 setNextBatchTime,
78 setNextRoll,
79 setPendingTransactions,
80 setInvites,
81 setInvitePoints,
82 setInviteGeneratingNum,
83 } = useRollerStore();
84 const [config, setConfig] = useState<Config | null>(null);
86 const options: Options = useMemo(() => {
87 const type = isRopsten || !isDevelopment ? 'https' : 'http';
88 const host = isRopsten
89 ? ROLLER_HOSTS.ROPSTEN
90 : isDevelopment
91 ? ROLLER_HOSTS.LOCAL
92 : ROLLER_HOSTS.MAINNET;
93 const port = isDevelopment && !isRopsten ? 8080 : 443;
94 const path = '/v1/roller';
96 return {
97 transport: {
98 type,
99 host,
100 port,
101 path,
102 },
103 };
104 }, []);
106 const api = useMemo(() => {
107 return new RollerRPCAPI(options);
108 }, [options]);
110 const fetchConfig = useCallback(async () => {
111 api
112 .getRollerConfig()
113 .then(response => {
114 setConfig(response);
115 setNextBatchTime(response.nextBatch);
116 })
117 .catch(err => {
118 // TODO: more elegant error handling
119 console.warn(
120 '[fetchConfig:failed] check the roller connection?\n',
121 err
122 );
123 });
124 }, [api]); // eslint-disable-line react-hooks/exhaustive-deps
126 const ls = useMemo(
127 () =>
128 new SecureLS({
129 isCompression: false,
130 encryptionSecret: authToken.getOrElse('default'),
131 }),
132 [authToken]
133 );
135 const generateInviteCodes = useCallback(
136 async (numInvites: number) => {
137 const _point = need.point(pointCursor);
138 const _contracts = contracts.getOrElse(null);
139 const _web3 = web3.getOrElse(null);
140 const _wallet = wallet.getOrElse(null);
141 const _authToken = authToken.getOrElse(null);
142 const _details = getDetails(_point);
144 if (!_contracts || !_web3 || !_wallet || !_authToken || !_details) {
145 // not using need because we want a custom error
146 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
147 }
149 // if (Just.hasInstance(authMnemonic)) {
150 // throw new Error(
151 // "Auth key Error: A management mnemonic can't create invite codes"
152 // );
153 // }
154 let planets: UnspawnedPoints = await api.getUnspawned(_point);
156 const pendingTxs = await api.getPendingByShip(_point);
158 const pendingSpawns = getPendingSpawns(pendingTxs);
160 planets = planets.filter((point: number) => !pendingSpawns.has(point));
162 const starInfo = await api.getPoint(_point);
163 const invites: Invite[] = [];
164 const requests: Promise<string>[] = [];
166 const proxy = getSpawnProxy(starInfo.ownership!, _wallet.address);
168 if (proxy === undefined)
169 throw new Error("Error: Address doesn't match proxy");
171 const nonce = await api.getNonce({
172 ship: _point,
173 proxy,
174 });
176 for (let i = 0; i < numInvites && planets[i]; i++) {
177 setInviteGeneratingNum(i + 1);
178 const planet = planets[i];
179 const nonceInc = i + nonce;
181 const { ticket, inviteWallet } = await generateInviteWallet(
182 planet,
183 _authToken
184 );
186 const spawnRequest = spawn(
187 api,
188 _wallet,
189 _point,
190 proxy,
191 nonceInc,
192 planet
193 );
195 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
196 inviteWallet,
197 1
198 );
199 const configureKeysRequest = configureKeys(
200 api,
201 _wallet,
202 planet,
203 'own',
204 0,
205 networkSeed
206 );
208 const setManagementProxyRequest = registerProxyAddress(
209 api,
210 _wallet,
211 planet,
212 'own',
213 'manage',
214 1,
215 inviteWallet.management.keys.address
216 );
218 const transferRequest = transferPointRequest(
219 api,
220 _wallet,
221 planet,
222 'own',
223 2,
224 inviteWallet.ownership.keys.address
225 );
227 requests.push(
228 spawnRequest,
229 configureKeysRequest,
230 setManagementProxyRequest,
231 transferRequest
232 );
233 await Promise.all(requests);
235 invites.push({
236 hash: '',
237 ticket,
238 planet,
239 owner: inviteWallet.ownership.keys.address,
240 });
241 }
243 setStoredInvites(ls, invites);
244 },
245 [
246 api,
247 authToken,
248 contracts,
249 pointCursor,
250 wallet,
251 web3,
252 getDetails,
253 ls,
254 setInviteGeneratingNum,
255 ]
256 );
258 const getPoints = useCallback(
259 async (proxy: Proxy, address: EthAddress) => {
260 const points: Ship[] =
261 proxy === 'own'
262 ? await api.getOwnedPoints(address)
263 : proxy === 'manage'
264 ? await api.getManagerFor(address)
265 : proxy === 'vote'
266 ? await api.getVotingFor(address)
267 : proxy === 'transfer'
268 ? await api.getTransferringFor(address)
269 : proxy === 'spawn'
270 ? await api.getSpawningFor(address)
271 : [];
273 return points;
274 },
275 [api]
276 );
278 const getPendingTransactions = useCallback(async () => {
279 try {
280 const curPoint = Number(need.point(pointCursor));
281 const newPending = await api.getPendingByShip(curPoint);
283 setPendingTransactions(newPending);
284 } catch (error) {
285 console.warn('ERROR GETTING PENDING', error);
286 }
287 }, [api, setPendingTransactions, pointCursor]);
289 const generateInviteInfo = async (planet: number, _authToken: string) => {
290 const { ticket, inviteWallet } = await generateInviteWallet(
291 planet,
292 _authToken
293 );
295 return inviteTemplate(planet, ticket, inviteWallet.ownership.keys.address);
296 };
298 const getInvites = useCallback(
299 async (isL2: boolean, getAll = false) => {
300 try {
301 const curPoint: number = Number(need.point(pointCursor));
302 setInvites(invitePoints.map(p => inviteTemplate(p)));
304 const _authToken = authToken.getOrElse(null);
305 const _contracts = contracts.getOrElse(null);
307 if (_authToken && _contracts) {
308 const spawnedPoints = isL2 ? await api.getSpawned(curPoint) : [];
310 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
311 _contracts,
312 curPoint
313 );
315 const outgoingPoints = getOutgoingPoints(
316 controlledPoints,
317 getDetails
318 ).filter((p: number) => isPlanet(p) && availablePoints.includes(p));
320 const invitePlanets = spawnedPoints.concat(
321 outgoingPoints.filter((p: number) => !spawnedPoints.includes(p))
322 );
324 if (isDevelopment) {
325 console.log('INVITED PLANETS', invitePlanets);
326 }
328 const newInvites: Invite[] = [];
329 // Iterate over all of the stored invites, generating wallet info as necessary
330 const storedInvites = getStoredInvites(ls);
332 for (let i = 0; i < invitePlanets.length; i++) {
333 setInviteGeneratingNum(i + 1);
334 const planet = invitePlanets[i];
335 const invite = storedInvites[planet];
337 if (invite?.ticket) {
338 newInvites.push(invite);
339 } else if (getAll) {
340 if (isDevelopment) {
341 console.log('MISSING INVITE INFO', planet);
342 }
343 const newInvite = await generateInviteInfo(planet, _authToken);
344 newInvites.push(newInvite);
345 } else {
346 newInvites.push(inviteTemplate(planet));
347 }
348 }
350 setStoredInvites(ls, newInvites);
351 setInvites(newInvites);
353 return newInvites;
354 }
355 } catch (error) {
356 console.warn('ERROR GETTING INVITES', error);
357 }
358 },
359 [
360 api,
361 authToken,
362 contracts,
363 controlledPoints,
364 getDetails,
365 pointCursor,
366 invitePoints,
367 ls,
368 setInvites,
369 setInviteGeneratingNum,
370 ]
371 );
373 const showInvite = useCallback(
374 async (planet: number) => {
375 const _authToken = authToken.getOrElse(null);
377 if (_authToken) {
378 const newInvite = await generateInviteInfo(planet, _authToken);
379 const newInvites = invites.map(invite =>
380 invite.planet === planet ? newInvite : invite
381 );
382 setInvites(newInvites);
383 setStoredInvites(ls, newInvites);
384 }
385 },
386 [authToken, invites, ls, setInvites]
387 );
389 const getNumInvites = useCallback(
390 async (isL2: boolean) => {
391 try {
392 const curPoint: number = Number(need.point(pointCursor));
393 const _authToken = authToken.getOrElse(null);
394 const _contracts = contracts.getOrElse(null);
396 if (_authToken && _contracts) {
397 let invitePoints = isL2 ? await api.getSpawned(curPoint) : [];
399 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
400 _contracts,
401 curPoint
402 );
404 const newPending = await api.getPendingByShip(curPoint);
405 const pendingSpawns = getPendingSpawns(newPending);
407 const outgoingPoints = getOutgoingPoints(
408 controlledPoints,
409 getDetails
410 ).filter((p: number) => isPlanet(p) && availablePoints.includes(p));
412 setPendingTransactions(newPending);
413 setInvitePoints(
414 invitePoints
415 .concat(
416 outgoingPoints.filter((p: number) => !invitePoints.includes(p))
417 )
418 .filter((p: number) => !pendingSpawns.has(p))
419 );
420 }
421 } catch (e) {
422 if (isDevelopment) {
423 console.warn(e);
424 }
425 }
426 },
427 [
428 api,
429 pointCursor,
430 authToken,
431 contracts,
432 controlledPoints,
433 getDetails,
434 setInvitePoints,
435 setPendingTransactions,
436 ]
437 );
439 interface ConfigureKeysParams {
440 breach: boolean;
441 customNetworkSeed?: string;
442 }
444 const configureNetworkingKeys = async ({
445 breach,
446 customNetworkSeed,
447 }: ConfigureKeysParams) => {
448 const _point = need.point(pointCursor);
449 const azimuthPoint = await api.getPoint(_point);
450 const _wallet = wallet.getOrElse(null);
451 const _details = getDetails(_point);
452 if (!_wallet || !_details) {
453 // not using need because we want a custom error
454 throw new Error('Internal Error: Missing Wallet/Details');
455 }
457 const proxy = getManagerProxy(azimuthPoint.ownership!, _wallet.address);
459 if (proxy === undefined)
460 throw new Error("Error: Address doesn't match proxy");
462 const nonce = await api.getNonce({
463 ship: _point,
464 proxy,
465 });
467 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
468 const nextRevision = networkRevision + 1;
469 const networkSeed = customNetworkSeed
470 ? customNetworkSeed
471 : await attemptNetworkSeedDerivation({
472 urbitWallet,
473 wallet,
474 authMnemonic,
475 details: _details,
476 point: _point,
477 authToken,
478 revision: nextRevision,
479 });
480 const txHash = await configureKeys(
481 api,
482 _wallet,
483 _point,
484 proxy!,
485 nonce,
486 networkSeed,
487 breach
488 );
489 const pendingTx = await api.getPendingTx(txHash);
491 return pendingTx;
492 };
494 interface ReticketParams {
495 point: Ship;
496 to: EthAddress;
497 manager: EthAddress;
498 fromWallet: any; // TODO: wallet type
499 toWallet: any; // TODO: wallet type
500 }
502 const performL2Reticket = async ({
503 point,
504 to,
505 manager,
506 fromWallet,
507 toWallet,
508 }: ReticketParams) => {
509 const azimuthPoint = await api.getPoint(point);
510 const proxy = 'own';
511 let nonce = await api.getNonce({ ship: point, proxy });
513 let requests = [];
515 // 1. Update networking keys
516 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
517 const nextRevision = networkRevision + 1;
518 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
519 toWallet,
520 nextRevision
521 );
522 const configureKeysRequest = await configureKeys(
523 api,
524 fromWallet,
525 point,
526 proxy,
527 nonce,
528 networkSeed
529 );
530 nonce = nonce + 1;
531 requests.push(configureKeysRequest);
533 // 2. Set Management Proxy
534 const registerMgmtRequest = await registerProxyAddress(
535 api,
536 fromWallet,
537 point,
538 proxy,
539 'manage',
540 nonce,
541 manager
542 );
543 nonce = nonce + 1;
544 requests.push(registerMgmtRequest);
546 // 3. Set Spawn Proxy
547 if (!isPlanet(Number(point))) {
548 const registerSpawnRequest = await registerProxyAddress(
549 api,
550 fromWallet,
551 point,
552 proxy,
553 'spawn',
554 nonce,
555 toWallet.spawn.keys.address
556 );
557 nonce = nonce + 1;
558 requests.push(registerSpawnRequest);
559 }
560 // 4. Transfer point
561 const transferTxRequest = await transferPointRequest(
562 api,
563 fromWallet,
564 point,
565 proxy,
566 nonce,
567 to
568 );
569 requests.push(transferTxRequest);
571 const hashes = await Promise.all(requests);
572 return hashes;
573 };
575 const setProxyAddress = useCallback(
576 async (proxyType: Proxy, address: EthAddress) => {
577 const _point = need.point(pointCursor);
578 const _wallet = wallet.getOrElse(null);
580 if (!_wallet) {
581 // not using need because we want a custom error
582 throw new Error('Internal Error: Missing Wallet');
583 }
585 const pointDetails = await api.getPoint(_point);
586 const ownership = pointDetails.ownership!;
587 const proxy = getAddressProxy(ownership, _wallet.address, proxyType);
589 if (proxy === undefined)
590 throw new Error("Error: Address doesn't match expected proxy");
592 const nonce = await api.getNonce({ ship: _point, proxy });
594 const txHash = await registerProxyAddress(
595 api,
596 _wallet,
597 _point,
598 proxy,
599 proxyType,
600 nonce,
601 address
602 );
604 const pendingTx = await api.getPendingTx(txHash);
606 return pendingTx;
607 },
608 [api, pointCursor, wallet]
609 );
611 const transferPoint = useCallback(
612 async (address: EthAddress, reset?: boolean) => {
613 const _point = need.point(pointCursor);
614 const _wallet = wallet.getOrElse(null);
616 if (!_wallet) {
617 // not using need because we want a custom error
618 throw new Error('Internal Error: Missing Wallet');
619 }
621 const pointDetails = await api.getPoint(_point);
622 const proxy = getTransferProxy(pointDetails.ownership!, _wallet.address);
624 if (proxy === undefined)
625 throw new Error("Error: Address doesn't match proxy");
627 const nonce = await api.getNonce({ ship: _point, proxy });
629 const txHash = await transferPointRequest(
630 api,
631 _wallet,
632 _point,
633 proxy,
634 nonce,
635 address,
636 reset || false
637 );
638 const pendingTx = await api.getPendingTx(txHash);
640 return pendingTx;
641 },
642 [api, pointCursor, wallet]
643 );
645 // On load, get initial config
646 useEffect(() => {
647 if (config) {
648 return;
649 }
651 fetchConfig();
652 }, [config, fetchConfig]);
654 useEffect(() => {
655 const interval = setInterval(() => {
656 const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
657 setNextRoll(nextRoll);
659 if (nextBatchTime - ONE_SECOND <= new Date().getTime()) {
660 api.getRollerConfig().then(response => {
661 setNextBatchTime(response.nextBatch);
662 getNumInvites(currentL2);
663 });
664 }
665 }, 1000);
667 return () => clearInterval(interval);
668 }, [nextBatchTime, getPendingTransactions, currentL2]); // eslint-disable-line react-hooks/exhaustive-deps
670 return {
671 api,
672 performL2Reticket,
673 config,
674 getPoints,
675 getInvites,
676 getPendingTransactions,
677 generateInviteCodes,
678 transferPoint,
679 setProxyAddress,
680 configureNetworkingKeys,
681 getNumInvites,
682 showInvite,
683 ls,
684 };