diff --git a/.changeset/sweet-rocks-raise.md b/.changeset/sweet-rocks-raise.md new file mode 100644 index 0000000..df72606 --- /dev/null +++ b/.changeset/sweet-rocks-raise.md @@ -0,0 +1,5 @@ +--- +'@tenderly/sdk': patch +--- + +add zod api response validation diff --git a/lib/common.schema.ts b/lib/common.schema.ts new file mode 100644 index 0000000..bd2ea55 --- /dev/null +++ b/lib/common.schema.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; +import { Network } from './common.types'; + +export const pathSchema = z.string(); + +export const web3AddressSchema = z.string().length(42).startsWith('0x'); + +export const tenderlyConfigurationSchema = z.object({ + accountName: z.string(), + projectName: z.string(), + accessKey: z.string(), + network: z.union([z.nativeEnum(Network), z.number()]), +}); diff --git a/lib/types.ts b/lib/common.types.ts similarity index 74% rename from lib/types.ts rename to lib/common.types.ts index 45e1db6..c12f633 100644 --- a/lib/types.ts +++ b/lib/common.types.ts @@ -1,5 +1,9 @@ -export type Path = string; -export type Web3Address = string; +import { z } from 'zod'; +import { pathSchema, tenderlyConfigurationSchema, web3AddressSchema } from './common.schema'; + +export type Path = z.infer; +export type Web3Address = z.infer; +export type TenderlyConfiguration = z.infer; export enum Network { MAINNET = 1, @@ -39,13 +43,6 @@ export enum Network { SEPOLIA = 11155111, } -export type TenderlyConfiguration = { - accountName: string; - projectName: string; - accessKey: string; - network: Network | number; -}; - // helper types export type WithRequired = T & { [P in K]-?: T[P] }; export type EmptyObject = Record; diff --git a/lib/core/ApiClientProvider.ts b/lib/core/ApiClientProvider.ts index a29ab94..f9f78a0 100644 --- a/lib/core/ApiClientProvider.ts +++ b/lib/core/ApiClientProvider.ts @@ -1,5 +1,5 @@ import { ApiClient, ApiVersion } from './ApiClient'; -import { EmptyObject } from '../types'; +import { EmptyObject } from '../common.types'; export class ApiClientProvider { static instance: ApiClientProvider; diff --git a/lib/core/Tenderly.ts b/lib/core/Tenderly.ts index 63c2c41..3abd112 100644 --- a/lib/core/Tenderly.ts +++ b/lib/core/Tenderly.ts @@ -1,4 +1,4 @@ -import { TenderlyConfiguration } from '../types'; +import { TenderlyConfiguration } from '../common.types'; import { WalletRepository, ContractRepository } from '../repositories'; import { Simulator } from '../executors'; import { VerificationRequest } from '../repositories/contracts/contracts.types'; diff --git a/lib/errors/Error.types.ts b/lib/errors/Error.types.ts index 37f4e9b..4e70bb9 100644 --- a/lib/errors/Error.types.ts +++ b/lib/errors/Error.types.ts @@ -1,5 +1,5 @@ import { AxiosError, isAxiosError } from 'axios'; -import { WithRequired } from '../types'; +import { WithRequired } from '../common.types'; export interface TenderlyError { readonly id?: string; diff --git a/lib/executors/Simulator.ts b/lib/executors/Simulator.ts index dff0858..507f919 100644 --- a/lib/executors/Simulator.ts +++ b/lib/executors/Simulator.ts @@ -1,5 +1,5 @@ import { ApiClient } from '../core/ApiClient'; -import { Web3Address } from '../types'; +import { Web3Address } from '../common.types'; import { SimulationParameters, SimulationRequest, @@ -16,7 +16,7 @@ import { SimulateBundleResponse, SimulationRequestOverride, } from './Simulator.types'; -import { TenderlyConfiguration } from '../types'; +import { TenderlyConfiguration } from '../common.types'; import { handleError, EncodingError } from '../errors'; import { ApiClientProvider } from '../core/ApiClientProvider'; import { isTenderlyAxiosError } from '../errors/Error.types'; diff --git a/lib/executors/Simulator.types.ts b/lib/executors/Simulator.types.ts index 2631b64..1aa8d6a 100644 --- a/lib/executors/Simulator.types.ts +++ b/lib/executors/Simulator.types.ts @@ -1,4 +1,4 @@ -import { Web3Address } from '../types'; +import { Web3Address } from '../common.types'; export type TransactionParameters = { from: Web3Address; diff --git a/lib/index.ts b/lib/index.ts index ab1f2dd..fff31af 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,5 +1,5 @@ export * from './core'; -export * from './types'; +export * from './common.types'; export * from './errors'; export * from './helpers'; diff --git a/lib/repositories/contracts/contracts.repository.ts b/lib/repositories/contracts/contracts.repository.ts index 2aa91a2..9b6d1de 100644 --- a/lib/repositories/contracts/contracts.repository.ts +++ b/lib/repositories/contracts/contracts.repository.ts @@ -1,4 +1,4 @@ -import { Network, Path, TenderlyConfiguration, Web3Address } from '../../types'; +import { Network, Path, TenderlyConfiguration, Web3Address } from '../../common.types'; import { Repository } from '../Repository'; import { ApiClient } from '../../core/ApiClient'; import { diff --git a/lib/repositories/contracts/contracts.schema.ts b/lib/repositories/contracts/contracts.schema.ts new file mode 100644 index 0000000..54f3f9e --- /dev/null +++ b/lib/repositories/contracts/contracts.schema.ts @@ -0,0 +1,188 @@ +import { z } from 'zod'; +import { Network } from '../../common.types'; +import { web3AddressSchema } from '../../common.schema'; + +export const contractSchema = z.object({ + address: web3AddressSchema, + network: z.nativeEnum(Network), + displayName: z.string().optional(), + tags: z.array(z.string()).optional(), +}); + +export const contractRequestSchema = z.object({ + address: web3AddressSchema, + network_id: z.string(), + display_name: z.string().optional(), +}); + +export const getByParamsSchema = z.object({ + tags: z.array(z.string()).optional(), + displayNames: z.array(z.string()).optional(), +}); + +const internalContractSchema = z.object({ + id: z.string(), + contract_id: z.string(), + balance: z.string(), + network_id: z.string(), + public: z.boolean(), + export: z.boolean(), + verified_by: z.string(), + verification_date: z.string().nullable(), + address: web3AddressSchema, + contract_name: z.string(), + ens_domain: z.string().nullable(), + type: z.string(), + standard: z.string(), + standards: z.array(z.string()), + token_data: z.object({ + symbol: z.string(), + name: z.string(), + main: z.boolean(), + }), + evm_version: z.string(), + compiler_version: z.string(), + optimizations_used: z.boolean(), + optimization_runs: z.number(), + libraries: z.null(), + data: z.object({ + main_contract: z.number(), + contract_info: z + .object({ + id: z.number(), + path: z.string(), + name: z.string(), + source: z.string(), + abi: z.array(z.unknown()), + raw_abi: z.array(z.unknown()), + states: z.array(z.unknown()), + }) + .nullable(), + creation_block: z.number(), + creation_tx: z.string(), + creator_address: web3AddressSchema, + created_at: z.string(), + number_of_watches: z.null(), + language: z.string(), + in_project: z.boolean(), + number_of_files: z.number(), + }), + account: z.object({ + id: z.string(), + contract_id: z.string(), + balance: z.string(), + network_id: z.string(), + public: z.boolean(), + export: z.boolean(), + verified_by: z.string(), + verification_date: z.string().nullable(), + address: web3AddressSchema, + contract_name: z.string(), + ens_domain: z.string().nullable(), + type: z.string(), + standard: z.string(), + standards: z.array(z.string()), + evm_version: z.string(), + compiler_version: z.string(), + optimizations_used: z.boolean(), + optimization_runs: z.number(), + libraries: z.null(), + data: z.null(), + creation_block: z.number(), + creation_tx: z.string(), + creator_address: web3AddressSchema, + created_at: z.string(), + number_of_watches: z.null(), + language: z.string(), + in_project: z.boolean(), + number_of_files: z.number(), + }), + project_id: z.string(), + added_by_id: z.string(), + previous_versions: z.array(z.unknown()), + details_visible: z.boolean(), + include_in_transaction_listing: z.boolean(), + display_name: z.string(), + account_type: z.string(), + verification_type: z.string(), + added_at: z.string(), +}); + +export const updateContractRequestSchema = z.object({ + displayName: z.string().optional(), + appendTags: z.array(z.string()).optional(), +}); + +export const solidityCompilerVersionsSchema = z + .string() + .regex(new RegExp('^v\\d+\\.\\d+\\.\\d+$'), { + message: 'Compiler version is not in supported format v[number].[number].[number]', + }); + +export const solcConfigSchema = z.object({ + version: solidityCompilerVersionsSchema, + sources: z.record( + z.object({ + content: z.string(), + }), + ), + settings: z.unknown(), +}); + +export const tenderlySolcConfigLibrariesSchema = z.record( + z.object({ + addresses: z.record(web3AddressSchema), + }), +); + +export const verificationRequestSchema = z.object({ + contractToVerify: z.string(), + solc: solcConfigSchema, + config: z.object({ + mode: z.union([z.literal('private'), z.literal('public')]), + }), +}); + +export const bytecodeMismatchErrorResponseSchema = z.object({ + contract_id: z.string(), + expected: z.string(), + got: z.string(), + similarity: z.number(), + assumed_reason: z.string(), +}); + +export const contractResponseSchema = z.object({ + id: z.string(), + account_type: z.literal('contract'), + contract: internalContractSchema, + display_name: z.string(), + tags: z + .array( + z.object({ + tag: z.string(), + }), + ) + .optional(), +}); + +export const compilationErrorResponseSchema = z.object({ + source_location: z.object({ + file: z.string(), + start: z.number(), + end: z.number(), + }), + error_ype: z.string(), + component: z.string(), + message: z.string(), + formatted_message: z.string(), +}); + +export const verificationResponseSchema = z.object({ + compilation_errors: z.array(compilationErrorResponseSchema), + results: z.array( + z.object({ + bytecode_mismatch_error: bytecodeMismatchErrorResponseSchema, + verified_contract: internalContractSchema, + }), + ), +}); diff --git a/lib/repositories/contracts/contracts.types.ts b/lib/repositories/contracts/contracts.types.ts index 1e39e87..d392275 100644 --- a/lib/repositories/contracts/contracts.types.ts +++ b/lib/repositories/contracts/contracts.types.ts @@ -1,173 +1,28 @@ -import { Network, Path } from '../../types'; - -export interface Contract { - address: string; - network: Network; - displayName?: string; - tags?: string[]; -} - +import { z } from 'zod'; +import { + bytecodeMismatchErrorResponseSchema, + compilationErrorResponseSchema, + contractRequestSchema, + contractResponseSchema, + contractSchema, + getByParamsSchema, + solcConfigSchema, + tenderlySolcConfigLibrariesSchema, + updateContractRequestSchema, + verificationRequestSchema, + verificationResponseSchema, +} from './contracts.schema'; + +export type Contract = z.infer; export type TenderlyContract = Contract; -export interface ContractRequest { - address: string; - network_id: string; - display_name?: string; -} - -export type GetByParams = { - tags?: string[]; - displayNames?: string[]; -}; - -export type ContractResponse = { - id: string; - account_type: 'contract'; - contract: InternalContract; - display_name: string; - tags?: { - tag: string; - }[]; -}; - -interface InternalContract { - id: string; - contract_id: string; - balance: string; - network_id: string; - public: boolean; - export: boolean; - verified_by: string; - verification_date: string | null; - address: string; - contract_name: string; - ens_domain: string | null; - type: string; - standard: string; - standards: string[]; - token_data: { - symbol: string; - name: string; - main: boolean; - }; - evm_version: string; - compiler_version: string; - optimizations_used: boolean; - optimization_runs: number; - libraries: null; - data: { - main_contract: number; - contract_info: { - id: number; - path: string; - name: string; - source: string; - - abi: unknown[]; - raw_abi: unknown[]; - states: unknown[]; - } | null; - creation_block: number; - creation_tx: string; - creator_address: string; - created_at: string; - number_of_watches: null; - language: string; - in_project: boolean; - number_of_files: number; - }; - account: { - id: string; - contract_id: string; - balance: string; - network_id: string; - public: boolean; - export: boolean; - verified_by: string; - verification_date: string | null; - address: string; - contract_name: string; - ens_domain: string | null; - type: string; - standard: string; - standards: string[]; - evm_version: string; - compiler_version: string; - optimizations_used: boolean; - optimization_runs: number; - libraries: null; - data: null; - creation_block: number; - creation_tx: string; - creator_address: string; - created_at: string; - number_of_watches: null; - language: string; - in_project: boolean; - number_of_files: number; - }; - project_id: string; - added_by_id: string; - previous_versions: unknown[]; - details_visible: boolean; - include_in_transaction_listing: boolean; - display_name: string; - account_type: string; - verification_type: string; - added_at: string; -} - -export type UpdateContractRequest = { - displayName?: string; - appendTags?: string[]; -}; - -export type SolidityCompilerVersions = `v${number}.${number}.${number}`; - -export type SolcConfig = { - version: SolidityCompilerVersions; - sources: Record; - settings: unknown; -}; - -export type TenderlySolcConfigLibraries = Record }>; - -export type VerificationRequest = { - contractToVerify: string; - solc: SolcConfig; - config: { - mode: 'private' | 'public'; - }; -}; - -export type VerificationResponse = { - compilation_errors: CompilationErrorResponse[]; - results: VerificationResult[]; -}; - -export type CompilationErrorResponse = { - source_location: SourceLocation; - error_ype: string; - component: string; - message: string; - formatted_message: string; -}; - -interface SourceLocation { - file: string; - start: number; - end: number; -} - -interface VerificationResult { - bytecode_mismatch_error: BytecodeMismatchErrorResponse; - verified_contract: InternalContract; -} - -export type BytecodeMismatchErrorResponse = { - contract_id: string; - expected: string; - got: string; - similarity: number; - assumed_reason: string; -}; +export type ContractRequest = z.infer; +export type GetByParams = z.infer; +export type ContractResponse = z.infer; +export type UpdateContractRequest = z.infer; +export type SolcConfig = z.infer; +export type TenderlySolcConfigLibraries = z.infer; +export type VerificationRequest = z.infer; +export type VerificationResponse = z.infer; +export type CompilationErrorResponse = z.infer; +export type BytecodeMismatchErrorResponse = z.infer; diff --git a/lib/repositories/wallets/wallets.repository.ts b/lib/repositories/wallets/wallets.repository.ts index 7e532e8..dbbb217 100644 --- a/lib/repositories/wallets/wallets.repository.ts +++ b/lib/repositories/wallets/wallets.repository.ts @@ -1,4 +1,4 @@ -import { Network, TenderlyConfiguration } from '../../types'; +import { Network, TenderlyConfiguration } from '../../common.types'; import { Repository } from '../Repository'; import { ApiClient } from '../../core/ApiClient'; import { diff --git a/lib/repositories/wallets/wallets.schema.ts b/lib/repositories/wallets/wallets.schema.ts new file mode 100644 index 0000000..683dc0b --- /dev/null +++ b/lib/repositories/wallets/wallets.schema.ts @@ -0,0 +1,59 @@ +import { z } from 'zod'; +import { Network } from '../../common.types'; +import { web3AddressSchema } from '../../common.schema'; + +export const walletSchema = z.object({ + address: web3AddressSchema, + displayName: z.string().optional(), + tags: z.array(z.string()).optional(), + network: z.nativeEnum(Network), +}); + +export const walletRequestSchema = z.object({ + address: web3AddressSchema, + display_name: z.string(), + network_ids: z.array(z.string()), +}); + +export const walletResponseSchema = z.object({ + id: z.string(), + account_type: z.literal('wallet'), + display_name: z.string(), + account: z.object({ + id: z.string(), + contract_id: z.string(), + balance: z.string(), + network_id: z.string(), + address: web3AddressSchema, + }), + contract: z + .object({ + id: z.string(), + contract_id: z.string(), + balance: z.string(), + network_id: z.string(), + address: web3AddressSchema, + }) + .optional(), + wallet: z + .object({ + id: z.string(), + contract_id: z.string(), + balance: z.string(), + network_id: z.string(), + address: web3AddressSchema, + }) + .optional(), + tags: z + .array( + z.object({ + tag: z.string(), + }), + ) + .optional(), +}); + +export const updateWalletRequestSchema = z.object({ + displayName: z.string().optional(), + appendTags: z.array(z.string()).optional(), +}); diff --git a/lib/repositories/wallets/wallets.types.ts b/lib/repositories/wallets/wallets.types.ts index 28de4d1..c535052 100644 --- a/lib/repositories/wallets/wallets.types.ts +++ b/lib/repositories/wallets/wallets.types.ts @@ -1,49 +1,14 @@ -import { Network } from '../../types'; - -export interface Wallet { - address: string; - displayName?: string; - tags?: string[]; - network: Network; -} - +import { z } from 'zod'; +import { + updateWalletRequestSchema, + walletRequestSchema, + walletResponseSchema, + walletSchema, +} from './wallets.schema'; + +export type Wallet = z.infer; export type TenderlyWallet = Wallet; -export type WalletRequest = { - address: string; - display_name: string; - network_ids: string[]; -}; - -export type WalletResponse = { - id: string; - account_type: 'wallet'; - display_name: string; - account: { - id: string; - contract_id: string; - balance: string; - network_id: string; - address: string; - }; - contract?: { - id: string; - contract_id: string; - balance: string; - network_id: string; - address: string; - }; - wallet?: { - id: string; - contract_id: string; - balance: string; - network_id: string; - address: string; - }; - tags?: { tag: string }[]; -}; - -export type UpdateWalletRequest = { - displayName?: string; - appendTags?: string[]; -}; +export type WalletRequest = z.infer; +export type WalletResponse = z.infer; +export type UpdateWalletRequest = z.infer; diff --git a/package.json b/package.json index 8749b84..f1c50da 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,8 @@ "ts-jest": "^29.0.5", "ts-node": "^10.9.1", "tsup": "^6.7.0", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "zod": "^3.21.4" }, "dependencies": { "axios": "^1.3.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79a2594..112e6eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,6 +79,9 @@ devDependencies: typescript: specifier: ^4.9.5 version: 4.9.5 + zod: + specifier: ^3.21.4 + version: 3.21.4 packages: @@ -5812,3 +5815,7 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true + + /zod@3.21.4: + resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} + dev: true diff --git a/test/contracts.repository.test.ts b/test/contracts.repository.test.ts index bf5a877..0e723cd 100644 --- a/test/contracts.repository.test.ts +++ b/test/contracts.repository.test.ts @@ -5,7 +5,10 @@ import { CompilationError, BytecodeMismatchError, getEnvironmentVariables, + Contract, } from '../lib'; +import { ZodError } from 'zod'; +import { contractSchema } from '../lib/repositories/contracts/contracts.schema'; const counterContractSource = ` // SPDX-License-Identifier: MIT @@ -181,12 +184,15 @@ describe('contracts.add', () => { test('successfully adds contract', async () => { const contract = await tenderly.contracts.add(lidoContract); + validateContracts([contract]); expect(contract?.address).toEqual(lidoContract); }); test(`adding contract twice doesn't throw an error`, async () => { await tenderly.contracts.add(lidoContract); const contract = await tenderly.contracts.add(lidoContract); + + validateContracts([contract]); expect(contract?.address).toEqual(lidoContract); }); @@ -195,6 +201,7 @@ describe('contracts.add', () => { displayName: 'Lido', }); + validateContracts([lidoContractResponse]); expect(lidoContractResponse?.address).toEqual(lidoContract); expect(lidoContractResponse?.displayName).toEqual('Lido'); // tags don't work yet @@ -205,6 +212,7 @@ describe('contracts.add', () => { await tenderly.contracts.add(lidoContract); const contract = await tenderly.contracts.add(lidoContract); + validateContracts([contract]); expect(contract?.address).toEqual(lidoContract); }); @@ -248,11 +256,14 @@ describe('contracts.get', () => { test('returns contract if it exists', async () => { const contract = await tenderly.contracts.get(kittyCoreContract); + validateContracts([contract]); expect(contract?.address).toEqual(kittyCoreContract); }); test('returns unverified contract if it exists', async () => { const contract = await tenderly.contracts.get(unverifiedContract); + + validateContracts([contract]); expect(contract?.address).toEqual(unverifiedContract); }); @@ -282,6 +293,7 @@ describe('contracts.update', () => { appendTags: ['NewTag', 'NewTag2'], }); + validateContracts([contract]); expect(contract?.address).toEqual(wrappedEtherContract); expect(contract?.displayName).toEqual('NewDisplayName'); expect(contract?.tags?.sort()).toEqual(['NewTag', 'NewTag2']); @@ -292,6 +304,7 @@ describe('contracts.update', () => { displayName: 'NewDisplayName', }); + validateContracts([contractResponse]); expect(contractResponse?.address).toEqual(wrappedEtherContract); expect(contractResponse?.displayName).toEqual('NewDisplayName'); }); @@ -301,6 +314,7 @@ describe('contracts.update', () => { appendTags: ['NewTag', 'NewTag2'], }); + validateContracts([contract]); expect(contract?.address).toEqual(wrappedEtherContract); expect(contract?.tags?.sort()).toEqual(['NewTag', 'NewTag2']); expect(contract?.displayName).toBeUndefined(); @@ -338,6 +352,7 @@ describe('contracts.verify', () => { }, }); + validateContracts([result]); expect(result?.address).toEqual(counterContract); }); @@ -371,6 +386,7 @@ describe('contracts.verify', () => { }, }); + validateContracts([verifiedContract]); expect(verifiedContract?.address).toEqual(libraryTokenContract); }); @@ -456,6 +472,7 @@ describe('contract.getBy', () => { test('returns 1 contract, when 1 tag matches (passed as 1 string, not an array)', async () => { const contracts = await getByTenderly.contracts.getBy({ tags: [tag1] }); + validateContracts(contracts); expect(contracts).toHaveLength(1); expect(contracts?.[0]?.address).toEqual(beaconDepositContract); expect(contracts?.[0]?.displayName).toEqual(beaconDepositContractDisplayName); @@ -473,6 +490,7 @@ describe('contract.getBy', () => { tags: [tag1], }); + validateContracts(contracts); expect(contracts).toHaveLength(1); expect(contracts?.[0]?.address).toEqual(beaconDepositContract); expect(contracts?.[0]?.displayName).toEqual(beaconDepositContractDisplayName); @@ -486,6 +504,7 @@ describe('contract.getBy', () => { a.address > b.address ? 1 : -1, ); + validateContracts(contracts); expect(contracts).toHaveLength(2); expect(contracts?.[0]?.address).toEqual(beaconDepositContract); expect(contracts?.[0]?.displayName).toEqual(beaconDepositContractDisplayName); @@ -502,6 +521,7 @@ describe('contract.getBy', () => { test('returns 1 contract, when `tag3` matches', async () => { const contracts = await getByTenderly.contracts.getBy({ tags: [tag3] }); + validateContracts(contracts); expect(contracts).toHaveLength(1); expect(contracts?.[0]?.address).toEqual(bitDAOTreasuryContract); expect(contracts?.[0]?.displayName).toEqual(bitDAOTreasuryContractDisplayName); @@ -515,6 +535,7 @@ describe('contract.getBy', () => { (a, b) => (a.address > b.address ? 1 : -1), ); + validateContracts(contracts); expect(contracts).toHaveLength(2); expect(contracts?.[0]?.address).toEqual(beaconDepositContract); expect(contracts?.[0]?.displayName).toEqual(beaconDepositContractDisplayName); @@ -533,6 +554,7 @@ describe('contract.getBy', () => { (a, b) => (a.address > b.address ? 1 : -1), ); + validateContracts(contracts); expect(contracts).toHaveLength(2); expect(contracts?.[0]?.address).toEqual(beaconDepositContract); expect(contracts?.[0]?.displayName).toEqual(beaconDepositContractDisplayName); @@ -551,6 +573,7 @@ describe('contract.getBy', () => { a.address > b.address ? 1 : -1, ); + validateContracts(contracts); expect(contracts).toHaveLength(2); expect(contracts?.[0]?.address).toEqual(beaconDepositContract); expect(contracts?.[0]?.displayName).toEqual(beaconDepositContractDisplayName); @@ -569,6 +592,7 @@ describe('contract.getBy', () => { a.address > b.address ? 1 : -1, ); + validateContracts(contracts); expect(contracts).toHaveLength(2); expect(contracts?.[0]?.address).toEqual(beaconDepositContract); expect(contracts?.[0]?.displayName).toEqual(beaconDepositContractDisplayName); @@ -589,6 +613,7 @@ describe('contract.getBy', () => { displayNames: [beaconDepositContractDisplayName], }); + validateContracts(contractsResponse); expect(contractsResponse).toHaveLength(1); expect(contractsResponse?.[0]?.address).toEqual(beaconDepositContract); expect(contractsResponse?.[0]?.displayName).toEqual(beaconDepositContractDisplayName); @@ -608,6 +633,7 @@ describe('contract.getBy', () => { a.address > b.address ? 1 : -1, ); + validateContracts(contractsResponse); expect(contractsResponse).toHaveLength(2); expect(contractsResponse?.[0]?.address).toEqual(beaconDepositContract); expect(contractsResponse?.[0]?.displayName).toEqual(beaconDepositContractDisplayName); @@ -624,6 +650,7 @@ describe('contract.getBy', () => { }) )?.sort((a, b) => (a.address > b.address ? 1 : -1)); + validateContracts(contractsResponse); expect(contractsResponse).toHaveLength(2); expect(contractsResponse?.[0]?.address).toEqual(beaconDepositContract); expect(contractsResponse?.[0]?.displayName).toEqual(beaconDepositContractDisplayName); @@ -639,3 +666,13 @@ test('Tenderly.with() overide works', () => { const newHandle = tenderly.with({ accountName: 'newAccountName' }); expect(newHandle.configuration.accountName).toEqual('newAccountName'); }); + +function validateContracts(contracts: (Contract | undefined)[] | undefined) { + if (!contracts) throw new Error('Contracts are not defined'); + + expect(() => + contracts.forEach(contract => { + return contractSchema.parse(contract); + }), + ).not.toThrow(ZodError); +} diff --git a/test/wallets.repository.test.ts b/test/wallets.repository.test.ts index 74e1251..4733b45 100644 --- a/test/wallets.repository.test.ts +++ b/test/wallets.repository.test.ts @@ -1,4 +1,6 @@ -import { Tenderly, Network, NotFoundError, getEnvironmentVariables } from '../lib'; +import { Tenderly, Network, NotFoundError, getEnvironmentVariables, Wallet } from '../lib'; +import { walletSchema } from '../lib/repositories/wallets/wallets.schema'; +import { ZodError } from 'zod'; jest.setTimeout(60000); @@ -62,6 +64,7 @@ describe('wallets.add', () => { test('successfully adds wallet', async () => { const wallet = await tenderly.wallets.add(walletAddress); + validateWallets([wallet]); expect(wallet?.address).toEqual(walletAddress); }); @@ -70,6 +73,7 @@ describe('wallets.add', () => { displayName: 'VB3', }); + validateWallets([wallet]); expect(wallet?.address).toEqual(walletAddress); expect(wallet?.displayName).toEqual('VB3'); // tags don't work yet @@ -80,6 +84,8 @@ describe('wallets.add', () => { test(`doesn't throw when adding existing wallet, and returns wallet model`, async () => { await tenderly.wallets.add(walletAddress); const existingWallet = await tenderly.wallets.add(walletAddress); + + validateWallets([existingWallet]); expect(existingWallet?.address).toEqual(walletAddress); }); }); @@ -91,7 +97,7 @@ describe('wallets.remove', () => { // FIXME: This should not throw, but currently that is what the API does test(`doesn't throw when removing non existing wallet`, async () => { - tenderly.wallets.remove('0xfake_wallet_address'); + await tenderly.wallets.remove('0xfake_wallet_address'); }); }); @@ -99,6 +105,7 @@ describe('wallets.get', () => { test('returns wallet if it exists', async () => { const walletResponse = await tenderly.wallets.get(someOtherWalletAddress); + validateWallets([walletResponse]); expect(walletResponse?.address).toEqual(someOtherWalletAddress); }); @@ -132,6 +139,7 @@ describe('wallets.update', () => { appendTags: [tag1, tag2], }); + validateWallets([wallet]); expect(wallet?.address).toEqual(someThirdWallet); expect(wallet?.displayName).toEqual(displayName); expect(wallet?.tags?.sort()).toEqual([tag1, tag2]); @@ -142,6 +150,7 @@ describe('wallets.update', () => { displayName, }); + validateWallets([wallet]); expect(wallet?.address).toEqual(someThirdWallet); expect(wallet?.displayName).toEqual(displayName); expect(wallet?.tags).toBeUndefined(); @@ -152,6 +161,7 @@ describe('wallets.update', () => { appendTags: [tag1, tag2], }); + validateWallets([wallet]); expect(wallet?.address).toEqual(someThirdWallet); expect(wallet?.displayName).toBeUndefined(); expect(wallet?.tags?.sort()).toEqual(expect.arrayContaining([tag1, tag2])); @@ -184,10 +194,7 @@ describe('wallets.getBy', () => { test('returns 1 wallet, when 1 tag matches (passed as 1 string, not an array)', async () => { const wallets = await getByTenderly.wallets.getBy({ tags: [tag1] }); - if (!wallets) { - throw new Error('Wallets are not defined'); - } - + validateWallets(wallets); expect(wallets).toHaveLength(1); expect(wallets?.[0]?.address).toEqual(binance7WalletAddress); expect(wallets?.[0]?.displayName).toEqual(binance7WalletDisplayName); @@ -203,10 +210,7 @@ describe('wallets.getBy', () => { test('returns 1 wallet, when `tag1` matches', async () => { const wallets = await getByTenderly.wallets.getBy({ tags: [tag1] }); - if (!wallets) { - throw new Error('Wallets are not defined'); - } - + validateWallets(wallets); expect(wallets).toHaveLength(1); expect(wallets?.[0]?.address).toEqual(binance7WalletAddress); expect(wallets?.[0]?.displayName).toEqual(binance7WalletDisplayName); @@ -218,10 +222,7 @@ describe('wallets.getBy', () => { a.address > b.address ? 1 : -1, ); - if (!wallets) { - throw new Error('Wallets are not defined'); - } - + validateWallets(wallets); expect(wallets).toHaveLength(2); expect(wallets?.[0]?.address).toEqual(binance7WalletAddress); expect(wallets?.[0]?.displayName).toEqual(binance7WalletDisplayName); @@ -233,13 +234,12 @@ describe('wallets.getBy', () => { test('returns 1 wallet, when `tag3` matches', async () => { const wallets = await getByTenderly.wallets.getBy({ tags: [tag3] }); - if (!wallets) { - throw new Error('Wallets are not defined'); - } + + validateWallets(wallets); expect(wallets).toHaveLength(1); - expect(wallets[0]?.address).toEqual(binance8WalletAddress); - expect(wallets[0]?.displayName).toEqual(binance8WalletDisplayName); - expect(wallets[0]?.tags?.sort()).toEqual(binance8WalletTags.sort()); + expect(wallets?.[0]?.address).toEqual(binance8WalletAddress); + expect(wallets?.[0]?.displayName).toEqual(binance8WalletDisplayName); + expect(wallets?.[0]?.tags?.sort()).toEqual(binance8WalletTags.sort()); }); test('returns 2 wallets, when any of 3 tags match', async () => { @@ -247,17 +247,14 @@ describe('wallets.getBy', () => { (a, b) => (a.address > b.address ? 1 : -1), ); - if (!wallets) { - throw new Error('Wallets are not defined'); - } - + validateWallets(wallets); expect(wallets).toHaveLength(2); - expect(wallets[0]?.address).toEqual(binance7WalletAddress); - expect(wallets[0]?.displayName).toEqual(binance7WalletDisplayName); - expect(wallets[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); - expect(wallets[1]?.address).toEqual(binance8WalletAddress); - expect(wallets[1]?.displayName).toEqual(binance8WalletDisplayName); - expect(wallets[1]?.tags?.sort()).toEqual(binance8WalletTags.sort()); + expect(wallets?.[0]?.address).toEqual(binance7WalletAddress); + expect(wallets?.[0]?.displayName).toEqual(binance7WalletDisplayName); + expect(wallets?.[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); + expect(wallets?.[1]?.address).toEqual(binance8WalletAddress); + expect(wallets?.[1]?.displayName).toEqual(binance8WalletDisplayName); + expect(wallets?.[1]?.tags?.sort()).toEqual(binance8WalletTags.sort()); }); test("returns 2 wallets, when both tags that don't overlap are passed", async () => { @@ -265,17 +262,14 @@ describe('wallets.getBy', () => { a.address > b.address ? 1 : -1, ); - if (!wallets) { - throw new Error('Wallets are not defined'); - } - + validateWallets(wallets); expect(wallets).toHaveLength(2); - expect(wallets[0]?.address).toEqual(binance7WalletAddress); - expect(wallets[0]?.displayName).toEqual(binance7WalletDisplayName); - expect(wallets[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); - expect(wallets[1]?.address).toEqual(binance8WalletAddress); - expect(wallets[1]?.displayName).toEqual(binance8WalletDisplayName); - expect(wallets[1]?.tags?.sort()).toEqual(binance8WalletTags.sort()); + expect(wallets?.[0]?.address).toEqual(binance7WalletAddress); + expect(wallets?.[0]?.displayName).toEqual(binance7WalletDisplayName); + expect(wallets?.[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); + expect(wallets?.[1]?.address).toEqual(binance8WalletAddress); + expect(wallets?.[1]?.displayName).toEqual(binance8WalletDisplayName); + expect(wallets?.[1]?.tags?.sort()).toEqual(binance8WalletTags.sort()); }); test('returns 2 wallets, when no tags are passed', async () => { @@ -283,17 +277,14 @@ describe('wallets.getBy', () => { a.address > b.address ? 1 : -1, ); - if (!wallets) { - throw new Error('Wallets are not defined'); - } - + validateWallets(wallets); expect(wallets).toHaveLength(2); - expect(wallets[0]?.address).toEqual(binance7WalletAddress); - expect(wallets[0]?.displayName).toEqual(binance7WalletDisplayName); - expect(wallets[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); - expect(wallets[1]?.address).toEqual(binance8WalletAddress); - expect(wallets[1]?.displayName).toEqual(binance8WalletDisplayName); - expect(wallets[1]?.tags?.sort()).toEqual(binance8WalletTags.sort()); + expect(wallets?.[0]?.address).toEqual(binance7WalletAddress); + expect(wallets?.[0]?.displayName).toEqual(binance7WalletDisplayName); + expect(wallets?.[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); + expect(wallets?.[1]?.address).toEqual(binance8WalletAddress); + expect(wallets?.[1]?.displayName).toEqual(binance8WalletDisplayName); + expect(wallets?.[1]?.tags?.sort()).toEqual(binance8WalletTags.sort()); }); test('returns 2 wallets, when empty array is passed', async () => { @@ -301,17 +292,14 @@ describe('wallets.getBy', () => { a.address > b.address ? 1 : -1, ); - if (!wallets) { - throw new Error('Wallets are not defined'); - } - + validateWallets(wallets); expect(wallets).toHaveLength(2); - expect(wallets[0]?.address).toEqual(binance7WalletAddress); - expect(wallets[0]?.displayName).toEqual(binance7WalletDisplayName); - expect(wallets[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); - expect(wallets[1]?.address).toEqual(binance8WalletAddress); - expect(wallets[1]?.displayName).toEqual(binance8WalletDisplayName); - expect(wallets[1]?.tags?.sort()).toEqual(binance8WalletTags.sort()); + expect(wallets?.[0]?.address).toEqual(binance7WalletAddress); + expect(wallets?.[0]?.displayName).toEqual(binance7WalletDisplayName); + expect(wallets?.[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); + expect(wallets?.[1]?.address).toEqual(binance8WalletAddress); + expect(wallets?.[1]?.displayName).toEqual(binance8WalletDisplayName); + expect(wallets?.[1]?.tags?.sort()).toEqual(binance8WalletTags.sort()); }); }); @@ -321,13 +309,11 @@ describe('wallets.getBy', () => { displayNames: [binance7WalletDisplayName], }); - if (!wallets) { - throw new Error('Wallets are not defined'); - } + validateWallets(wallets); expect(wallets).toHaveLength(1); - expect(wallets[0]?.address).toEqual(binance7WalletAddress); - expect(wallets[0]?.displayName).toEqual(binance7WalletDisplayName); - expect(wallets[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); + expect(wallets?.[0]?.address).toEqual(binance7WalletAddress); + expect(wallets?.[0]?.displayName).toEqual(binance7WalletDisplayName); + expect(wallets?.[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); }); test('returns 0 wallets, when displayName does not match', async () => { @@ -343,17 +329,14 @@ describe('wallets.getBy', () => { a.address > b.address ? 1 : -1, ); - if (!wallets) { - throw new Error('Wallets are not defined'); - } - + validateWallets(wallets); expect(wallets).toHaveLength(2); - expect(wallets[0]?.address).toEqual(binance7WalletAddress); - expect(wallets[0]?.displayName).toEqual(binance7WalletDisplayName); - expect(wallets[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); - expect(wallets[1]?.address).toEqual(binance8WalletAddress); - expect(wallets[1]?.displayName).toEqual(binance8WalletDisplayName); - expect(wallets[1]?.tags?.sort()).toEqual(binance8WalletTags.sort()); + expect(wallets?.[0]?.address).toEqual(binance7WalletAddress); + expect(wallets?.[0]?.displayName).toEqual(binance7WalletDisplayName); + expect(wallets?.[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); + expect(wallets?.[1]?.address).toEqual(binance8WalletAddress); + expect(wallets?.[1]?.displayName).toEqual(binance8WalletDisplayName); + expect(wallets?.[1]?.tags?.sort()).toEqual(binance8WalletTags.sort()); }); test('returns 2 contracts, when both displayNames match', async () => { @@ -363,17 +346,24 @@ describe('wallets.getBy', () => { }) )?.sort((a, b) => (a.address > b.address ? 1 : -1)); - if (!wallets) { - throw new Error('Wallets are not defined'); - } - + validateWallets(wallets); expect(wallets).toHaveLength(2); - expect(wallets[0]?.address).toEqual(binance7WalletAddress); - expect(wallets[0]?.displayName).toEqual(binance7WalletDisplayName); - expect(wallets[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); - expect(wallets[1]?.address).toEqual(binance8WalletAddress); - expect(wallets[1]?.displayName).toEqual(binance8WalletDisplayName); - expect(wallets[1]?.tags?.sort()).toEqual(binance8WalletTags.sort()); + expect(wallets?.[0]?.address).toEqual(binance7WalletAddress); + expect(wallets?.[0]?.displayName).toEqual(binance7WalletDisplayName); + expect(wallets?.[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); + expect(wallets?.[1]?.address).toEqual(binance8WalletAddress); + expect(wallets?.[1]?.displayName).toEqual(binance8WalletDisplayName); + expect(wallets?.[1]?.tags?.sort()).toEqual(binance8WalletTags.sort()); }); }); }); + +function validateWallets(wallets: (Wallet | undefined)[] | undefined) { + if (!wallets) throw new Error('Wallets are not defined'); + + expect(() => + wallets.forEach(wallet => { + return walletSchema.parse(wallet); + }), + ).not.toThrow(ZodError); +}