on 2bca737 help sign in
bridge/src/lib/useRoller.ts
503 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 * as wg from 'lib/walletgen';
7import { convertToInt } from './convertToInt';
8import { ensureHexPrefix } from 'form/formatters';
9import { getOutgoingPoints, maybeGetResult } from 'views/Points';
10import { isDevelopment, isRopsten } from './flags';
11import { isPlanet } from './utils/point';
12import { ROLLER_HOSTS } from './constants';
13import { signTransactionHash } from './authToken';
14import { useNetwork } from 'store/network';
15import { usePointCache } from 'store/pointCache';
16import { usePointCursor } from 'store/pointCursor';
17import { useRollerStore } from 'store/roller';
18import { useWallet } from 'store/wallet';
20import {
21 deriveNetworkKeys,
22 CRYPTO_SUITE_VERSION,
23 deriveNetworkSeedFromUrbitWallet,
24} from 'lib/keys';
26import {
27 Config,
28 Ship,
29 L2Point,
30 Proxy,
31 RollerRPCAPI,
32 Options,
33 EthAddress,
34 UnspawnedPoints,
35 From,
36 Hash,
37} from '@urbit/roller-api';
39import {
40 configureKeys,
41 getTimeToNextBatch,
42 spawn,
43 transferPoint,
44} from './utils/roller';
46import {
47 getStoredInvites,
48 setPendingInvites,
49 setStoredInvites,
50} from 'store/storage/roller';
52const getProxyAndNonce = (
53 point: L2Point,
54 address: string
55): { proxy?: string; nonce?: number } => {
56 return point.ownership?.managementProxy?.address === address
57 ? { proxy: 'manage', nonce: point.ownership?.managementProxy.nonce }
58 : point.ownership?.owner?.address === address
59 ? { proxy: 'own', nonce: point.ownership?.owner.nonce }
60 : point.ownership?.spawnProxy?.address === address
61 ? { proxy: 'spawn', nonce: point.ownership?.spawn?.owner.nonce }
62 : point.ownership?.votingProxy?.address === address
63 ? { proxy: 'vote', nonce: point.ownership?.votingProxy?.owner.nonce }
64 : point.ownership?.transferProxy?.address === address
65 ? {
66 proxy: 'transfer',
67 nonce: point.transferProxy?.votingProxy?.owner.nonce,
68 }
69 : { proxy: undefined, nonce: undefined };
70};
72export default function useRoller() {
73 const { wallet, authToken, authMnemonic, urbitWallet }: any = useWallet();
74 const { pointCursor }: any = usePointCursor();
75 const { web3, contracts }: any = useNetwork();
76 const allPoints: any = usePointCache();
77 const controlledPoints = allPoints?.controlledPoints;
78 const getDetails = allPoints?.getDetails;
80 const {
81 nextBatchTime,
82 setNextBatchTime,
83 setNextRoll,
84 setPendingTransactions,
85 setInvites,
86 currentL2,
87 } = useRollerStore();
88 const [config, setConfig] = useState<Config | null>(null);
90 const options: Options = useMemo(() => {
91 const type = isRopsten || !isDevelopment ? 'https' : 'http';
92 const host = isRopsten
93 ? ROLLER_HOSTS.ROPSTEN
94 : isDevelopment
95 ? ROLLER_HOSTS.LOCAL
96 : ROLLER_HOSTS.MAINNET;
97 const port = isDevelopment ? 8080 : 80;
98 const path = '/v1/roller';
100 return {
101 transport: {
102 type,
103 host,
104 port,
105 path,
106 },
107 };
108 }, []);
110 const api = useMemo(() => {
111 return new RollerRPCAPI(options);
112 }, [options]);
114 const fetchConfig = useCallback(async () => {
115 api
116 .getRollerConfig()
117 .then(response => {
118 setConfig(response);
119 setNextBatchTime(response.nextBatch);
120 })
121 .catch(err => {
122 // TODO: more elegant error handling
123 console.warn(
124 '[fetchConfig:failed] check the roller connection?\n',
125 err
126 );
127 });
128 }, [api]); // eslint-disable-line react-hooks/exhaustive-deps
130 const generateInviteCodes = useCallback(
131 async (numInvites: number) => {
132 const _point = need.point(pointCursor);
133 const _contracts = contracts.getOrElse(null);
134 const _web3 = web3.getOrElse(null);
135 const _wallet = wallet.getOrElse(null);
136 const _authToken = authToken.getOrElse(null);
137 const _details = getDetails(_point);
138 if (!_contracts || !_web3 || !_wallet || !_authToken || !_details) {
139 // not using need because we want a custom error
140 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
141 }
143 const planets: UnspawnedPoints = await api.getUnspawned(_point);
144 const starInfo = await api.getPoint(_point);
146 const tickets: { ticket: string; planet: number; owner: string }[] = [];
147 const requests: Promise<string>[] = [];
149 const { proxy, nonce } = getProxyAndNonce(starInfo, _wallet.address);
151 if (!(proxy === 'own' || proxy === 'spawn') || nonce === undefined)
152 throw new Error("Error: Address doesn't match proxy");
154 for (let i = 0; i < numInvites && planets[i]; i++) {
155 const planet = planets[i];
156 const nonceInc = i + nonce;
158 const { ticket, owner } = await wg.generateTemporaryDeterministicWallet(
159 planet,
160 _authToken
161 );
163 const spawnRequest = await spawn(
164 api,
165 _wallet,
166 _point,
167 proxy,
168 nonceInc,
169 planet
170 );
172 const configureKeysRequest = await configureKeys(
173 api,
174 _wallet,
175 planet,
176 proxy,
177 0, //nonceInc + 1,
178 _details,
179 authToken,
180 authMnemonic,
181 wallet,
182 urbitWallet
183 );
185 const transferPointRequest = await transferPoint(
186 api,
187 _wallet,
188 planet,
189 proxy,
190 1, //nonceInc + 2,
191 owner.keys.address
192 );
194 requests.push(spawnRequest, configureKeysRequest, transferPointRequest);
195 tickets.push({
196 ticket,
197 planet,
198 owner: owner.keys.address,
199 });
200 }
202 const hashes = await Promise.all(requests);
204 const pendingInvites = tickets.map((ticket, ind) => ({
205 ...ticket,
206 hash: hashes[ind * 3 + 2],
207 status: 'pending',
208 }));
210 setPendingInvites(_point, pendingInvites);
211 },
212 [
213 api,
214 authToken,
215 contracts,
216 pointCursor,
217 wallet,
218 // walletHdPath,
219 // walletType,
220 web3,
221 getDetails,
222 authMnemonic,
223 urbitWallet,
224 ]
225 );
227 const getPoints = useCallback(
228 async (proxy: Proxy, address: EthAddress) => {
229 const points: Ship[] =
230 proxy === 'own'
231 ? await api.getOwnedPoints(address)
232 : proxy === 'manage'
233 ? await api.getManagerFor(address)
234 : proxy === 'vote'
235 ? await api.getVotingFor(address)
236 : proxy === 'transfer'
237 ? await api.getTransferringFor(address)
238 : proxy === 'spawn'
239 ? await api.getSpawningFor(address)
240 : [];
242 return points;
243 },
244 [api]
245 );
247 const getPendingTransactions = useCallback(async () => {
248 try {
249 const curPoint = Number(need.point(pointCursor));
250 const newPending = await api.getPendingByShip(curPoint);
251 setPendingTransactions(newPending);
253 // const allTransactions = await api.getHistory()
254 } catch (error) {
255 console.warn('ERROR GETTING PENDING', error);
256 }
257 }, [api, setPendingTransactions, pointCursor]);
259 const getInvites = useCallback(
260 async (isL2: boolean) => {
261 try {
262 const curPoint: number = Number(need.point(pointCursor));
263 const invites = getStoredInvites(curPoint);
264 const availableInvites = invites.available;
266 const pendingTransactions = await api.getPendingByShip(curPoint);
267 if (isDevelopment) {
268 console.log('PENDING', pendingTransactions);
269 }
270 setPendingTransactions(pendingTransactions);
272 const stillPending = invites.pending.filter(invite => {
273 const completed = !pendingTransactions.find(
274 p => `~${p?.rawTx?.tx?.data?.ship}` === ob.patp(invite.planet)
275 );
277 if (
278 completed &&
279 !availableInvites.find(({ ship }) => invite.ship === ship)
280 ) {
281 availableInvites.push({ ...invite, status: 'available' });
282 }
284 return !completed;
285 });
287 setStoredInvites(curPoint, {
288 available: availableInvites,
289 pending: stillPending,
290 });
291 setInvites(availableInvites);
293 const _authToken = authToken.getOrElse(null);
294 const _contracts = contracts.getOrElse(null);
296 if (_authToken && _contracts) {
297 let possibleMissingInvites: number[] = [];
298 if (isL2) {
299 const allSpawned = await api.getSpawned(curPoint);
300 const ownedPoints = maybeGetResult(
301 controlledPoints,
302 'ownedPoints',
303 []
304 );
305 possibleMissingInvites = allSpawned.filter(
306 (p: number) => ownedPoints.includes(p) && isPlanet(p)
307 );
308 }
310 const outgoingPoints = getOutgoingPoints(
311 controlledPoints,
312 getDetails
313 );
315 const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
316 _contracts,
317 curPoint
318 );
320 possibleMissingInvites = possibleMissingInvites.concat(
321 outgoingPoints.filter(
322 (p: number) => isPlanet(p) && availablePoints.includes(p)
323 )
324 );
326 // Iterate over all spawned and controlled planets
327 // If the planet is not in available invites, generate the ticket and add it
328 if (isDevelopment) {
329 console.log('POSSIBLE MISSING', possibleMissingInvites);
330 }
332 for (let i = 0; i < possibleMissingInvites.length; i++) {
333 const planet = possibleMissingInvites[i];
335 if (
336 !availableInvites.find(hasPoint(planet)) &&
337 !(await azimuth.azimuth.isActive(_contracts, planet))
338 ) {
339 console.log('MISSING IN AVAILABLE', planet);
340 const {
341 ticket,
342 owner,
343 } = await wg.generateTemporaryDeterministicWallet(
344 planet,
345 _authToken
346 );
348 availableInvites.push({
349 ticket,
350 status: 'available',
351 planet,
352 hash: '',
353 owner: owner.keys.address,
354 });
355 }
356 }
358 setStoredInvites(curPoint, {
359 available: availableInvites.filter(({ planet }) =>
360 possibleMissingInvites.includes(planet)
361 ),
362 pending: stillPending,
363 });
364 setInvites(availableInvites);
365 }
366 } catch (error) {
367 console.warn('ERROR GETTING INVITES', error);
368 }
369 },
370 [
371 api,
372 pointCursor,
373 setInvites,
374 setPendingTransactions,
375 authToken,
376 contracts,
377 controlledPoints,
378 getDetails,
379 ]
380 );
382 // TODO: extract from #transferPoint
383 // interface ConfigureKeysParams {
385 // }
387 // const configureKeys = async () => {
389 // }
391 interface AcceptInviteParams {
392 point: Ship;
393 to: EthAddress;
394 fromWallet: any; // TODO: wallet type
395 toWallet: any; // TODO: wallet type
396 }
398 const acceptInvite = async ({
399 point,
400 to,
401 fromWallet,
402 toWallet,
403 }: AcceptInviteParams) => {
404 const azimuthPoint = await api.getPoint(point);
405 const spawnerPatp = ob.patp(azimuthPoint?.network?.sponsor?.who);
406 console.log('azimuthPoint', azimuthPoint);
407 const ownerAddress = azimuthPoint?.ownership?.owner?.address!;
409 const { nonce } = getProxyAndNonce(azimuthPoint, ownerAddress);
410 if (nonce === undefined) {
411 throw new Error('Nonce unavailable');
412 }
414 // 1. Update networking keys
415 // TODO: extract this to a useRoller#configureKeys function?
416 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
417 const nextRevision = networkRevision + 1;
418 const fromOwnerProxy: From = {
419 ship: spawnerPatp,
420 proxy: 'own',
421 };
423 const seed = await deriveNetworkSeedFromUrbitWallet(toWallet, nextRevision);
424 const keys = deriveNetworkKeys(seed.value);
425 const keysData = {
426 auth: ensureHexPrefix(keys.auth.public),
427 breach: false,
428 cryptoSuite: CRYPTO_SUITE_VERSION.toString(),
429 encrypt: ensureHexPrefix(keys.crypt.public),
430 };
432 const keysHash: Hash = await api.hashTransaction(
433 nonce,
434 fromOwnerProxy,
435 'configureKeys',
436 keysData
437 );
439 const keysSigningKey = fromWallet.privateKey;
440 await api.configureKeys(
441 signTransactionHash(keysHash, Buffer.from(keysSigningKey, 'hex')),
442 fromOwnerProxy,
443 ownerAddress,
444 keysData
445 );
447 // 2. Transfer point
448 const transferData = { address: ownerAddress, reset: false };
449 const transferHash: Hash = await api.hashTransaction(
450 nonce + 1,
451 fromOwnerProxy,
452 'transferPoint',
453 transferData
454 );
456 const transferSigningKey = toWallet.ownership.keys.private;
457 const transferTxHash = await api.transferPoint(
458 signTransactionHash(transferHash, Buffer.from(transferSigningKey, 'hex')),
459 fromOwnerProxy,
460 to,
461 transferData
462 );
464 return transferTxHash;
465 };
467 // On load, get initial config
468 useEffect(() => {
469 if (config) {
470 return;
471 }
473 fetchConfig();
474 }, [config, fetchConfig]);
476 useEffect(() => {
477 const interval = setInterval(() => {
478 const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
479 setNextRoll(nextRoll);
481 if (nextBatchTime <= new Date().getTime()) {
482 api.getRollerConfig().then(response => {
483 setNextBatchTime(response.nextBatch);
484 });
486 getInvites(currentL2);
487 }
488 }, 1000);
490 return () => clearInterval(interval);
491 }, [nextBatchTime, getPendingTransactions, currentL2]); // eslint-disable-line react-hooks/exhaustive-deps
493 return {
494 api,
495 acceptInvite,
496 config,
497 getPoints,
498 getInvites,
499 getPendingTransactions,
500 generateInviteCodes,
501 transferPoint,
502 };