Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A bunch of fixes #23

Merged
merged 8 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/__tests__/mnemonic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { normalizeMnemonic } from '../utils/mnemonic';

describe('Mnemonic', () => {
describe('normalizeMnemonic', () => {
it('returns the same mnemonic', () => {
expect(normalizeMnemonic('a b c d e f g h i j k l m')).toEqual(
'a b c d e f g h i j k l m'
);
});
it('replaces new lines with spaces', () => {
expect(normalizeMnemonic('a\nb\nc\nd\ne\n\n\nf\ng\nh i j k l m')).toEqual(
'a b c d e f g h i j k l m'
);
});
it('replaces multiple spaces with single space', () => {
expect(normalizeMnemonic('a b c d e f g h i j k l m')).toEqual(
'a b c d e f g h i j k l m'
);
});
it('omits numbers and new lines', () => {
const input = [
'1. aaaa',
'2. bbbb',
'3. ccccc',
'4. dd',
'5. eeee',
'6. fffff',
'7. ggg',
'8. hhhh',
'9. ii',
'10. jjjj',
'11. kkkk',
'12. lllll',
'13. mmmm',
'14. nnnnn',
'15. oooo',
'16. pppp',
'17. qqq',
'18. rrrrrr',
'19. sssss',
'20. tttt',
'21. uuuu',
'22. vvvvv',
'23. wwww',
'24. xxxxxx',
].join('\n');

expect(normalizeMnemonic(input)).toEqual(
// eslint-disable-next-line max-len
'aaaa bbbb ccccc dd eeee fffff ggg hhhh ii jjjj kkkk lllll mmmm nnnnn oooo pppp qqq rrrrrr sssss tttt uuuu vvvvv wwww xxxxxx'
);
});
});
});
19 changes: 17 additions & 2 deletions src/__tests__/smh.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,42 @@ describe('formatSmidge', () => {
});
};

// Numbers
check(0, '0 SMH');
check(1, '1 Smidge');
check(12500, '12500 Smidge');
check(10 ** 10, '10 SMH');
check(10 ** 10 + 2315, '10.231 SMH');
check(10 ** 10 + 2315, '10 SMH');
check(10 ** 10 + 231500000, '10.231 SMH');

// Strings
check('0', '0 SMH');
check('1', '1 Smidge');
check('12500', '12500 Smidge');
check(String(10 ** 10), '10 SMH');
check(String(10 ** 10 + 2315), '10.231 SMH');
check(String(10 ** 10 + 2315), '10 SMH');
check(String(10 ** 10 + 231500000), '10.231 SMH');

// Positive values
check(0n, '0 SMH');
check(1n, '1 Smidge');
check(12500n, '12500 Smidge');
check(10n ** 10n, '10 SMH');
check(2681927462728471910n, '2681927462.728 SMH');

// Negative values
check(-0n, '0 SMH');
check(-1n, '-1 Smidge');
check(-12500n, '-12500 Smidge');
check(-1n * 10n ** 10n, '-10 SMH');
check(-2681927462728471910n, '-2681927462.728 SMH');

// Check rounding
check(999999999n, '0.999 SMH');
check(1000000000n, '1 SMH');
check(1234500000n, '1.234 SMH');
check(1021234500000n, '1021.234 SMH');
check(99999999n, '0.099 SMH');
check(9999999n, '0.009 SMH');
check(999999n, '999999 Smidge');
});
3 changes: 2 additions & 1 deletion src/api/requests/balance.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AccountStatesWithAddress } from '../../types/account';
import { Bech32Address } from '../../types/common';
import { BalanceResponseSchema } from '../schemas/account';
import { parseResponse } from '../schemas/error';

