Skip to content
Open
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
16 changes: 16 additions & 0 deletions .changeset/zod-v4-upgrade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@reown/appkit-wallet': patch
'@reown/appkit-experimental': patch
---

Upgrade `zod` from `3.22.4` to `4.4.3`.

- `@reown/appkit-wallet`: refactored `W3mFrameSchema` to use `z.discriminatedUnion('type', [...])` and `z.intersection(...)` (replacing long `.or().or()...and()` chains) to keep TypeScript inference within bounds with Zod 4's stricter generics. Updated `z.string().email()` to the top-level `z.email()` per Zod 4 API.
- `@reown/appkit-experimental`: migrated the smart-session schema to Zod 4 APIs:
- `errorMap` / `invalid_type_error` → unified `error` parameter
- `z.nativeEnum(X)` → `z.enum(X)` (now accepts native enums directly)
- `z.record(V)` → `z.record(K, V)` (single-arg signature removed)
- `ZodError.errors` → `ZodError.issues`
- Default Zod error messages now use the `Invalid input: expected X, received Y` format; updated tests and `ERROR_MESSAGES` constants accordingly.

No public API changes. Consumers that already pin a specific `zod` version in their own apps may see deduplication via overrides; the wallet's runtime postMessage validation behavior is preserved.
2 changes: 1 addition & 1 deletion packages/experimental/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"lit": "3.3.0",
"valtio": "2.1.7",
"viem": "2.45.0",
"zod": "3.22.4"
"zod": "4.4.3"
},
"devDependencies": {
"@vitest/coverage-v8": "2.1.9",
Expand Down
2 changes: 1 addition & 1 deletion packages/experimental/src/smart-session/helper/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function validateRequest(request: SmartSessionGrantPermissionsRequest) {
return SmartSessionGrantPermissionsRequestSchema.parse(request)
} catch (e) {
if (e instanceof ZodError) {
const formattedErrors = e.errors
const formattedErrors = e.issues
.map(err => `Invalid ${err.path.join('.') || 'Unknown field'}: ${err.message}`)
.join('; ')
throw new Error(formattedErrors)
Expand Down
18 changes: 8 additions & 10 deletions packages/experimental/src/smart-session/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ export const ERROR_MESSAGES = {
INVALID_PUBLIC_KEY_FORMAT: 'Invalid public key: must start with "0x"',
//PermissionSchema
INVALID_PERMISSIONS: 'Invalid permissions: must be a non-empty array',
INVALID_PERMISSIONS_TYPE: 'Invalid permissions: Expected array, received object',
INVALID_PERMISSIONS_TYPE: 'Invalid permissions: Invalid input: expected array, received object',

INVALID_ALLOWANCE_FORMAT: 'Invalid allowance: must be a hexadecimal string starting with "0x"',
INVALID_START: 'Invalid start time: must be a positive integer and in the future',
INVALID_PERIOD: 'Invalid period: must be a positive integer',
//PolicySchema
INVALID_POLICIES: 'Invalid policies: must be a non-empty array',
INVALID_POLICIES_TYPE: 'Invalid policies: Expected array, received object',
INVALID_POLICIES_TYPE: 'Invalid policies: Invalid input: expected array, received object',

INVALID_GRANT_PERMISSIONS_RESPONSE: 'Invalid grantPermissions response'
}
Expand All @@ -46,9 +46,7 @@ const ChainIdSchema = z

// Address Schema
const AddressSchema = z
.string({
invalid_type_error: ERROR_MESSAGES.INVALID_ADDRESS
})
.string({ error: ERROR_MESSAGES.INVALID_ADDRESS })
.startsWith('0x', { message: ERROR_MESSAGES.INVALID_ADDRESS })
.optional()

Expand All @@ -63,7 +61,7 @@ const ExpirySchema = z
// Key Schema
const KeySchema = z.object({
type: z.enum(['secp256r1', 'secp256k1'], {
errorMap: () => ({ message: ERROR_MESSAGES.UNSUPPORTED_KEY_TYPE })
error: () => ERROR_MESSAGES.UNSUPPORTED_KEY_TYPE
}),
publicKey: z.string().refine(val => val.startsWith('0x'), {
message: ERROR_MESSAGES.INVALID_PUBLIC_KEY_FORMAT
Expand All @@ -80,7 +78,7 @@ const SignerSchema = z.object({

// Argument Condition Schema
const ArgumentConditionSchema = z.object({
operator: z.nativeEnum(ParamOperator, { errorMap: () => ({ message: 'Invalid operator type' }) }),
operator: z.enum(ParamOperator, { error: () => 'Invalid operator type' }),
value: z.string().startsWith('0x', { message: ERROR_MESSAGES.INVALID_ADDRESS })
})

Expand All @@ -89,15 +87,15 @@ const FunctionPermissionSchema = z.object({
functionName: z.string(),
args: z.array(ArgumentConditionSchema).optional(),
valueLimit: z.string().startsWith('0x', { message: ERROR_MESSAGES.INVALID_ADDRESS }).optional(),
operation: z.nativeEnum(Operation).optional()
operation: z.enum(Operation).optional()
})

// Contract Call Permission Schema
const ContractCallPermissionSchema = z.object({
type: z.literal('contract-call'),
data: z.object({
address: z.string().startsWith('0x', { message: ERROR_MESSAGES.INVALID_ADDRESS }),
abi: z.array(z.record(z.unknown())),
abi: z.array(z.record(z.string(), z.unknown())),
functions: z.array(FunctionPermissionSchema)
})
})
Expand Down Expand Up @@ -142,7 +140,7 @@ const PermissionSchema = z.union([
// Policies Schema
const PolicySchema = z.object({
type: z.string(),
data: z.record(z.unknown())
data: z.record(z.string(), z.unknown())
})

// Smart Session Grant Permissions Request Schema
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ describe('Common field validation', () => {

it('should fail for missing chainId', () => {
const { chainId, ...requestWithoutChainId } = testRequest
expect(() => validateRequest(requestWithoutChainId as any)).toThrow('Invalid chainId: Required')
expect(() => validateRequest(requestWithoutChainId as any)).toThrow(
'Invalid chainId: Invalid input: expected string, received undefined'
)
})

describe('ChainIdSchema Validation', () => {
Expand Down Expand Up @@ -164,19 +166,21 @@ describe('Common field validation', () => {
it('should fail for a non-number expiry', () => {
const request = { ...testRequest, expiry: '1234567890' as any }
expect(() => validateRequest(request)).toThrow(
'Invalid expiry: Expected number, received string'
'Invalid expiry: Invalid input: expected number, received string'
)
})

it('should fail for an undefined expiry', () => {
const { expiry, ...requestWithoutExpiry } = testRequest
expect(() => validateRequest(requestWithoutExpiry as any)).toThrow('Invalid expiry: Required')
expect(() => validateRequest(requestWithoutExpiry as any)).toThrow(
'Invalid expiry: Invalid input: expected number, received undefined'
)
})

it('should fail for a null expiry', () => {
const request = { ...testRequest, expiry: null as any }
expect(() => validateRequest(request)).toThrow(
'Invalid expiry: Expected number, received null'
'Invalid expiry: Invalid input: expected number, received null'
)
})

Expand Down Expand Up @@ -227,15 +231,19 @@ describe('Common field validation', () => {
...testRequest,
signer: { type: 'keys' } as any
}
expect(() => validateRequest(request)).toThrow('Invalid signer.data: Required')
expect(() => validateRequest(request)).toThrow(
'Invalid signer.data: Invalid input: expected object, received undefined'
)
})

it('should fail for missing keys in multi-key signer', () => {
const request = {
...testRequest,
signer: { type: 'keys', data: {} } as any
}
expect(() => validateRequest(request)).toThrow('Invalid signer.data.keys: Required')
expect(() => validateRequest(request)).toThrow(
'Invalid signer.data.keys: Invalid input: expected array, received undefined'
)
})

it('should fail for non-object signer', () => {
Expand All @@ -244,7 +252,7 @@ describe('Common field validation', () => {
signer: 'invalid' as any
}
expect(() => validateRequest(request)).toThrow(
'Invalid signer: Expected object, received string'
'Invalid signer: Invalid input: expected object, received string'
)
})

Expand All @@ -254,7 +262,7 @@ describe('Common field validation', () => {
signer: null as any
}
expect(() => validateRequest(request)).toThrow(
'Invalid signer: Expected object, received null'
'Invalid signer: Invalid input: expected object, received null'
)
})

Expand Down Expand Up @@ -319,7 +327,7 @@ describe('Common field validation', () => {
policies: [{ invalidKey: 'value' }] as any
}
expect(() => validateRequest(request)).toThrow(
'Invalid policies.0.type: Required; Invalid policies.0.data: Required'
'Invalid policies.0.type: Invalid input: expected string, received undefined; Invalid policies.0.data: Invalid input: expected record, received undefined'
)
})

Expand All @@ -328,15 +336,19 @@ describe('Common field validation', () => {
...testRequest,
policies: [{ data: { key: 'value' } }] as any
}
expect(() => validateRequest(request)).toThrow('Invalid policies.0.type: Required')
expect(() => validateRequest(request)).toThrow(
'Invalid policies.0.type: Invalid input: expected string, received undefined'
)
})

it('should fail for policies with missing data', () => {
const request = {
...testRequest,
policies: [{ type: 'someType' }] as any
}
expect(() => validateRequest(request)).toThrow('Invalid policies.0.data: Required')
expect(() => validateRequest(request)).toThrow(
'Invalid policies.0.data: Invalid input: expected record, received undefined'
)
})

it('should fail for policies with non-object data', () => {
Expand All @@ -345,7 +357,7 @@ describe('Common field validation', () => {
policies: [{ type: 'someType', data: 'invalidData' }] as any
}
expect(() => validateRequest(request)).toThrow(
'Invalid policies.0.data: Expected object, received string'
'Invalid policies.0.data: Invalid input: expected record, received string'
)
})
})
Expand Down
2 changes: 1 addition & 1 deletion packages/wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"@reown/appkit-common": "workspace:*",
"@reown/appkit-polyfills": "workspace:*",
"@walletconnect/logger": "3.0.2",
"zod": "3.22.4"
"zod": "4.4.3"
},
"author": "Reown (https://discord.gg/reown)",
"license": "SEE LICENSE IN LICENSE.md",
Expand Down
Loading
Loading