on 2d3487f help sign in
bridge/src/lib/useRoller.ts
649 lines 18 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 * as ob from 'urbit-ob';
6import { Just } from 'folktale/maybe';
8import { Invite } from 'lib/types/Invite';
9import { convertToInt } from './convertToInt';
10import { isDevelopment, isRopsten } from './flags';
11import { hasPoint, generateInviteWallet } from './utils/roller';
12import { ROLLER_HOSTS } from './constants';
14import { useNetwork } from 'store/network';
15import { usePointCursor } from 'store/pointCursor';
16import { useRollerStore } from 'store/roller';
17import { useWallet } from 'store/wallet';
18import {
19 getStoredInvites,
20 setPendingInvites,
21 setStoredInvites,
22} from 'store/storage/roller';
23import { usePointCache } from 'store/pointCache';
24import { getOutgoingPoints } from 'views/Points';
25import {
26 getSpawnNonce,
27 getOwnerNonce,
28 getManagementNonce,
29 getTransferNonce,
30} from './utils/nonce';
31import { isPlanet } from './utils/point';
33import {
34 deriveNetworkSeedFromUrbitWallet,
35 attemptNetworkSeedDerivation,
36} from 'lib/keys';
38import {
39 Config,
40 Ship,
41 Proxy,
42 RollerRPCAPI,
43 Options,
44 EthAddress,
45 UnspawnedPoints,
46 SpawnParams,
47 L2Data,
48} from '@urbit/roller-api';
50import {
51 configureKeys,
52 getTimeToNextBatch,
53 spawn,
54 transferPointRequest,
55 registerProxyAddress,
56} from './utils/roller';
58function isSpawn(tx: L2Data | undefined): tx is SpawnParams {
59 return (tx as SpawnParams) !== undefined;
62function isShipNumber(ship: number | string | undefined): ship is number {
63 return (ship as number) !== undefined;
66export default function useRoller() {
67 const { wallet, authToken, authMnemonic, urbitWallet }: any = useWallet();
68 const { pointCursor }: any = usePointCursor();
69 const { web3, contracts }: any = useNetwork();
70 const allPoints: any = usePointCache();
71 const controlledPoints = allPoints?.controlledPoints;
72 const getDetails = allPoints?.getDetails;
74 const {
75 nextBatchTime,
76 setNextBatchTime,
77 setNextRoll,
78 setPendingTransactions,
79 setInvites,
80 currentL2,
81 nonces,
82 increaseNonce,
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 = isRopsten ? 443 : 8080;
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 generateInviteCodes = useCallback(
127 async (numInvites: number) => {
128 const _point = need.point(pointCursor);
129 const _contracts = contracts.getOrElse(null);
130 const _web3 = web3.getOrElse(null);
131 const _wallet = wallet.getOrElse(null);
132 const _authToken = authToken.getOrElse(null);
133 const _details = getDetails(_point);
135 if (!_contracts || !_web3 || !_wallet || !_authToken || !_details) {
136 // not using need because we want a custom error
137 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
138 }
140 if (Just.hasInstance(authMnemonic)) {
141 throw new Error(
142 "Auth key Error: A management mnemonic can't create invite codes"
143 );
144 }
145 let planets: UnspawnedPoints = await api.getUnspawned(_point);
147 const pendingTxs = await api.getPendingByShip(_point);
149 const pendingSpawns = _.reduce(
150 pendingTxs,
151 (acc, pending) => {
152 return isSpawn(pending.rawTx?.tx) &&
153 isShipNumber(pending.rawTx?.tx.data.ship)
154 ? acc.add(pending.rawTx?.tx.data.ship!)
155 : acc;
156 },
157 new Set<number>()
158 );
160 console.log(pendingTxs, pendingSpawns);
162 planets = planets.filter((point: number) => !pendingSpawns.has(point));
164 const starInfo = nonces[_point];
165 console.log(starInfo);
166 const tickets: {
167 ticket: string;
168 planet: number;
169 owner: string;
170 }[] = [];
171 const requests: Promise<string>[] = [];
173 const operator =
174 getSpawnNonce(starInfo, _wallet.address) ||
175 getOwnerNonce(starInfo, _wallet.address);
177 if (operator === undefined)
178 throw new Error("Error: Address doesn't match proxy");
179 console.log('operator', operator);
180 for (let i = 0; i < numInvites && planets[i]; i++) {
181 const planet = planets[i];
182 const nonceInc = i + operator.nonce!;
184 const { ticket, inviteWallet } = await generateInviteWallet(
185 planet,
186 _authToken
187 );
189 const spawnRequest = await spawn(
190 api,
191 _wallet,
192 _point,
193 operator.proxy!,
194 nonceInc,
195 planet
196 );
197 increaseNonce(_point, operator.proxy!);
199 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
200 inviteWallet,
201 1
202 );
203 const configureKeysRequest = await configureKeys(
204 api,
205 _wallet,
206 planet,
207 operator.proxy!,
208 0,
209 networkSeed
210 );
212 const setManagementProxyRequest = await registerProxyAddress(
213 api,
214 _wallet,
215 planet,
216 operator.proxy!,
217 'manage',
218 1,
219 inviteWallet.management.keys.address
220 );
222 const transferRequest = await transferPointRequest(
223 api,
224 _wallet,
225 planet,
226 operator.proxy!,
227 2,
228 inviteWallet.ownership.keys.address
229 );
231 requests.push(
232 spawnRequest,
233 configureKeysRequest,
234 setManagementProxyRequest,
235 transferRequest
236 );
237 tickets.push({
238 ticket,
239 planet,
240 owner: inviteWallet.ownership.keys.address,
241 });
242 }
244 const hashes = await Promise.all(requests);
246 const pendingInvites: Invite[] = tickets.map((ticket, ind) => ({
247 ...ticket,
248 hash: hashes[ind * 3 + 2],
249 status: 'pending',
250 }));
252 console.log('latest update', nonces[_point]);
253 setPendingInvites(_point, pendingInvites);
254 },
255 [
256 api,
257 authToken,
258 contracts,
259 pointCursor,
260 wallet,
261 web3,
262 getDetails,
263 authMnemonic,
264 nonces,
265 increaseNonce,
266 ]
267 );
269 const getPoints = useCallback(
270 async (proxy: Proxy, address: EthAddress) => {
271 const points: Ship[] =
272 proxy === 'own'
273 ? await api.getOwnedPoints(address)
274 : proxy === 'manage'
275 ? await api.getManagerFor(address)
276 : proxy === 'vote'
277 ? await api.getVotingFor(address)
278 : proxy === 'transfer'
279 ? await api.getTransferringFor(address)
280 : proxy === 'spawn'
281 ? await api.getSpawningFor(address)
282 : [];
284 return points;
285 },
286 [api]
287 );
289 const getPendingTransactions = useCallback(async () => {
290 try {
291 const curPoint = Number(need.point(pointCursor));
292 const newPending = await api.getPendingByShip(curPoint);
293 setPendingTransactions(newPending);
295 // const allTransactions = await api.getHistory()
296 } catch (error) {
297 console.warn('ERROR GETTING PENDING', error);
298 }
299 }, [api, setPendingTransactions, pointCursor]);
301 const getInvites = useCallback(
302 async (isL2: boolean) => {
303 try {
304 const curPoint: number = Number(need.point(pointCursor));
305 const invites = getStoredInvites(curPoint);
306 const availableInvites = invites.available;
307 const pendingTransactions = await api.getPendingByShip(curPoint);
308 if (isDevelopment) {
309 console.log('PENDING', pendingTransactions);
310 }
311 setPendingTransactions(pendingTransactions);
313 const stillPending = invites.pending.filter(invite => {
314 const completed = !pendingTransactions.find(
315 p => `~${p?.rawTx?.tx?.data?.ship}` === ob.patp(invite.planet)
316 );
318 if (
319 completed &&
320 !availableInvites.find(({ planet }) => invite.planet === planet)
321 ) {
322 availableInvites.push({ ...invite, status: 'available' });
323 }
325 return !completed;
326 });
328 setStoredInvites(curPoint, {
329 available: availableInvites,
330 pending: stillPending,
331 });
332 setInvites(availableInvites);
334 const _authToken = authToken.getOrElse(null);
335 const _contracts = contracts.getOrElse(null);
337 if (_authToken && _contracts) {
338 let possibleMissingInvites: number[] = [];
339 let availablePoints: number[] = [];
340 if (isL2) {
341 possibleMissingInvites = await api.getSpawned(curPoint);
342 availablePoints = await api.getUnspawned(curPoint);
343 } else {
344 // TODO: should it be removed?
345 availablePoints = await azimuth.azimuth.getUnspawnedChildren(
346 _contracts,
347 curPoint
348 );
349 }
351 const outgoingPoints = getOutgoingPoints(
352 controlledPoints,
353 getDetails
354 );
356 possibleMissingInvites = possibleMissingInvites.concat(
357 outgoingPoints.filter(
358 (p: number) => isPlanet(p) && availablePoints.includes(p)
359 )
360 );
362 // Iterate over all spawned and controlled planets
363 // If the planet is not in available invites, generate the ticket and add it
364 if (isDevelopment) {
365 console.log('POSSIBLE MISSING', possibleMissingInvites);
366 }
367 // TODO: this can be verym slow, so best to allow the user to manually
368 // reveal the ticket, after checking if the invite is still "active"
369 // (i.e. if the address inferred from the derived inviteWallet corresponds to
370 // to the address the point has as owner)
371 //
372 for (let i = 0; i < possibleMissingInvites.length; i++) {
373 const planet = possibleMissingInvites[i];
374 if (!availableInvites.find(hasPoint(planet))) {
375 console.log('MISSING IN AVAILABLE', planet);
376 const { ticket, inviteWallet } = await generateInviteWallet(
377 planet,
378 _authToken
379 );
381 availableInvites.push({
382 ticket,
383 status: 'available',
384 planet,
385 hash: '',
386 owner: inviteWallet.ownership.keys.address,
387 });
388 }
389 }
391 setStoredInvites(curPoint, {
392 available: availableInvites.filter(({ planet }) =>
393 possibleMissingInvites.includes(planet)
394 ),
395 pending: stillPending,
396 });
397 setInvites(availableInvites);
398 }
399 } catch (error) {
400 console.warn('ERROR GETTING INVITES', error);
401 }
402 },
403 [
404 api,
405 authToken,
406 contracts,
407 controlledPoints,
408 getDetails,
409 pointCursor,
410 setInvites,
411 setPendingTransactions,
412 ]
413 );
415 interface ConfigureKeysParams {
416 breach: boolean;
417 customNetworkSeed?: string;
418 }
420 const configureNetworkingKeys = async ({
421 breach,
422 customNetworkSeed,
423 }: ConfigureKeysParams) => {
424 const _point = need.point(pointCursor);
425 const azimuthPoint = await api.getPoint(_point);
426 const _wallet = wallet.getOrElse(null);
427 const _details = getDetails(_point);
428 if (!_wallet || !_details) {
429 // not using need because we want a custom error
430 throw new Error('Internal Error: Missing Wallet/Details');
431 }
433 const pointDetails = nonces[_point];
434 const operator =
435 getManagementNonce(pointDetails, _wallet.address) ||
436 getOwnerNonce(pointDetails, _wallet.address);
437 if (operator === undefined)
438 throw new Error("Error: Address doesn't match proxy");
439 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
440 const nextRevision = networkRevision + 1;
441 const networkSeed = customNetworkSeed
442 ? customNetworkSeed
443 : await attemptNetworkSeedDerivation({
444 urbitWallet,
445 wallet,
446 authMnemonic,
447 details: _details,
448 point: _point,
449 authToken,
450 revision: nextRevision,
451 });
452 const txHash = await configureKeys(
453 api,
454 _wallet,
455 _point,
456 operator.proxy!,
457 operator.nonce!,
458 networkSeed,
459 breach
460 );
461 const pendingTx = await api.getPendingTx(txHash);
463 return pendingTx;
464 };
466 interface AcceptInviteParams {
467 point: Ship;
468 to: EthAddress;
469 manager: EthAddress;
470 fromWallet: any; // TODO: wallet type
471 toWallet: any; // TODO: wallet type
472 }
474 const acceptInvite = async ({
475 point,
476 to,
477 manager,
478 fromWallet,
479 toWallet,
480 }: AcceptInviteParams) => {
481 const azimuthPoint = await api.getPoint(point);
482 console.log('azimuthPoint', azimuthPoint);
483 const ownerAddress = azimuthPoint?.ownership?.owner?.address!;
485 const operator =
486 azimuthPoint.ownership &&
487 getOwnerNonce(azimuthPoint.ownership, ownerAddress);
489 if (operator === undefined)
490 throw new Error("Error: Address doesn't match proxy");
492 const nonce = operator.nonce!;
494 // 1. Update networking keys
495 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
496 const nextRevision = networkRevision + 1;
497 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
498 toWallet,
499 nextRevision
500 );
501 const configureKeysRequest = await configureKeys(
502 api,
503 fromWallet,
504 point,
505 operator.proxy!,
506 nonce,
507 networkSeed
508 );
510 // TODO: not needed? next time we operate with this point we will login, and get
511 // the latest nonce (updated with any nonce increase from possible pending txs)
512 // increaseNonce(point, operator.proxy!);
514 // 2. Set Management Proxy
515 const registerProxyAddressRequest = await registerProxyAddress(
516 api,
517 fromWallet,
518 point,
519 operator.proxy!,
520 'manage',
521 nonce + 1,
522 manager
523 );
525 // 3. Transfer point
526 const transferTxRequest = await transferPointRequest(
527 api,
528 fromWallet,
529 point,
530 operator.proxy!,
531 nonce + 2,
532 to
533 );
534 const hashes = await Promise.all([
535 configureKeysRequest,
536 registerProxyAddressRequest,
537 transferTxRequest,
538 ]);
539 return hashes;
540 };
542 const setProxyAddress = useCallback(
543 async (proxyType: Proxy, address: EthAddress) => {
544 const _point = need.point(pointCursor);
545 const _wallet = wallet.getOrElse(null);
547 if (!_wallet) {
548 // not using need because we want a custom error
549 throw new Error('Internal Error: Missing Wallet');
550 }
552 const pointDetails = nonces[_point];
553 const operator =
554 getManagementNonce(pointDetails, _wallet.address) ||
555 getOwnerNonce(pointDetails, _wallet.address);
557 if (operator === undefined)
558 throw new Error("Error: Address doesn't match proxy");
560 const txHash = await registerProxyAddress(
561 api,
562 _wallet,
563 _point,
564 operator.proxy!,
565 proxyType,
566 operator.nonce!,
567 address
568 );
570 const pendingTx = await api.getPendingTx(txHash);
572 return pendingTx;
573 },
574 [api, pointCursor, wallet, nonces]
575 );
577 const transferPoint = useCallback(
578 async (address: EthAddress, reset?: boolean) => {
579 const _point = need.point(pointCursor);
580 const _wallet = wallet.getOrElse(null);
582 if (!_wallet) {
583 // not using need because we want a custom error
584 throw new Error('Internal Error: Missing Wallet');
585 }
587 const pointDetails = nonces[_point];
588 const operator =
589 getTransferNonce(pointDetails, _wallet.address) ||
590 getOwnerNonce(pointDetails, _wallet.address);
592 if (operator === undefined)
593 throw new Error("Error: Address doesn't match proxy");
595 const txHash = await transferPointRequest(
596 api,
597 _wallet,
598 _point,
599 operator.proxy!,
600 operator.nonce!,
601 address,
602 reset || false
603 );
604 const pendingTx = await api.getPendingTx(txHash);
606 return pendingTx;
607 },
608 [api, pointCursor, wallet, nonces]
609 );
611 // On load, get initial config
612 useEffect(() => {
613 if (config) {
614 return;
615 }
617 fetchConfig();
618 }, [config, fetchConfig]);
620 useEffect(() => {
621 const interval = setInterval(() => {
622 const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
623 setNextRoll(nextRoll);
625 if (nextBatchTime <= new Date().getTime()) {
626 api.getRollerConfig().then(response => {
627 setNextBatchTime(response.nextBatch);
628 });
630 getInvites(currentL2);
631 }
632 }, 1000);
634 return () => clearInterval(interval);
635 }, [nextBatchTime, getPendingTransactions, currentL2]); // eslint-disable-line react-hooks/exhaustive-deps
637 return {
638 api,
639 acceptInvite,
640 config,
641 getPoints,
642 getInvites,
643 getPendingTransactions,
644 generateInviteCodes,
645 transferPoint,
646 setProxyAddress,
647 configureNetworkingKeys,
648 };