const DEFAULT_STATE = {
nonce: '0',
Expand All @@ -17,7 +18,7 @@ export const fetchBalances = async (rpc: string, addresses: Bech32Address[]) =>
}),
})
.then((r) => r.json())
.then(BalanceResponseSchema.parse)
.then(parseResponse(BalanceResponseSchema))
.then(({ accounts }) =>
addresses.reduce((acc, nextAddress) => {
const states = accounts.find((s) => s.address === nextAddress);
Expand Down
5 changes: 3 additions & 2 deletions src/api/requests/netinfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import parse from 'parse-duration';
import { fromBase64 } from '../../utils/base64';
import fetchJSON from '../../utils/fetchJSON';
import { toHexString } from '../../utils/hexString';
import { parseResponse } from '../schemas/error';
import { NetworkInfoResponseSchema } from '../schemas/network';
import { NodeStatusSchema, NodeSyncStatus } from '../schemas/node';

export const fetchNetworkInfo = (rpc: string) =>
fetchJSON(`${rpc}/spacemesh.v2alpha1.NetworkService/Info`, {
method: 'POST',
})
.then(NetworkInfoResponseSchema.parse)
.then(parseResponse(NetworkInfoResponseSchema))
.then((res) => ({
...res,
genesisId: toHexString(fromBase64(res.genesisId)),
Expand All @@ -20,7 +21,7 @@ export const fetchNetworkInfo = (rpc: string) =>

export const fetchNodeStatus = (rpc: string) =>
fetchJSON(`${rpc}/spacemesh.v2alpha1.NodeService/Status`, { method: 'POST' })
.then(NodeStatusSchema.parse)
.then(parseResponse(NodeStatusSchema))
.then((status) => ({
connectedPeers: parseInt(status.connectedPeers, 10),
isSynced: status.status === NodeSyncStatus.SYNCED,
Expand Down
3 changes: 2 additions & 1 deletion src/api/requests/rewards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Bech32Address } from '../../types/common';
import { Reward } from '../../types/reward';
import { fromBase64 } from '../../utils/base64';
import { toHexString } from '../../utils/hexString';
import { parseResponse } from '../schemas/error';
import { RewardsListSchema } from '../schemas/rewards';

import getFetchAll from './getFetchAll';
Expand All @@ -21,7 +22,7 @@ export const fetchRewardsChunk = (
}),
})
.then((r) => r.json())
.then(RewardsListSchema.parse)
.then(parseResponse(RewardsListSchema))
.then(({ rewards }) =>
rewards.map(
(reward): Reward => ({
Expand Down
7 changes: 4 additions & 3 deletions src/api/requests/tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Transaction } from '../../types/tx';
import { fromBase64, toBase64 } from '../../utils/base64';
import { toHexString } from '../../utils/hexString';
import { getMethodName, getTemplateNameByAddress } from '../../utils/templates';
import { parseResponse } from '../schemas/error';
import {
EstimateGasResponseSchema,
SubmitTxResponseSchema,
Expand Down Expand Up @@ -38,7 +39,7 @@ export const fetchTransactionsChunk = async (
}),
})
.then((r) => r.json())
.then(TransactionResponseSchema.parse)
.then(parseResponse(TransactionResponseSchema))
.then(({ transactions }) =>
transactions.map((tx) => ({
...tx.tx,
Expand Down Expand Up @@ -104,7 +105,7 @@ export const fetchEstimatedGas = async (rpc: string, encodedTx: Uint8Array) =>
}),
})
.then((r) => r.json())
.then(EstimateGasResponseSchema.parse)
.then(parseResponse(EstimateGasResponseSchema))
.then(({ recommendedMaxGas }) => recommendedMaxGas);

export const fetchPublishTx = async (rpc: string, encodedTx: Uint8Array) =>
Expand All @@ -115,5 +116,5 @@ export const fetchPublishTx = async (rpc: string, encodedTx: Uint8Array) =>
}),
})
.then((r) => r.json())
.then(SubmitTxResponseSchema.parse)
.then(parseResponse(SubmitTxResponseSchema))
.then(({ txId }) => txId);
2 changes: 1 addition & 1 deletion src/api/schemas/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const Bech32AddressSchema = z.custom<string>((addr) => {
if (typeof addr !== 'string') return false;
try {
const p = bech32.decode(addr);
if (!['sm', 'stest'].includes(p.prefix)) return false;
if (!['sm', 'stest', 'standalone'].includes(p.prefix)) return false;
if (bech32.fromWords(p.words).length !== 24) return false;
return true;
} catch (err) {
Expand Down
27 changes: 27 additions & 0 deletions src/api/schemas/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { z } from 'zod';

export const ErrorResponse = z.object({
code: z.number(),
message: z.string(),
details: z.array(z.any()),
});

type ErrorResponse = z.infer<typeof ErrorResponse>;

export type APIError = Error & { code: number };

export const toError = (err: ErrorResponse): APIError => {
const error = new Error(err.message) as APIError;
error.code = err.code;
return error;
};

export const parseResponse =
<T extends z.ZodType>(schema: T) =>
(input: unknown): z.infer<T> => {
try {
return schema.parse(input);
} catch (err) {
throw toError(ErrorResponse.parse(input));
}
};
110 changes: 110 additions & 0 deletions src/components/FormAmountInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { PropsWithChildren, ReactNode, useState } from 'react';
import {
FieldErrors,
FieldValues,
Path,
PathValue,
UseFormGetValues,
UseFormRegisterReturn,
UseFormSetValue,
} from 'react-hook-form';

import {
FormControl,
FormErrorMessage,
FormLabel,
IconButton,
Input,
InputGroup,
InputRightElement,
Text,
} from '@chakra-ui/react';
import { IconSwitchHorizontal } from '@tabler/icons-react';

import { CoinUnits, toSMH, toSmidge } from '../utils/smh';

type Props<T extends FieldValues> = PropsWithChildren<{
label: string;
register: UseFormRegisterReturn;
errors: FieldErrors<T>;
isSubmitted?: boolean;
setValue: UseFormSetValue<T>;
getValues: UseFormGetValues<T>;
}>;

function FormAmountInput<T extends FieldValues>({
label,
register,
errors,
setValue,
getValues,
isSubmitted = false,
}: Props<T>): JSX.Element {
const [units, setUnits] = useState(CoinUnits.SMH);
const [displayValue, setDisplayValue] = useState('0');
const toggleUnits = () => {
const paths = register.name.split('.');
const vals = getValues();

const val = paths.reduce((acc, next) => acc?.[next], vals);
if (units === CoinUnits.SMH) {
setDisplayValue(String(val));
setUnits(CoinUnits.Smidge);
} else {
setUnits(CoinUnits.SMH);
setDisplayValue(toSMH(BigInt(String(val))));
}
};

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
setDisplayValue(val);

if (units === CoinUnits.SMH) {
setValue(
register.name as Path<T>,
toSmidge(Number(val)) as PathValue<T, Path<T>>
);
} else {
setValue(
register.name as Path<T>,
String(BigInt(val)) as PathValue<T, Path<T>>
);
}
};

const error = errors[register.name];
return (
<FormControl
isRequired={!!register.required}
isInvalid={isSubmitted && !!errors[register.name]?.message}
mt={2}
mb={2}
>
<FormLabel fontSize="sm" mb={0}>
{label}
</FormLabel>
<Input type="hidden" {...register} />
<InputGroup>
<Input type="number" value={displayValue} onChange={onChange} />
<InputRightElement p={0} w={100} justifyContent="end" pr={2}>
<Text fontSize="xs">{units}</Text>
<IconButton
aria-label={
units === CoinUnits.SMH ? 'Switch to Smidge' : 'Switch to SMH'
}
size="xs"
onClick={toggleUnits}
icon={<IconSwitchHorizontal size={16} />}
ml={1}
/>
</InputRightElement>
</InputGroup>
{error?.message && (
<FormErrorMessage>{error.message as ReactNode}</FormErrorMessage>
)}
</FormControl>
);
}

export default FormAmountInput;
2 changes: 1 addition & 1 deletion src/components/KeyManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import CopyButton from './CopyButton';
import CreateAccountModal from './CreateAccountModal';
import CreateKeyPairModal from './CreateKeyPairModal';
import ExplorerButton from './ExplorerButton';
import ImportKeyPairModal from './ImportKeyPairModal copy';
import ImportKeyPairModal from './ImportKeyPairModal';
import RevealSecretKeyModal from './RevealSecretKeyModal';

type KeyManagerProps = {
Expand Down
Loading