on d2d22cf help sign in
bridge/src/lib/useRoller.ts
673 lines 17 kB history raw
1import { useCallback, useEffect, useMemo, useState } from 'react';
2import _ from 'lodash';
3import * as azimuth from 'azimuth-js';
4import * as need from 'lib/need';
5import { Just } from 'folktale/maybe';
7import { Invite } from 'lib/types/Invite';
8import { convertToInt } from './convertToInt';
9import { isDevelopment, isRopsten } from './flags';
10import { hasInvite, generateInviteWallet } from './utils/roller';
11import { INVITES_PER_PAGE, ROLLER_HOSTS } from './constants';
13import { useNetwork } from 'store/network';
14import { usePointCursor } from 'store/pointCursor';
15import { useRollerStore } from 'store/roller';
16import { useWallet } from 'store/wallet';
17import { getStoredInvites, setStoredInvites } from 'store/storage/roller';
18import { usePointCache } from 'store/pointCache';
19import { getOutgoingPoints } from 'views/Points';
20import {
21 getSpawnProxy,
22 getManagerProxy,
23 getAddressProxy,
24 getTransferProxy,
25} from './utils/proxy';
26import { isPlanet } from './utils/point';
28import {
29 deriveNetworkSeedFromUrbitWallet,
30 attemptNetworkSeedDerivation,
31} from 'lib/keys';
33import {
34 Config,
35 Ship,
36 Proxy,
37 RollerRPCAPI,
38 Options,
39 EthAddress,
40 UnspawnedPoints,
41 SpawnParams,
42 L2Data,
43} from '@urbit/roller-api';
45import {
46 configureKeys,
47 getTimeToNextBatch,
48 spawn,
49 transferPointRequest,
50 registerProxyAddress,
51} from './utils/roller';
53function isSpawn(tx: L2Data | undefined): tx is SpawnParams {
54 return (tx as SpawnParams) !== undefined;
57function isShipNumber(ship: number | string | undefined): ship is number {
58 return (ship as number) !== undefined;
61const inviteTemplate = (
62 planet: number,
63 ticket = '',
64 owner = '',
65 hash = ''
66): Invite => ({
67 planet,
68 ticket,
69 owner,
70 hash,
71});
73export default function useRoller() {
74 const { wallet, authToken, authMnemonic, urbitWallet }: any = useWallet();
75 const { pointCursor }: any = usePointCursor();
76 const { web3, contracts }: any = useNetwork();
77 const allPoints: any = usePointCache();
78 const controlledPoints = allPoints?.controlledPoints;
79 const getDetails = allPoints?.getDetails;
81 const {
82 nextBatchTime,
83 currentL2,
84 invitePoints,
85 setNextBatchTime,
86 setNextRoll,
87 setPendingTransactions,
88 setInvites,
89 setInvitePoints,
90 setInviteGeneratingNum,
91 } = useRollerStore();
92 const [config, setConfig] = useState<Config | null>(null);
94 const options: Options = useMemo(() => {
95 const type = isRopsten || !isDevelopment ? 'https' : 'http';
96 const host = isRopsten
97 ? ROLLER_HOSTS.ROPSTEN
98 : isDevelopment
99 ? ROLLER_HOSTS.LOCAL
100 : ROLLER_HOSTS.MAINNET;
101 const port = isRopsten ? 443 : 8080;
102 const path = '/v1/roller';
104 return {
105 transport: {
106 type,
107 host,
108 port,
109 path,
110 },
111 };
112 }, []);
114 const api = useMemo(() => {
115 return new RollerRPCAPI(options);
116 }, [options]);
118 const fetchConfig = useCallback(async () => {
119 api
120 .getRollerConfig()
121 .then(response => {
122 setConfig(response);
123 setNextBatchTime(response.nextBatch);
124 })
125 .catch(err => {
126 // TODO: more elegant error handling
127 console.warn(
128 '[fetchConfig:failed] check the roller connection?\n',
129 err
130 );
131 });
132 }, [api]); // eslint-disable-line react-hooks/exhaustive-deps
134 const generateInviteCodes = useCallback(
135 async (numInvites: number) => {
136 const _point = need.point(pointCursor);
137 const _contracts = contracts.getOrElse(null);
138 const _web3 = web3.getOrElse(null);
139 const _wallet = wallet.getOrElse(null);
140 const _authToken = authToken.getOrElse(null);
141 const _details = getDetails(_point);
143 if (!_contracts || !_web3 || !_wallet || !_authToken || !_details) {
144 // not using need because we want a custom error
145 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
146 }
148 if (Just.hasInstance(authMnemonic)) {
149 throw new Error(
150 "Auth key Error: A management mnemonic can't create invite codes"
151 );
152 }
153 let planets: UnspawnedPoints = await api.getUnspawned(_point);
155 const pendingTxs = await api.getPendingByShip(_point);
157 const pendingSpawns = _.reduce(
158 pendingTxs,
159 (acc, pending) => {
160 return isSpawn(pending.rawTx?.tx) &&
161 isShipNumber(pending.rawTx?.tx.data.ship)
162 ? acc.add(pending.rawTx?.tx.data.ship!)
163 : acc;
164 },
165 new Set<number>()
166 );
168 planets = planets.filter((point: number) => !pendingSpawns.has(point));
170 const starInfo = await api.getPoint(_point);
171 const tickets: {
172 ticket: string;
173 planet: number;
174 owner: string;
175 }[] = [];
176 const requests: Promise<string>[] = [];
178 const proxy = getSpawnProxy(starInfo.ownership!, _wallet.address);
180 if (proxy === undefined)
181 throw new Error("Error: Address doesn't match proxy");
183 const nonce = await api.getNonce({
184 ship: _point,
185 proxy,
186 });
188 for (let i = 0; i < numInvites && planets[i]; i++) {
189 setInviteGeneratingNum(i + 1);
190 const planet = planets[i];
191 const nonceInc = i + nonce;
193 const { ticket, inviteWallet } = await generateInviteWallet(
194 planet,
195 _authToken
196 );
198 const spawnRequest = spawn(
199 api,
200 _wallet,
201 _point,
202 proxy,
203 nonceInc,
204 planet
205 );
207 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
208 inviteWallet,
209 1
210 );
211 const configureKeysRequest = configureKeys(
212 api,
213 _wallet,
214 planet,
215 'own',
216 0,
217 networkSeed
218 );
220 const setManagementProxyRequest = registerProxyAddress(
221 api,
222 _wallet,
223 planet,
224 'own',
225 'manage',
226 1,
227 inviteWallet.management.keys.address
228 );
230 const transferRequest = transferPointRequest(
231 api,
232 _wallet,
233 planet,
234 'own',
235 2,
236 inviteWallet.ownership.keys.address
237 );
239 requests.push(
240 spawnRequest,
241 configureKeysRequest,
242 setManagementProxyRequest,
243 transferRequest
244 );
245 await Promise.all(requests);
247 tickets.push({
248 ticket,
249 planet,
250 owner: inviteWallet.ownership.keys.address,
251 });
252 }
253 },
254 [
255 api,
256 authToken,
257 contracts,
258 pointCursor,
259 wallet,
260 web3,
261 getDetails,
262 authMnemonic,
263 setInviteGeneratingNum,
264 ]
265 );
267 const getPoints = useCallback(
268 async (proxy: Proxy, address: EthAddress) => {
269 const points: Ship[] =
270 proxy === 'own'
271 ? await api.getOwnedPoints(address)
272 : proxy === 'manage'
273 ? await api.getManagerFor(address)
274 : proxy === 'vote'
275 ? await api.getVotingFor(address)
276 : proxy === 'transfer'
277 ? await api.getTransferringFor(address)
278 : proxy === 'spawn'
279 ? await api.getSpawningFor(address)
280 : [];
282 return points;
283 },
284 [api]
285 );
287 const getPendingTransactions = useCallback(async () => {
288 try {
289 const curPoint = Number(need.point(pointCursor));
290 const newPending = await api.getPendingByShip(curPoint);
292 setPendingTransactions(newPending);
294 // const allTransactions = await api.getHistory()
295 } catch (error) {
296 console.warn('ERROR GETTING PENDING', error);
297 }
298 }, [api, setPendingTransactions, pointCursor]);
300 const getInvites = useCallback(
301 async (isL2: boolean, page: number, getAll = false) => {
302 try {
303 const curPoint: number = Number(need.point(pointCursor));
304 const storedInvites = getStoredInvites(curPoint);
305 if (storedInvites.length) {
306 setInvites(storedInvites);
307 } else {
308 setInvites(invitePoints.map(p => inviteTemplate(p)));
309 }
311 const _authToken = authToken.getOrElse(null);
312 const _contracts = contracts.getOrElse(null);
314 if (_authToken && _contracts) {
315 const spawnedPoints = isL2 ? await api.getSpawned(curPoint) : [];
317 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
318 _contracts,
319 curPoint
320 );
322 const outgoingPoints = getOutgoingPoints(
323 controlledPoints,
324 getDetails
325 ).filter((p: number) => isPlanet(p) && availablePoints.includes(p));
327 const invitePlanets = spawnedPoints.concat(outgoingPoints);
329 if (isDevelopment) {
330 console.log('INVITED PLANETS', invitePlanets);
331 }
333 const newInvites: Invite[] = [];
334 // Iterate over all of the stored invites, generating wallet info as necessary
335 for (let i = 0; i < invitePlanets.length; i++) {
336 setInviteGeneratingNum(i + 1);
337 const planet = invitePlanets[i];
338 const invite = storedInvites.find(hasInvite(planet));
340 const start = page * INVITES_PER_PAGE;
341 const end = start + INVITES_PER_PAGE;
343 if (invite) {
344 newInvites.push(invite);
345 } else if (getAll || (i >= start && i < end)) {
346 if (isDevelopment) {
347 console.log('MISSING INVITE INFO', planet);
348 }
349 const { ticket, inviteWallet } = await generateInviteWallet(
350 planet,
351 _authToken
352 );
354 newInvites.push(
355 inviteTemplate(
356 planet,
357 ticket,
358 inviteWallet.ownership.keys.address
359 )
360 );
361 } else {
362 newInvites.push(inviteTemplate(planet));
363 }
364 }
366 setStoredInvites(curPoint, newInvites);
367 setInvites(newInvites);
369 return newInvites;
370 }
371 } catch (error) {
372 console.warn('ERROR GETTING INVITES', error);
373 }
374 },
375 [
376 api,
377 authToken,
378 contracts,
379 controlledPoints,
380 getDetails,
381 pointCursor,
382 invitePoints,
383 setInvites,
384 setInviteGeneratingNum,
385 ]
386 );
388 const getNumInvites = useCallback(
389 async (isL2: boolean) => {
390 try {
391 const curPoint: number = Number(need.point(pointCursor));
392 const _authToken = authToken.getOrElse(null);
393 const _contracts = contracts.getOrElse(null);
395 if (_authToken && _contracts) {
396 let invitePoints = isL2 ? await api.getSpawned(curPoint) : [];
398 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
399 _contracts,
400 curPoint
401 );
403 const outgoingPoints = getOutgoingPoints(
404 controlledPoints,
405 getDetails
406 ).filter((p: number) => isPlanet(p) && availablePoints.includes(p));
408 setInvitePoints(invitePoints.concat(outgoingPoints));
409 }
410 } catch (e) {
411 if (isDevelopment) {
412 console.warn(e);
413 }
414 }
415 },
416 [
417 api,
418 pointCursor,
419 setInvitePoints,
420 authToken,
421 contracts,
422 controlledPoints,
423 getDetails,
424 ]
425 );
427 interface ConfigureKeysParams {
428 breach: boolean;
429 customNetworkSeed?: string;
430 }
432 const configureNetworkingKeys = async ({
433 breach,
434 customNetworkSeed,
435 }: ConfigureKeysParams) => {
436 const _point = need.point(pointCursor);
437 const azimuthPoint = await api.getPoint(_point);
438 const _wallet = wallet.getOrElse(null);
439 const _details = getDetails(_point);
440 if (!_wallet || !_details) {
441 // not using need because we want a custom error
442 throw new Error('Internal Error: Missing Wallet/Details');
443 }
445 const pointDetails = await api.getPoint(_point);
446 const proxy = getManagerProxy(pointDetails.ownership!, _wallet.address);
448 if (proxy === undefined)
449 throw new Error("Error: Address doesn't match proxy");
451 const nonce = await api.getNonce({
452 ship: _point,
453 proxy,
454 });
456 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
457 const nextRevision = networkRevision + 1;
458 const networkSeed = customNetworkSeed
459 ? customNetworkSeed
460 : await attemptNetworkSeedDerivation({
461 urbitWallet,
462 wallet,
463 authMnemonic,
464 details: _details,
465 point: _point,
466 authToken,
467 revision: nextRevision,
468 });
469 const txHash = await configureKeys(
470 api,
471 _wallet,
472 _point,
473 proxy!,
474 nonce,
475 networkSeed,
476 breach
477 );
478 const pendingTx = await api.getPendingTx(txHash);
480 return pendingTx;
481 };
483 interface ReticketParams {
484 point: Ship;
485 to: EthAddress;
486 manager: EthAddress;
487 fromWallet: any; // TODO: wallet type
488 toWallet: any; // TODO: wallet type
489 }
491 const performL2Reticket = async ({
492 point,
493 to,
494 manager,
495 fromWallet,
496 toWallet,
497 }: ReticketParams) => {
498 const azimuthPoint = await api.getPoint(point);
499 const proxy = 'own';
500 let nonce = await api.getNonce({ ship: point, proxy });
502 let requests = [];
504 // 1. Update networking keys
505 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
506 const nextRevision = networkRevision + 1;
507 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
508 toWallet,
509 nextRevision
510 );
511 const configureKeysRequest = await configureKeys(
512 api,
513 fromWallet,
514 point,
515 proxy,
516 nonce,
517 networkSeed
518 );
519 nonce = nonce + 1;
520 requests.push(configureKeysRequest);
522 // 2. Set Management Proxy
523 const registerMgmtRequest = await registerProxyAddress(
524 api,
525 fromWallet,
526 point,
527 proxy,
528 'manage',
529 nonce,
530 manager
531 );
532 nonce = nonce + 1;
533 requests.push(registerMgmtRequest);
535 // 3. Set Spawn Proxy
536 if (!isPlanet(Number(point))) {
537 const registerSpawnRequest = await registerProxyAddress(
538 api,
539 fromWallet,
540 point,
541 proxy,
542 'spawn',
543 nonce,
544 toWallet.spawn.keys.address
545 );
546 nonce = nonce + 1;
547 requests.push(registerSpawnRequest);
548 }
549 // 4. Transfer point
550 const transferTxRequest = await transferPointRequest(
551 api,
552 fromWallet,
553 point,
554 proxy,
555 nonce,
556 to
557 );
558 requests.push(transferTxRequest);
560 const hashes = await Promise.all(requests);
561 return hashes;
562 };
564 const setProxyAddress = useCallback(
565 async (proxyType: Proxy, address: EthAddress) => {
566 const _point = need.point(pointCursor);
567 const _wallet = wallet.getOrElse(null);
569 if (!_wallet) {
570 // not using need because we want a custom error
571 throw new Error('Internal Error: Missing Wallet');
572 }
574 const pointDetails = await api.getPoint(_point);
575 const ownership = pointDetails.ownership!;
576 const proxy = getAddressProxy(ownership, _wallet.address, proxyType);
578 if (proxy === undefined)
579 throw new Error("Error: Address doesn't match expected proxy");
581 const nonce = await api.getNonce({ ship: _point, proxy });
583 const txHash = await registerProxyAddress(
584 api,
585 _wallet,
586 _point,
587 proxy,
588 proxyType,
589 nonce,
590 address
591 );
593 const pendingTx = await api.getPendingTx(txHash);
595 return pendingTx;
596 },
597 [api, pointCursor, wallet]
598 );
600 const transferPoint = useCallback(
601 async (address: EthAddress, reset?: boolean) => {
602 const _point = need.point(pointCursor);
603 const _wallet = wallet.getOrElse(null);
605 if (!_wallet) {
606 // not using need because we want a custom error
607 throw new Error('Internal Error: Missing Wallet');
608 }
610 const pointDetails = await api.getPoint(_point);
611 const proxy = getTransferProxy(pointDetails.ownership!, _wallet.address);
613 if (proxy === undefined)
614 throw new Error("Error: Address doesn't match proxy");
616 const nonce = await api.getNonce({ ship: _point, proxy });
618 const txHash = await transferPointRequest(
619 api,
620 _wallet,
621 _point,
622 proxy,
623 nonce,
624 address,
625 reset || false
626 );
627 const pendingTx = await api.getPendingTx(txHash);
629 return pendingTx;
630 },
631 [api, pointCursor, wallet]
632 );
634 // On load, get initial config
635 useEffect(() => {
636 if (config) {
637 return;
638 }
640 fetchConfig();
641 }, [config, fetchConfig]);
643 useEffect(() => {
644 const interval = setInterval(() => {
645 const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
646 setNextRoll(nextRoll);
648 if (nextBatchTime <= new Date().getTime()) {
649 api.getRollerConfig().then(response => {
650 setNextBatchTime(response.nextBatch);
651 });
653 getInvites(currentL2, 0);
654 }
655 }, 1000);
657 return () => clearInterval(interval);
658 }, [nextBatchTime, getPendingTransactions, currentL2]); // eslint-disable-line react-hooks/exhaustive-deps
660 return {
661 api,
662 performL2Reticket,
663 config,
664 getPoints,
665 getInvites,
666 getPendingTransactions,
667 generateInviteCodes,
668 transferPoint,
669 setProxyAddress,
670 configureNetworkingKeys,
671 getNumInvites,
672 };