diff --git a/package.json b/package.json index 7782ce8..f582260 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@ethereumjs/common": "^2.6.3", "@ethereumjs/tx": "^3.5.1", "@ethersproject/abi": "^5.6.0", + "@ethersproject/address": "^5.6.0", "@ethersproject/bignumber": "^5.5.0", "@ethersproject/logger": "^5.6.0", "@ethersproject/strings": "5.0.0", diff --git a/pages/contract-address-calculator/index.page.tsx b/pages/contract-address-calculator/index.page.tsx new file mode 100644 index 0000000..d1b51e6 --- /dev/null +++ b/pages/contract-address-calculator/index.page.tsx @@ -0,0 +1,106 @@ +import React, { ReactElement, useState } from 'react'; + +import { CopyableConversionInput } from '../../src/components/CopyableConversionInput'; +import { CalculatorIcon } from '../../src/components/icons/CalculatorIcon'; +import { Button } from '../../src/components/lib/Button'; +import { Header } from '../../src/components/lib/Header'; +import { NodeBlock } from '../../src/components/lib/NodeBlock'; +import { ToolContainer } from '../../src/components/ToolContainer'; +import { getContractAddress } from '../../src/lib/contractAddress'; +import { handleChangeValidated } from '../../src/misc/handleChangeValidated'; +import { WithError } from '../../src/misc/types'; +import { addressValidator } from '../../src/misc/validation/validators/addressValidator'; +import { numberValidator } from '../../src/misc/validation/validators/numberValidator'; + +export default function ContractAddressCalculator(): ReactElement { + const [fromAddress, setFromAddress] = useState>({ + value: '', + }); + const [nonce, setNonce] = useState>({ + value: '', + }); + + const [calculatedContractAddress, setCalculatedContractAddress] = useState< + string | undefined + >(); + + function flushResults(): void { + setCalculatedContractAddress(undefined); + } + + function handleChangeFromAddress(newValue: string): void { + handleChangeValidated({ + newValue, + validateFn: (newValue) => addressValidator(newValue), + setState: setFromAddress, + flushFn: flushResults, + }); + } + + function handleChangeNonce(newValue: string): void { + handleChangeValidated({ + newValue, + validateFn: (newValue) => numberValidator(newValue), + setState: setNonce, + flushFn: flushResults, + }); + } + + function handleCalculateContractAddress(): void { + try { + setCalculatedContractAddress( + getContractAddress({ + from: fromAddress.value, + nonce: nonce.value, + }), + ); + } catch { + flushResults(); + } + } + + const calculateButtonIsDisabled = Boolean( + !fromAddress.value || fromAddress.error || !nonce.value || nonce.error, + ); + + return ( + +
+
} + text={['Calculators', 'Contract Address Calculator']} + /> +
+ handleChangeFromAddress(event.target.value)} + /> + handleChangeNonce(event.target.value)} + /> +
+ + + {calculatedContractAddress && ( +
+ + Contract address: + +
+ )} + + ); +} diff --git a/pages/vanity-address-generator/index.page.tsx b/pages/vanity-address-generator/index.page.tsx index 0991c45..68054ca 100644 --- a/pages/vanity-address-generator/index.page.tsx +++ b/pages/vanity-address-generator/index.page.tsx @@ -91,7 +91,10 @@ export default function VanityAddressGenerator(): ReactElement { const handleChangeCpuCoreCount = (newValue: string): void => handleChangeValidated({ newValue, - validateFn: (newValue) => numberValidator(newValue), + validateFn: (newValue) => + numberValidator(newValue, (schema) => + schema.refine((n) => n >= 0 && n <= 128), + ), setState: setCpuCores, flushFn: flushResults, }); diff --git a/src/components/ToolTree.tsx b/src/components/ToolTree.tsx index e046858..9b9c6c4 100644 --- a/src/components/ToolTree.tsx +++ b/src/components/ToolTree.tsx @@ -101,6 +101,10 @@ const tree: Tree = { title: 'String Bytes32 Conversion', pageHref: 'string-bytes32-conversion', }, + { + title: 'Contract Address Calculator', + pageHref: 'contract-address-calculator', + }, ], }, decoders: { diff --git a/src/lib/contractAddress.test.ts b/src/lib/contractAddress.test.ts new file mode 100644 index 0000000..43c5ec5 --- /dev/null +++ b/src/lib/contractAddress.test.ts @@ -0,0 +1,14 @@ +import { expect } from 'earljs'; + +import { getContractAddress } from './contractAddress'; + +describe(getContractAddress.name, () => { + it('calculates a contract address based on a from address and a nonce', () => { + expect( + getContractAddress({ + from: '0x9C33eaCc2F50E39940D3AfaF2c7B8246B681A374', + nonce: '0', + }), + ).toEqual('0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f'); + }); +}); diff --git a/src/lib/contractAddress.ts b/src/lib/contractAddress.ts new file mode 100644 index 0000000..b7ccb6d --- /dev/null +++ b/src/lib/contractAddress.ts @@ -0,0 +1,8 @@ +import { getContractAddress as getContractAddressImpl } from '@ethersproject/address'; + +export function getContractAddress(params: { + from: string; + nonce: string; +}): string { + return getContractAddressImpl(params); +} diff --git a/src/misc/validation/schemas/addressSchema.ts b/src/misc/validation/schemas/addressSchema.ts new file mode 100644 index 0000000..fc37fc9 --- /dev/null +++ b/src/misc/validation/schemas/addressSchema.ts @@ -0,0 +1,5 @@ +import { z } from 'zod'; + +export const addressSchema = z.string().regex(/^0x[0-9a-fA-F]{40}$/, { + message: 'The value must be a valid address', +}); diff --git a/src/misc/validation/schemas/decimalSchema.ts b/src/misc/validation/schemas/decimalSchema.ts index 7b3e2be..47ae4dd 100644 --- a/src/misc/validation/schemas/decimalSchema.ts +++ b/src/misc/validation/schemas/decimalSchema.ts @@ -1,14 +1,10 @@ import { z } from 'zod'; -export const decimalSchema = z - .number() - .or( - z - .string() - .regex(/^\d*$/, { - message: - 'The value must be a hexadecimal number, 0x-prefix is required', - }) - .transform(Number), - ) - .refine((n) => n >= 0 && n <= 128); +export const decimalSchema = z.number().or( + z + .string() + .regex(/^\d*$/, { + message: 'The value must be a decimal number', + }) + .transform(Number), +); diff --git a/src/misc/validation/validators/addressValidator.ts b/src/misc/validation/validators/addressValidator.ts new file mode 100644 index 0000000..454da4e --- /dev/null +++ b/src/misc/validation/validators/addressValidator.ts @@ -0,0 +1,9 @@ +import { zodResultMessage } from '../../../../src/misc/zodResultMessage'; +import { addressSchema } from '../schemas/addressSchema'; +import { ValidatorResult } from './result'; + +export function addressValidator(newValue: string): ValidatorResult { + const validated = addressSchema.safeParse(newValue); + if (validated.success) return { success: true }; + else return { success: false, error: zodResultMessage(validated) }; +} diff --git a/src/misc/validation/validators/numberValidator.ts b/src/misc/validation/validators/numberValidator.ts index 8df1839..2d5fb36 100644 --- a/src/misc/validation/validators/numberValidator.ts +++ b/src/misc/validation/validators/numberValidator.ts @@ -1,9 +1,15 @@ +import { identity } from 'lodash'; +import { ZodTypeAny } from 'zod'; + import { zodResultMessage } from '../../../../src/misc/zodResultMessage'; import { decimalSchema } from '../schemas/decimalSchema'; import { ValidatorResult } from './result'; -export function numberValidator(newValue: string): ValidatorResult { - const validated = decimalSchema.safeParse(newValue); +export function numberValidator( + newValue: string, + enhanceSchema: (schema: ZodTypeAny) => ZodTypeAny = identity, +): ValidatorResult { + const validated = enhanceSchema(decimalSchema).safeParse(newValue); if (validated.success) return { success: true }; else return { success: false, error: zodResultMessage(validated) }; }