import React, { useCallback, useMemo, useRef, useState } from 'react';
import { Just, Nothing } from 'folktale/maybe';
import { Grid, Text, H5, Flex, ToggleInput, CheckboxInput } from 'indigo-react';
import * as azimuth from 'azimuth-js';
import ob from 'urbit-ob';
import { randomHex } from 'web3-utils';
import { usePointCursor } from 'store/pointCursor';
import { usePointCache } from 'store/pointCache';
import { useNetwork } from 'store/network';
import { useWallet } from 'store/wallet';
import { useLocalRouter } from 'lib/LocalRouter';
import * as need from 'lib/need';
import {
segmentNetworkKey,
attemptNetworkSeedDerivation,
deriveNetworkKeys,
CRYPTO_SUITE_VERSION,
CURVE_ZERO_ADDR,
} from 'lib/keys';
import { formatDotsWithTime } from 'lib/dateFormat';
import useEthereumTransaction from 'lib/useEthereumTransaction';
import { GAS_LIMITS } from 'lib/constants';
import { addHexPrefix } from 'lib/wallet';
import useKeyfileGenerator from 'lib/useKeyfileGenerator';
import ViewHeader from 'components/ViewHeader';
import { BootUrbitOSButton } from 'components/Buttons';
import FooterButton from 'components/FooterButton';
import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
import NoticeBox from 'components/NoticeBox';
import { HexInput } from 'form/Inputs';
import {
composeValidator,
buildCheckboxValidator,
buildHexValidator,
} from 'form/validators';
import BridgeForm from 'form/BridgeForm';
import Condition from 'form/Condition';
import FormError from 'form/FormError';
import convertToInt from 'lib/convertToInt';
const chainKeyProp = name => d =>
d[name] === CURVE_ZERO_ADDR ? Nothing() : Just(d[name]);
const renderNetworkKey = key => {
const segments = segmentNetworkKey(key);
if (!segments) {
return 'Unset';
}
return segments.join('\n');
};
function useSetKeys() {
const { urbitWallet, wallet, authMnemonic } = useWallet();
const { pointCursor } = usePointCursor();
const { syncDetails, syncRekeyDate, getDetails } = usePointCache();
const { contracts } = useNetwork();
const _point = need.point(pointCursor);
const _contracts = need.contracts(contracts);
const _details = need.details(getDetails(_point));
const networkRevision = convertToInt(_details.keyRevisionNumber, 10);
const randomSeed = useRef();
// NOTE: nd = 'nondeterministic' (can also be a 'manual' seed)
const [ndNetworkSeed, setNdNetworkSeed] = useState();
const {
available: keyfileAvailable,
generating: keyfileGenerating,
filename,
bind: keyfileBind,
} = useKeyfileGenerator(ndNetworkSeed);
const buildNetworkSeed = useCallback(
async manualSeed => {
if (manualSeed !== undefined) {
setNdNetworkSeed(manualSeed);
return manualSeed;
} else {
const newNetworkRevision = networkRevision + 1;
console.log(`deriving seed with revision ${newNetworkRevision}`);
const networkSeed = await attemptNetworkSeedDerivation({
urbitWallet,
wallet,
authMnemonic,
details: _details,
revision: newNetworkRevision,
});
if (Just.hasInstance(networkSeed)) {
return networkSeed.value;
}
randomSeed.current = randomSeed.current || randomHex(32); // 32 bytes
setNdNetworkSeed(randomSeed.current);
return randomSeed.current;
}
},
[_details, authMnemonic, networkRevision, urbitWallet, wallet]
);
const { completed: _completed, ...rest } = useEthereumTransaction(
useCallback(
async (manualSeed, isDiscontinuity) => {
const seed = await buildNetworkSeed(manualSeed);
const pair = deriveNetworkKeys(seed);
return azimuth.ecliptic.configureKeys(
_contracts,
_point,
addHexPrefix(pair.crypt.public),
addHexPrefix(pair.auth.public),
CRYPTO_SUITE_VERSION,
isDiscontinuity
);
},
[_contracts, _point, buildNetworkSeed]
),
useCallback(
() => Promise.all([syncDetails(_point), syncRekeyDate(_point)]),
[_point, syncDetails, syncRekeyDate]
),
GAS_LIMITS.CONFIGURE_KEYS
);
// only treat the transaction as completed once we also have keys to download
const completed = _completed && keyfileAvailable && !keyfileGenerating;
return {
completed,
ndNetworkSeed,
filename,
keyfileBind,
...rest,
};
}
export default function AdminNetworkingKeys() {
const { push, names, pop } = useLocalRouter();
const { pointCursor } = usePointCursor();
const { getDetails, getRekeyDate } = usePointCache();
const point = need.point(pointCursor);
const details = getDetails(point);
const hasKeys = details.matchWith({
Nothing: () => false,
Just: ({ value: details }) => parseInt(details.keyRevisionNumber, 10) > 0,
// we actually don't mind the default NaN behavior of parseInt here,
// since NaN > 0 === false and that's a reasonable result
});
const [usedDiscontinuity, setUsedDiscontinuity] = useState(false);
const {
isDefaultState,
construct,
unconstruct,
broadcasting,
completed,
inputsLocked,
filename,
bind,
keyfileBind,
} = useSetKeys();
const validateForm = useCallback((values, errors) => {
if (values.useNetworkSeed && errors.networkSeed) {
return errors;
}
}, []);
const validate = useMemo(
() =>
composeValidator(
{
useNetworkSeed: buildCheckboxValidator(),
networkSeed: buildHexValidator(64), // 64 chars
useDiscontinuity: buildCheckboxValidator(),
},
validateForm
),
[validateForm]
);
const onValues = useCallback(
({ valid, values, form }) => {
if (valid) {
construct(
values.useNetworkSeed ? values.networkSeed : undefined,
values.useDiscontinuity
);
setUsedDiscontinuity(values.useDiscontinuity);
} else {
unconstruct();
}
if (!values.useNetworkSeed && values.networkSeed) {
form.change('networkSeed', '');
}
},
[construct, unconstruct, setUsedDiscontinuity]
);
const initialValues = useMemo(
() => ({
useNetworkSeed: false,
useDiscontinuity: false,
}),
[]
);
const goRelocate = useCallback(() => push(names.RELOCATE), [push, names]);
const usageMessage = useMemo(() => {
const need = 'You need this keyfile to authenticate with Arvo.';
const boot = (
<>
boot fresh with{' '}
urbit -w {ob.patp(point).slice(1)} -k {filename}
>
);
if (usedDiscontinuity) {
return (
<>
{need} Since you broke continuity, (re)move your old files, and {boot}
>
);
} else {
return (
<>
{need} If you have already booted Arvo, run{' '}
|rekey '0wkeyfile.contents'. Otherwise, {boot}
>
);
}
}, [usedDiscontinuity, filename, point]);
const renderTitle = () => {
if (completed) {
return 'Networking keys are now set. Download your Keyfile to authenticate Arvo.';
}
if (broadcasting) {
return 'Setting Network Keys...';
}
if (!hasKeys) {
return 'Networking keys have not yet been set.';
}
return 'Networking';
};
const renderNetworkKeySection = (title, key) => (
<>
{title}
{key.matchWith({
Nothing: () => (
Unset
),
Just: ({ value: key }) => (
<>
0x
{renderNetworkKey(key)}
>
),
})}
>
);
const renderDetail = (title, value) => (
{title}
{value.matchWith({
Nothing: () => (
Unset
),
Just: ({ value }) => {value},
})}
);
const renderDetails = () => {
return (
<>
{renderNetworkKeySection(
'Authentication',
details.chain(chainKeyProp('authenticationKey'))
)}
{renderNetworkKeySection(
'Encryption',
details.chain(chainKeyProp('encryptionKey'))
)}
{renderDetail('Revision', details.map(d => d.keyRevisionNumber))}
{renderDetail('Continuity Era', details.map(d => d.continuityNumber))}
{renderDetail(
'Crypto Suite Ver.',
details.map(d => d.cryptoSuiteVersion)
)}
{hasKeys && (
{renderDetail(
'Last Set',
getRekeyDate(point).map(date =>
date.matchWith({
Ok: r => formatDotsWithTime(r.value),
Error: () => Nothing(),
})
)
)}
)}
>
);
};
return (
<>
{renderTitle()}
{hasKeys
? 'Here are your public keys that authenticate your Arvo.'
: 'Set your networking keys to authenticate with Arvo.'}
{completed && (
<>
{usageMessage}
>
)}
{!completed && (
{() => (
<>
When using a custom network seed, you'll need to download
your Arvo keyfile immediately after this transaction is
Bridge does not store your seed.
pop()}
/>
>
)}
)}
{isDefaultState && renderDetails()}
{completed && (
<>
>
)}
{isDefaultState && (
Relocate
)}
>
);
}