import React, { useCallback, useEffect, useMemo } from 'react';
import { Just, Nothing } from 'folktale/maybe';
import { Grid, H5, HelpText, LinkButton, Flex } from 'indigo-react';
import { get } from 'lodash';
import { useHistory } from 'store/history';
import { useWallet } from 'store/wallet';
import { usePointCache } from 'store/pointCache';
import { usePointCursor } from 'store/pointCursor';
import { useStarReleaseCache } from 'store/starRelease';
import * as need from 'lib/need';
import { isZeroAddress, abbreviateAddress } from 'lib/utils/address';
import useIsEclipticOwner from 'lib/useIsEclipticOwner';
import { useSyncDetails } from 'lib/useSyncPoints';
import useRejectedIncomingPointTransfers from 'lib/useRejectedIncomingPointTransfers';
import { pluralize } from 'lib/pluralize';
import newGithubIssueUrl from 'new-github-issue-url';
import View from 'components/View';
import Blinky from 'components/Blinky';
import Passport from 'components/Passport';
import Footer from 'components/Footer';
import { ForwardButton } from 'components/Buttons';
import CopiableAddress from 'components/copiable/CopiableAddress';
import NavHeader from 'components/NavHeader';
import L2PointHeader from 'components/L2/Headers/L2PointHeader';
import IncomingPoint from 'components/L2/Points/IncomingPoint';
import { useNetwork } from 'store/network';
export const maybeGetResult = (obj, key, defaultValue) =>
obj.matchWith({
Nothing: () => defaultValue,
Just: p =>
p.value.matchWith({
Ok: r => get(r.value, key, defaultValue),
Error: e => defaultValue,
}),
});
export const hasTransferProxy = details =>
!isZeroAddress(details.transferProxy);
export const getOutgoingPoints = (controlledPoints, getDetails) => {
return controlledPoints
.chain(points =>
points.matchWith({
Error: () => Nothing(),
Ok: c => {
const points = c.value.ownedPoints.map(point =>
getDetails(point).chain(details =>
Just({ point: point, has: hasTransferProxy(details) })
)
);
if (points.every(p => Just.hasInstance(p))) {
const outgoing = points
.filter(p => p.value.has)
.map(p => p.value.point);
return Just(outgoing);
} else {
return Nothing();
}
},
})
)
.getOrElse([]);
};
export const isLocked = (details, contracts) =>
details.owner === contracts.linearSR ||
details.owner === contracts.conditionalSR;
const PointList = function({
points,
className,
actions,
locked = false,
...rest
}) {
const { setPointCursor } = usePointCursor();
const { push, names } = useHistory();
return (
{points.map((point, i) => (
{
setPointCursor(Just(point));
push(names.POINT);
}
}
{...rest}
/>
{actions && (
{actions(point, i)}
)}
))}
);
};
function ActionButtons({ actions = [] }) {
return (
{actions.map(action => (
{action.text}
))}
);
}
export default function Points() {
const { wallet } = useWallet();
const { pop, push, popAndPush, names } = useHistory();
const { setPointCursor } = usePointCursor();
const { controlledPoints, getDetails } = usePointCache();
const { contracts } = useNetwork();
const isEclipticOwner = useIsEclipticOwner();
const [
rejectedPoints,
addRejectedPoint,
] = useRejectedIncomingPointTransfers();
const { syncStarReleaseDetails, starReleaseDetails } = useStarReleaseCache();
const outgoingPoints = useMemo(
() => getOutgoingPoints(controlledPoints, getDetails),
[getDetails, controlledPoints]
);
const _contracts = need.contracts(contracts);
const maybeLockedPoints = useMemo(
() =>
controlledPoints.chain(points =>
points.matchWith({
Error: () => Nothing(),
Ok: c => {
const points = c.value.ownedPoints.map(point =>
getDetails(point).chain(details =>
Just({ point, has: isLocked(details, _contracts) })
)
);
// if we have details for every point,
// return the array of pending transfers.
if (points.every(p => Just.hasInstance(p))) {
const locked = points
.filter(p => p.value.has)
.map(p => p.value.point);
return Just(locked);
} else {
return Nothing();
}
},
})
),
[getDetails, controlledPoints, _contracts]
);
// if we can only interact with a single point, forget about the existence
// of this page and jump to the point page.
// if there are any pending transfers, incoming or outgoing, stay on this
// page, because those can only be completed/cancelled here.
useEffect(() => {
if (outgoingPoints.length < 1 || Nothing.hasInstance(starReleaseDetails)) {
return;
}
controlledPoints.matchWith({
Nothing: () => null,
Just: r => {
r.value.matchWith({
Error: () => null,
Ok: c => {
let all = [
...c.value.ownedPoints,
...c.value.votingPoints,
...c.value.managingPoints,
...c.value.spawningPoints,
];
const incoming = c.value.incomingPoints.filter(
p => !rejectedPoints.includes(p)
);
if (
all.length === 1 &&
incoming.length === 0 &&
outgoingPoints.length === 0 &&
(starReleaseDetails.value === null ||
starReleaseDetails.value.total === 0)
) {
setPointCursor(Just(all[0]));
popAndPush(names.POINT);
}
},
});
},
});
}, [
controlledPoints,
rejectedPoints,
outgoingPoints,
setPointCursor,
popAndPush,
names,
starReleaseDetails,
]);
const address = need.addressFromWallet(wallet);
const loading = Nothing.hasInstance(controlledPoints);
const ownedPoints = maybeGetResult(controlledPoints, 'ownedPoints', []);
const incomingPoints = maybeGetResult(
controlledPoints,
'incomingPoints',
[]
).filter(point => !rejectedPoints.includes(point));
const managingPoints = maybeGetResult(controlledPoints, 'managingPoints', []);
const votingPoints = maybeGetResult(controlledPoints, 'votingPoints', []);
const spawningPoints = maybeGetResult(controlledPoints, 'spawningPoints', []);
const lockedPoints = maybeLockedPoints.getOrElse([]);
const allPoints = [
...ownedPoints.filter(p => !outgoingPoints.includes(p)),
...managingPoints,
...votingPoints,
...spawningPoints,
].sort((a, b) => Number(a) - Number(b));
const displayEmptyState =
!loading && incomingPoints.length === 0 && allPoints.length === 0;
const starReleasing = starReleaseDetails
.map(s => (s ? s.total > 0 : false))
.getOrElse(false);
useEffect(() => {
syncStarReleaseDetails();
}, [syncStarReleaseDetails]);
// sync display details for known points
useSyncDetails(ownedPoints);
const goCreateGalaxy = useCallback(() => push(names.CREATE_GALAXY), [
names.CREATE_GALAXY,
push,
]);
const goStarRelease = useCallback(() => push(names.STAR_RELEASE), [
names.STAR_RELEASE,
push,
]);
// if we got an error result, we should display it, instead of showing
// a potentially inaccurately empty point list.
//
if (
Just.hasInstance(controlledPoints) &&
controlledPoints.value.matchWith({
Error: e => true,
Ok: v => false,
})
) {
const url = newGithubIssueUrl({
user: 'urbit',
repo: 'bridge',
title: controlledPoints.value.value,
body: ``,
});
return (
{controlledPoints.value.value}
Try reloading the page. If this problem persists, please{' '}
file an issue on GitHub.
);
}
if (
loading ||
(allPoints.length === 1 &&
incomingPoints.length === 0 &&
outgoingPoints.length === 0 &&
!starReleasing)
) {
return (
Loading...
);
}
return (
}>
{abbreviateAddress(address)}
{incomingPoints.map(point => (
{
setPointCursor(Just(point));
push(names.ACCEPT_TRANSFER);
}}
reject={() => addRejectedPoint(point)}
/>
))}
{displayEmptyState && (
No points to display. This wallet is not the owner or proxy for any
points.
)}
{outgoingPoints.length > 0 && (
{pluralize(
outgoingPoints.length,
'Outgoing Transfer',
'Outgoing Transfers'
)}
(
{
setPointCursor(Just(point));
// TODO: deep linking to fix this duplicate route
push(names.CANCEL_TRANSFER);
},
},
]}
/>
)}
/>
)}
{allPoints.length > 0 && (
)}
{lockedPoints.length > 0 && (
)}
);
}