on master help sign in
star-ops: Sponsor can detach Sponsee
137116ftomholford
src/lib/useRoller.ts+24−1
@@ -6,8 +6,9 @@ import SecureLS from 'secure-ls';
6import { Invite } from 'lib/types/Invite';
6import { Invite } from 'lib/types/Invite';
7import { convertToInt } from './convertToInt';
7import { convertToInt } from './convertToInt';
8import { isDevelopment, isRopsten } from './flags';
8import { isDevelopment, isRopsten } from './flags';
9−import { generateInviteWallet, getPendingSpawns } from './utils/roller';
9+import { detach, generateInviteWallet, getPendingSpawns } from './utils/roller';
10import { ROLLER_HOSTS } from './constants';
10import { ROLLER_HOSTS } from './constants';
11+import { patp2dec } from './patp2dec';
11
12
12import { useNetwork } from 'store/network';
13import { useNetwork } from 'store/network';
13import { usePointCursor } from 'store/pointCursor';
14import { usePointCursor } from 'store/pointCursor';
@@ -636,6 +637,27 @@ export default function useRoller() {
636 [api, pointCursor, wallet, web3, walletType]
637 [api, pointCursor, wallet, web3, walletType]
637 );
638 );
638
639
640+ const kickPoint = useCallback(
641+ async (point: Ship) => {
642+ const sponsor = need.point(pointCursor);
643+ const _wallet = wallet.getOrElse(null);
644+
645+ const sponsorInfo = await api.getPoint(sponsor);
646+ const ownership = sponsorInfo.ownership!;
647+ const proxy = getManagerProxy(ownership, _wallet.address);
648+
649+ if (proxy === undefined)
650+ throw new Error("Error: Address doesn't match expected proxy");
651+
652+ const nonce = await api.getNonce({ ship: sponsor, proxy });
653+ const txHash = await detach(api, _wallet, sponsor, proxy, nonce, point);
654+ const pendingTx = await api.getPendingTx(txHash);
655+
656+ return pendingTx;
657+ },
658+ [api, pointCursor, wallet]
659+ );
660+
639 const transferPoint = useCallback(
661 const transferPoint = useCallback(
640 async (address: EthAddress, reset?: boolean) => {
662 async (address: EthAddress, reset?: boolean) => {
641 const _point = need.point(pointCursor);
663 const _point = need.point(pointCursor);
@@ -706,6 +728,7 @@ export default function useRoller() {
706 getInvites,
728 getInvites,
707 getPendingTransactions,
729 getPendingTransactions,
708 generateInviteCodes,
730 generateInviteCodes,
731+ kickPoint,
709 transferPoint,
732 transferPoint,
710 setProxyAddress,
733 setProxyAddress,
711 configureNetworkingKeys,
734 configureNetworkingKeys,
src/lib/utils/roller.ts+22
@@ -189,6 +189,28 @@ export const transferPointRequest = async (
189 return api.transferPoint(sig, from, wallet.address, data);
189 return api.transferPoint(sig, from, wallet.address, data);
190};
190};
191
191
192+export const detach = async (
193+ api: RollerRPCAPI,
194+ _wallet: any,
195+ sponsor: Ship,
196+ proxy: string,
197+ nonce: number,
198+ ship: Ship
199+) => {
200+ const from = {
201+ ship: sponsor, //ship that is detaching the planet
202+ proxy,
203+ };
204+
205+ const data = {
206+ ship, // ship to detach
207+ };
208+
209+ const hash = await api.getUnsignedTx(nonce, from, 'detach', data);
210+ const sSig = signTransactionHash(hash, _wallet.privateKey);
211+ return api.detach(sSig, from, _wallet.address, data);
212+};
213+
192const proxyType = (proxy: Proxy) => {
214const proxyType = (proxy: Proxy) => {
193 switch (proxy) {
215 switch (proxy) {
194 case 'manage':
216 case 'manage':
src/views/Ops/Requests.scssnew file+75
@@ -0,0 +1,75 @@
1+.ops-requests {
2+ div.content-container {
3+ height: 550px;
4+ overflow-y: scroll;
5+ width: 100%;
6+
7+ // hide the vertical scrollbar
8+ -ms-overflow-style: none; /* IE and Edge */
9+ scrollbar-width: none; /* Firefox */
10+ &::-webkit-scrollbar {
11+ display: none; /* Chrome */
12+ }
13+ }
14+
15+ .loading {
16+ &> svg {
17+ width: 24px;
18+ height: 24px;
19+ }
20+ }
21+
22+ .loading, .no-results {
23+ width: 100%;
24+ min-height: 500px;
25+ display: flex;
26+ flex-flow: column nowrap;
27+ justify-content: center;
28+ align-items: center;
29+ color: rgba(0,0,0,0.6);
30+ }
31+
32+ ul.requests-list {
33+ padding-left: unset;
34+ li {
35+ list-style: none;
36+ margin-bottom: 16px;
37+ height: 24px;
38+ display: flex;
39+ flex-flow: row nowrap;
40+
41+ &:first {
42+ margin-top: unset;
43+ }
44+
45+ div.sigil {
46+ height: 24px;
47+ width: 24px;
48+ margin-right: 8px;
49+ border-radius: 2px;
50+ background: black;
51+
52+ div.sigil-container {
53+ height: 16px;
54+ width: 16px;
55+ padding: 4px;
56+
57+ svg {
58+
59+ }
60+ }
61+
62+ }
63+
64+ .patp {
65+ font-family: Source Code Pro;
66+ font-size: 14px;
67+ font-style: normal;
68+ font-weight: 500;
69+ line-height: 16px;
70+ letter-spacing: 0em;
71+ align-self: center;
72+ }
73+ }
74+ }
75+}
src/views/Ops/Requests.tsx+53−11
@@ -1,5 +1,6 @@
1import * as need from 'lib/need';
1import * as need from 'lib/need';
2+import ob from 'urbit-ob';
2−import { Row } from '@tlon/indigo-react';
3+import { Box, LoadingSpinner, Row } from '@tlon/indigo-react';
3
4
4import Window from 'components/L2/Window/Window';
5import Window from 'components/L2/Window/Window';
5import HeaderPane from 'components/L2/Window/HeaderPane';
6import HeaderPane from 'components/L2/Window/HeaderPane';
@@ -8,25 +9,40 @@ import { useLocalRouter } from 'lib/LocalRouter';
8import View from 'components/View';
9import View from 'components/View';
9import L2BackHeader from 'components/L2/Headers/L2BackHeader';
10import L2BackHeader from 'components/L2/Headers/L2BackHeader';
10import { usePointCursor } from 'store/pointCursor';
11import { usePointCursor } from 'store/pointCursor';
11−import { usePointCache } from 'store/pointCache';
12−import { useEffect } from 'react';
12+import { useCallback, useEffect, useState } from 'react';
13+
14+import './Requests.scss';
15+import Sigil from 'components/Sigil';
16+import useRoller from 'lib/useRoller';
17+import { Ship } from '@urbit/roller-api';
13
18
14export const Requests = () => {
19export const Requests = () => {
15 const { pop }: any = useLocalRouter();
20 const { pop }: any = useLocalRouter();
16 const { pointCursor } = usePointCursor();
21 const { pointCursor } = usePointCursor();
17 const point = need.point(pointCursor);
22 const point = need.point(pointCursor);
23+ const { api } = useRoller();
24+ const [requestingPoints, setRequestingPoints] = useState<Ship[]>([]);
25+ const [loading, setLoading] = useState<boolean>(false);
26+
27+ const fetchRequests = useCallback(async () => {
28+ if (!point) {
29+ return;
30+ }
31+
32+ setLoading(true);
33+ const { requests } = await api.getSponsoredPoints(point);
18
34
19− const { getResidents, syncResidents } = usePointCache();
35+ setRequestingPoints((requests || []).sort());
20− const { requestCount } = getResidents(point);
36+ setLoading(false);
37+ }, [api, point]);
21
38
22 useEffect(() => {
39 useEffect(() => {
23− console.log('sync');
40+ fetchRequests();
24− syncResidents(point);
41+ }, [fetchRequests]);
25− }, [point, syncResidents]);
26
42
27 return (
43 return (
28 <View
44 <View
29− className="ops-residents"
45+ className="ops-requests"
30 pop={pop}
46 pop={pop}
31 inset
47 inset
32 hideBack
48 hideBack
@@ -36,12 +52,38 @@ export const Requests = () => {
36 <Row className="header-row">
52 <Row className="header-row">
37 <h5>
53 <h5>
38 Requests{' '}
54 Requests{' '}
39− <span className="header-count">{requestCount?.value || 0}</span>
55+ <span className="header-count">{requestingPoints.length}</span>
40 </h5>
56 </h5>
41 </Row>
57 </Row>
42 </HeaderPane>
58 </HeaderPane>
43 <BodyPane>
59 <BodyPane>
44− <p>Requests go here</p>
60+ <Box className="content-container">
61+ {loading ? (
62+ <Box className={'loading'}>
63+ <LoadingSpinner />
64+ </Box>
65+ ) : requestingPoints.length > 0 ? (
66+ <ul className="requests-list">
67+ {requestingPoints.map(sp => (
68+ <li>
69+ <Box className="sigil">
70+ <Box className="sigil-container">
71+ <Sigil
72+ icon
73+ patp={ob.patp(sp)}
74+ size={16}
75+ colors={['#000000', '#FFFFFF']}
76+ />
77+ </Box>
78+ </Box>
79+ <Box className={'patp'}>{ob.patp(sp)}</Box>
80+ </li>
81+ ))}
82+ </ul>
83+ ) : (
84+ <Box className="no-results">No requests</Box>
85+ )}
86+ </Box>
45 </BodyPane>
87 </BodyPane>
46 </Window>
88 </Window>
47 </View>
89 </View>
src/views/Ops/ResidentRow.tsxnew file+47
@@ -0,0 +1,47 @@
1+import ob from 'urbit-ob';
2+import { Box, Icon, Text} from '@tlon/indigo-react';
3+import { Ship } from '@urbit/roller-api';
4+import Sigil from 'components/Sigil';
5+import { useCallback, useState } from 'react';
6+
7+interface ResidentRowProps {
8+ point: Ship;
9+ onKick: (point: Ship) => Promise<void>;
10+}
11+
12+export const ResidentRow = ({ point, onKick }: ResidentRowProps) => {
13+ const [showDropdown, setShowDropdown] = useState<boolean>(false);
14+
15+ const handleKickClick = useCallback(async () => {
16+ setShowDropdown(false);
17+ await onKick(point);
18+ }, [onKick, point]);
19+
20+ return (
21+ <li key={point}>
22+ <Box className={'point-wrapper'}>
23+ <Box className="sigil">
24+ <Box className="sigil-container">
25+ <Sigil
26+ icon
27+ patp={ob.patp(point)}
28+ size={16}
29+ colors={['#000000', '#FFFFFF']}
30+ />
31+ </Box>
32+ </Box>
33+ <Box className={'patp'}>{ob.patp(point)}</Box>
34+ </Box>
35+ <Box
36+ className="button-wrapper"
37+ onClick={() => setShowDropdown(!showDropdown)}>
38+ <Icon icon="Ellipsis" size="18px" />
39+ {showDropdown && (
40+ <Box className={'kick-dropdown'} onClick={handleKickClick}>
41+ <Text>Kick</Text>
42+ </Box>
43+ )}
44+ </Box>
45+ </li>
46+ );
47+};
src/views/Ops/Residents.scss+58
@@ -12,6 +12,23 @@
12 }
12 }
13 }
13 }
14
14
15+ .loading {
16+ &> svg {
17+ width: 24px;
18+ height: 24px;
19+ }
20+ }
21+
22+ .loading, .no-results {
23+ width: 100%;
24+ min-height: 500px;
25+ display: flex;
26+ flex-flow: column nowrap;
27+ justify-content: center;
28+ align-items: center;
29+ color: rgba(0,0,0,0.6);
30+ }
31+
15 ul.residents-list {
32 ul.residents-list {
16 padding-left: unset;
33 padding-left: unset;
17 li {
34 li {
@@ -20,11 +37,52 @@
20 height: 24px;
37 height: 24px;
21 display: flex;
38 display: flex;
22 flex-flow: row nowrap;
39 flex-flow: row nowrap;
40+ justify-content: space-between;
23
41
24 &:first {
42 &:first {
25 margin-top: unset;
43 margin-top: unset;
26 }
44 }
27
45
46+ .point-wrapper {
47+ display: flex;
48+ flex-flow: row nowrap;
49+ }
50+
51+ .button-wrapper {
52+ cursor: pointer;
53+ display: flex;
54+ flex-flow: column nowrap;
55+ justify-content: center;
56+ position: relative;
57+
58+ .kick-dropdown {
59+ z-index: 9999;
60+ display: flex;
61+ flex-direction: column;
62+ align-items: flex-start;
63+ justify-content: center;
64+ width: 100px;
65+ height: 48px;
66+ background-color: #fff;
67+ border: 1px solid rgba(0, 0, 0, 0.2);
68+ box-sizing: border-box;
69+ border-radius: 4px;
70+ position: absolute;
71+ top: 28px;
72+ right: 0px;
73+ padding: 16px;
74+
75+ span {
76+ font-family: Inter;
77+ font-size: 14px;
78+ font-style: normal;
79+ font-weight: 500;
80+ line-height: 16px;
81+ color: rgba(255, 65, 54, 1);
82+ }
83+ }
84+ }
85+
28 div.sigil {
86 div.sigil {
29 height: 24px;
87 height: 24px;
30 width: 24px;
88 width: 24px;
src/views/Ops/Residents.tsx+29−24
@@ -1,6 +1,5 @@
1import * as need from 'lib/need';
1import * as need from 'lib/need';
2−import ob from 'urbit-ob';
3−import { Box, Row } from '@tlon/indigo-react';
2+import { Box, LoadingSpinner, Row } from '@tlon/indigo-react';
4
3
5import Window from 'components/L2/Window/Window';
4import Window from 'components/L2/Window/Window';
6import HeaderPane from 'components/L2/Window/HeaderPane';
5import HeaderPane from 'components/L2/Window/HeaderPane';
@@ -9,31 +8,41 @@ import { useLocalRouter } from 'lib/LocalRouter';
9import View from 'components/View';
8import View from 'components/View';
10import L2BackHeader from 'components/L2/Headers/L2BackHeader';
9import L2BackHeader from 'components/L2/Headers/L2BackHeader';
11import { usePointCursor } from 'store/pointCursor';
10import { usePointCursor } from 'store/pointCursor';
12−import { useCallback, useEffect, useState } from 'react';
11+import { useCallback, useEffect, useMemo, useState } from 'react';
13
12
14import './Residents.scss';
13import './Residents.scss';
15−import Sigil from 'components/Sigil';
16import useRoller from 'lib/useRoller';
14import useRoller from 'lib/useRoller';
17import { Ship } from '@urbit/roller-api';
15import { Ship } from '@urbit/roller-api';
16+import { ResidentRow } from './ResidentRow';
18
17
19export const Residents = () => {
18export const Residents = () => {
20 const { pop }: any = useLocalRouter();
19 const { pop }: any = useLocalRouter();
21 const { pointCursor } = usePointCursor();
20 const { pointCursor } = usePointCursor();
22− const point = need.point(pointCursor);
21+ const point = useMemo(() => need.point(pointCursor), [pointCursor]);
23− const { api } = useRoller();
22+ const { api, kickPoint } = useRoller();
24 const [sponsoredPoints, setSponsoredPoints] = useState<Ship[]>([]);
23 const [sponsoredPoints, setSponsoredPoints] = useState<Ship[]>([]);
24+ const [loading, setLoading] = useState<boolean>(false);
25
25
26 const fetchResidents = useCallback(async () => {
26 const fetchResidents = useCallback(async () => {
27 if (!point) {
27 if (!point) {
28 return;
28 return;
29 }
29 }
30
30
31+ setLoading(true);
31 // TODO: remove this hack once the API is updated
32 // TODO: remove this hack once the API is updated
32 const { residentes, residents } = await api.getSponsoredPoints(point);
33 const { residentes, residents } = await api.getSponsoredPoints(point);
33−
34 setSponsoredPoints((residents || residentes || []).sort());
34 setSponsoredPoints((residents || residentes || []).sort());
35+ setLoading(false);
35 }, [api, point]);
36 }, [api, point]);
36
37
38+ const handleKick = useCallback(
39+ async (point: Ship) => {
40+ await kickPoint(point);
41+ await fetchResidents();
42+ },
43+ [fetchResidents, kickPoint]
44+ );
45+
37 useEffect(() => {
46 useEffect(() => {
38 fetchResidents();
47 fetchResidents();
39 }, [fetchResidents]);
48 }, [fetchResidents]);
@@ -56,23 +65,19 @@ export const Residents = () => {
56 </HeaderPane>
65 </HeaderPane>
57 <BodyPane>
66 <BodyPane>
58 <Box className="content-container">
67 <Box className="content-container">
68+ {loading ? (
69+ <Box className={'loading'}>
70+ <LoadingSpinner />
71+ </Box>
72+ ) : sponsoredPoints.length > 0 ? (
59− <ul className="residents-list">
73+ <ul className="residents-list">
60− {sponsoredPoints.map(sp => (
74+ {sponsoredPoints.map(sp => (
61− <li>
75+ <ResidentRow point={sp} onKick={handleKick} />
62− <Box className="sigil">
76+ ))}
63− <Box className="sigil-container">
77+ </ul>
64− <Sigil
78+ ) : (
65− icon
66− patp={ob.patp(sp)}
67− size={16}
68− colors={['#000000', '#FFFFFF']}
69− />
70− </Box>
71− </Box>
72− <Box className={'patp'}>{ob.patp(sp)}</Box>
79+ <Box className="no-results">No residents</Box>
73− </li>
74− ))}
80+ )}
75− </ul>
76 </Box>
81 </Box>
77 </BodyPane>
82 </BodyPane>
78 </Window>
83 </Window>
src/views/Point/Point.tsx+2
@@ -201,6 +201,8 @@ export default function Point() {
201 ? 'Transfer Proxy Changed'
201 ? 'Transfer Proxy Changed'
202 : pendingTx.rawTx?.tx?.type === 'set-spawn-proxy'
202 : pendingTx.rawTx?.tx?.type === 'set-spawn-proxy'
203 ? 'Spawn Proxy Changed'
203 ? 'Spawn Proxy Changed'
204+ : pendingTx.rawTx?.tx?.type === 'detach'
205+ ? 'Sponsee Detached'
204 : ''}
206 : ''}
205 </div>
207 </div>
206 <div className="rollup-timer">
208 <div className="rollup-timer">