diff --git a/packages/web3-eth-contract/test/unit/contract.test.ts b/packages/web3-eth-contract/test/unit/contract.test.ts index c870f56e255..a9a0c863242 100644 --- a/packages/web3-eth-contract/test/unit/contract.test.ts +++ b/packages/web3-eth-contract/test/unit/contract.test.ts @@ -508,7 +508,7 @@ describe('Contract', () => { expect(error).toBeInstanceOf(Web3ValidatorError); // eslint-disable-next-line jest/no-conditional-expect expect((error as Web3ValidatorError).message).toBe( - 'Web3 validator found 1 error[s]:\nmust NOT have more than 1 items', + 'Web3 validator found 2 error[s]:\nmust NOT have more than 1 items\nvalue "true" at "/1" must pass "string" validation', ); } diff --git a/packages/web3-validator/CHANGELOG.md b/packages/web3-validator/CHANGELOG.md index d63f8cfe25c..18ffb62c040 100644 --- a/packages/web3-validator/CHANGELOG.md +++ b/packages/web3-validator/CHANGELOG.md @@ -162,4 +162,8 @@ Documentation: - Fixed an issue with detecting Uint8Array (#6486) -## [Unreleased] \ No newline at end of file +## [Unreleased] + +### Fixed + +- Multi-dimensional arrays(with a fix length) are now handled properly when parsing ABIs (#6798) diff --git a/packages/web3-validator/src/utils.ts b/packages/web3-validator/src/utils.ts index f7d632d1dc9..36a65e3f122 100644 --- a/packages/web3-validator/src/utils.ts +++ b/packages/web3-validator/src/utils.ts @@ -177,6 +177,7 @@ export const abiSchemaToJsonSchema = ( for (let i = arraySizes.length - 1; i > 0; i -= 1) { childSchema = { type: 'array', + $id: abiName, items: [], maxItems: arraySizes[i], minItems: arraySizes[i], @@ -192,7 +193,7 @@ export const abiSchemaToJsonSchema = ( lastSchema.items = [lastSchema.items as JsonSchema, childSchema]; } // lastSchema.items is an empty Scheme array, set it to 'childSchema' else if (lastSchema.items.length === 0) { - lastSchema.items = childSchema; + lastSchema.items = [childSchema]; } // lastSchema.items is a non-empty Scheme array, append 'childSchema' else { lastSchema.items.push(childSchema); @@ -205,43 +206,31 @@ export const abiSchemaToJsonSchema = ( nestedTuple.$id = abiName; (lastSchema.items as JsonSchema[]).push(nestedTuple); } else if (baseType === 'tuple' && isArray) { - const arraySize = arraySizes[0]; - const item: JsonSchema = { - $id: abiName, - type: 'array', - items: abiSchemaToJsonSchema(abiComponents, abiName), - maxItems: arraySize, - minItems: arraySize, - }; - - if (arraySize < 0) { - delete item.maxItems; - delete item.minItems; - } - - (lastSchema.items as JsonSchema[]).push(item); + const arraySize = arraySizes[0]; + const item: JsonSchema = { + type: 'array', + $id: abiName, + items: abiSchemaToJsonSchema(abiComponents, abiName), + ...(arraySize >= 0 && { minItems: arraySize, maxItems: arraySize }), + }; + + (lastSchema.items as JsonSchema[]).push(item); } else if (isArray) { - const arraySize = arraySizes[0]; - const item: JsonSchema = { - type: 'array', - $id: abiName, - items: convertEthType(String(baseType)), - minItems: arraySize, - maxItems: arraySize, - }; - - if (arraySize < 0) { - delete item.maxItems; - delete item.minItems; - } - - (lastSchema.items as JsonSchema[]).push(item); + const arraySize = arraySizes[0]; + const item: JsonSchema = { + type: 'array', + $id: abiName, + items: convertEthType(abiType), + ...(arraySize >= 0 && { minItems: arraySize, maxItems: arraySize }), + }; + + (lastSchema.items as JsonSchema[]).push(item); } else if (Array.isArray(lastSchema.items)) { // Array of non-tuple items lastSchema.items.push({ $id: abiName, ...convertEthType(abiType) }); } else { // Nested object - ((lastSchema.items as JsonSchema).items as JsonSchema[]).push({ + (lastSchema.items as JsonSchema[]).push({ $id: abiName, ...convertEthType(abiType), }); @@ -505,4 +494,4 @@ export function ensureIfUint8Array(data: T) { return Uint8Array.from(data as unknown as Uint8Array); } return data; -} \ No newline at end of file +} diff --git a/packages/web3-validator/src/validator.ts b/packages/web3-validator/src/validator.ts index d477a06d96b..82d7a596052 100644 --- a/packages/web3-validator/src/validator.ts +++ b/packages/web3-validator/src/validator.ts @@ -44,7 +44,9 @@ const convertToZod = (schema: JsonSchema): ZodType => { } if (schema?.type === 'array' && schema?.items) { - if (Array.isArray(schema.items) && schema.items.length > 0) { + if (Array.isArray(schema.items) && schema.items.length > 1 + && schema.maxItems !== undefined + && new Set(schema.items.map((item: JsonSchema) => item.$id)).size === schema.items.length) { const arr: Partial<[ZodTypeAny, ...ZodTypeAny[]]> = []; for (const item of schema.items) { const zItem = convertToZod(item); @@ -54,7 +56,12 @@ const convertToZod = (schema: JsonSchema): ZodType => { } return z.tuple(arr as [ZodTypeAny, ...ZodTypeAny[]]); } - return z.array(convertToZod(schema.items as JsonSchema)); + const nextSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items; + let zodArraySchema = z.array(convertToZod(nextSchema)); + + zodArraySchema = schema.minItems !== undefined ? zodArraySchema.min(schema.minItems) : zodArraySchema; + zodArraySchema = schema.maxItems !== undefined ? zodArraySchema.max(schema.maxItems) : zodArraySchema; + return zodArraySchema; } if (schema.oneOf && Array.isArray(schema.oneOf)) { diff --git a/packages/web3-validator/test/fixtures/abi_to_json_schema.ts b/packages/web3-validator/test/fixtures/abi_to_json_schema.ts index 1a2cb7bd36f..35ed35bce6f 100644 --- a/packages/web3-validator/test/fixtures/abi_to_json_schema.ts +++ b/packages/web3-validator/test/fixtures/abi_to_json_schema.ts @@ -712,49 +712,55 @@ const abiToJsonSchemaCases: AbiToJsonSchemaCase[] = [ }, json: { fullSchema: { - type: 'array', - items: { - type: 'array', - items: [ - { - type: 'array', - $id: 'a', - items: { - format: 'uint', - required: true, - }, - minItems: 2, - maxItems: 2, - }, - ], - maxItems: 3, - minItems: 3, - }, - maxItems: 1, - minItems: 1, - }, + type: 'array', + items: [ + { + type: 'array', + $id: 'a', + items: [ + { + type: 'array', + $id: 'a', + items: { + format: 'uint', + required: true, + }, + minItems: 2, + maxItems: 2, + } + ], + maxItems: 3, + minItems: 3, + } + ], + maxItems: 1, + minItems: 1, + }, shortSchema: { - type: 'array', - items: { - type: 'array', - items: [ - { - type: 'array', - $id: '/0/0', - items: { - format: 'uint', - required: true, - }, - minItems: 2, - maxItems: 2, - }, - ], - maxItems: 3, - minItems: 3, - }, - maxItems: 1, - minItems: 1, - }, + type: 'array', + items: [ + { + type: 'array', + $id: '/0/0', + items: [ + { + type: 'array', + $id: '/0/0', + items: { + format: 'uint', + required: true, + }, + minItems: 2, + maxItems: 2, + } + ], + maxItems: 3, + minItems: 3, + } + ], + maxItems: 1, + minItems: 1, + }, data: [ [ [1, 1], @@ -785,45 +791,51 @@ const abiToJsonSchemaCases: AbiToJsonSchemaCase[] = [ }, json: { fullSchema: { - type: 'array', - items: { - type: 'array', - items: [ - { - type: 'array', - $id: 'a', - items: { - format: 'uint', - required: true, - }, - }, - ], - maxItems: 3, - minItems: 3, - }, - maxItems: 1, - minItems: 1, - }, + type: 'array', + items: [ + { + type: 'array', + $id: 'a', + items: [ + { + type: 'array', + $id: 'a', + items: { + format: 'uint', + required: true, + } + } + ], + maxItems: 3, + minItems: 3, + } + ], + maxItems: 1, + minItems: 1, + }, shortSchema: { - type: 'array', - items: { - type: 'array', - items: [ - { - type: 'array', - $id: '/0/0', - items: { - format: 'uint', - required: true, - }, - }, - ], - maxItems: 3, - minItems: 3, - }, - maxItems: 1, - minItems: 1, - }, + type: 'array', + items: [ + { + type: 'array', + $id: '/0/0', + items: [ + { + type: 'array', + $id: '/0/0', + items: { + format: 'uint', + required: true, + } + } + ], + maxItems: 3, + minItems: 3, + } + ], + maxItems: 1, + minItems: 1, + }, data: [ [ [1, 1], @@ -878,71 +890,77 @@ const abiToJsonSchemaCases: AbiToJsonSchemaCase[] = [ }, json: { fullSchema: { - type: 'array', - items: { - type: 'array', - items: [ - { - $id: 'a', - type: 'array', - items: { - type: 'array', - items: [ - { - $id: 'level', - format: 'uint', - required: true, - }, - { - $id: 'message', - format: 'string', - required: true, - }, - ], - maxItems: 2, - minItems: 2, - }, - }, - ], - maxItems: 3, - minItems: 3, - }, - maxItems: 1, - minItems: 1, - }, + type: 'array', + items: [ + { + type: 'array', + $id: 'a', + items: [ + { + type: 'array', + $id: 'a', + items: { + type: 'array', + items: [ + { + $id: 'level', + format: 'uint', + required: true, + }, + { + $id: 'message', + format: 'string', + required: true, + } + ], + maxItems: 2, + minItems: 2, + } + } + ], + maxItems: 3, + minItems: 3, + } + ], + maxItems: 1, + minItems: 1 + }, shortSchema: { - type: 'array', - items: { - type: 'array', - items: [ - { - $id: '/0/0', - type: 'array', - items: { - type: 'array', - items: [ - { - $id: '/0/0/0', - format: 'uint', - required: true, - }, - { - $id: '/0/0/1', - format: 'string', - required: true, - }, - ], - maxItems: 2, - minItems: 2, - }, - }, - ], - maxItems: 3, - minItems: 3, - }, - maxItems: 1, - minItems: 1, - }, + type: 'array', + items: [ + { + type: 'array', + $id: '/0/0', + items: [ + { + type: 'array', + $id: '/0/0', + items: { + type: 'array', + items: [ + { + $id: '/0/0/0', + format: 'uint', + required: true, + }, + { + $id: '/0/0/1', + format: 'string', + required: true, + } + ], + maxItems: 2, + minItems: 2, + } + } + ], + maxItems: 3, + minItems: 3, + } + ], + maxItems: 1, + minItems: 1 + }, data: [ [ [ @@ -1019,75 +1037,81 @@ const abiToJsonSchemaCases: AbiToJsonSchemaCase[] = [ }, json: { fullSchema: { - type: 'array', - items: { - type: 'array', - items: [ - { - $id: 'a', - type: 'array', - items: { - type: 'array', - items: [ - { - $id: 'level', - format: 'uint', - required: true, - }, - { - $id: 'message', - format: 'string', - required: true, - }, - ], - maxItems: 2, - minItems: 2, - }, - maxItems: 3, - minItems: 3, - }, - ], - maxItems: 5, - minItems: 5, - }, - maxItems: 1, - minItems: 1, - }, + type: 'array', + items: [ + { + type: 'array', + $id: 'a', + items: [ + { + type: 'array', + $id: 'a', + items: { + type: 'array', + items: [ + { + $id: 'level', + format: 'uint', + required: true, + }, + { + $id: 'message', + format: 'string', + required: true, + } + ], + maxItems: 2, + minItems: 2, + }, + minItems: 3, + maxItems: 3, + } + ], + maxItems: 5, + minItems: 5, + } + ], + maxItems: 1, + minItems: 1, + }, shortSchema: { - type: 'array', - items: { - type: 'array', - items: [ - { - $id: '/0/0', - type: 'array', - items: { - type: 'array', - items: [ - { - $id: '/0/0/0', - format: 'uint', - required: true, - }, - { - $id: '/0/0/1', - format: 'string', - required: true, - }, - ], - maxItems: 2, - minItems: 2, - }, - maxItems: 3, - minItems: 3, - }, - ], - maxItems: 5, - minItems: 5, - }, - maxItems: 1, - minItems: 1, - }, + type: 'array', + items: [ + { + type: 'array', + $id: '/0/0', + items: [ + { + type: 'array', + $id: '/0/0', + items: { + type: 'array', + items: [ + { + $id: '/0/0/0', + format: 'uint', + required: true, + }, + { + $id: '/0/0/1', + format: 'string', + required: true, + } + ], + maxItems: 2, + minItems: 2, + }, + minItems: 3, + maxItems: 3, + } + ], + maxItems: 5, + minItems: 5, + } + ], + maxItems: 1, + minItems: 1, + }, data: [ [ [ @@ -1375,130 +1399,186 @@ const abiToJsonSchemaCases: AbiToJsonSchemaCase[] = [ 'bool', ], data: [ - [ - [ - [ - [0, 0], - [1, 1], - ], - [ - [2, 2], - [3, 3], - ], - [ - [4, 4], - [5, 5], - ], - ], - [ - [ - [0, 0], - [-1, -1], - ], - [ - [-2, -2], - [-3, -3], - ], - [ - [-4, -4], - [-5, -5], - ], - ], - ], - -123, - true, - ], + [ + [ + [ + [0, 0], + [1, 1], + ], + [ + [2, 2], + [3, 3], + ], + ], + [ + [ + [0, 0], + [-1, -1], + ], + [ + [-2, -2], + [-3, -3], + ], + [ + [-4, -4], + [-5, -5], + ], + ], + [ + [ + [4, 4], + [5, 5], + ], + ] + ], + 123, + true, + ], }, json: { fullSchema: { - type: 'array', - items: { - type: 'array', - items: [ - { - $id: 'rects', - type: 'array', - items: { - type: 'array', - items: [ - { - type: 'array', - items: [ - { $id: 'x', format: 'int256', required: true }, - { $id: 'y', format: 'int256', required: true }, - ], - maxItems: 2, - minItems: 2, - $id: 'start', - }, - { - type: 'array', - items: [ - { $id: 'x', format: 'int256', required: true }, - { $id: 'y', format: 'int256', required: true }, - ], - maxItems: 2, - minItems: 2, - $id: 'end', - }, - ], - maxItems: 2, - minItems: 2, - }, - }, - { $id: 'numberValue', format: 'uint256', required: true }, - { $id: 'boolValue', format: 'bool', required: true }, - ], - maxItems: 3, - minItems: 3, - }, - maxItems: 3, - minItems: 3, - }, + type: 'array', + items: [ + { + type: 'array', + $id: 'rects', + items: [ + { + type: 'array', + $id: 'rects', + items: { + type: 'array', + items: [ + { + type: 'array', + items: [ + { + $id: 'x', + format: 'int256', + required: true, + }, + { + $id: 'y', + format: 'int256', + required: true + } + ], + maxItems: 2, + minItems: 2, + $id: 'start', + }, + { + type: 'array', + items: [ + { + $id: 'x', + format: 'int256', + required: true, + }, + { + $id: 'y', + format: 'int256', + required: true, + } + ], + maxItems: 2, + minItems: 2, + $id: 'end', + } + ], + maxItems: 2, + minItems: 2, + } + } + ], + maxItems: 3, + minItems: 3, + }, + { + $id: 'numberValue', + format: 'uint256', + required: true, + }, + { + $id: 'boolValue', + format: 'bool', + required: true, + } + ], + maxItems: 3, + minItems: 3, + }, shortSchema: { type: 'array', - items: { - type: 'array', - items: [ - { - $id: '/0/0', - type: 'array', - items: { - type: 'array', - items: [ - { - type: 'array', - items: [ - { $id: '/0/0/0/0', format: 'int256', required: true }, - { $id: '/0/0/0/1', format: 'int256', required: true }, - ], - maxItems: 2, - minItems: 2, - $id: '/0/0/0', - }, - { - type: 'array', - items: [ - { $id: '/0/0/1/0', format: 'int256', required: true }, - { $id: '/0/0/1/1', format: 'int256', required: true }, - ], - maxItems: 2, - minItems: 2, - $id: '/0/0/1', - }, - ], - maxItems: 2, - minItems: 2, - }, - }, - { $id: '/0/1', format: 'uint256', required: true }, - { $id: '/0/2', format: 'bool', required: true }, - ], - maxItems: 3, - minItems: 3, - }, - maxItems: 3, - minItems: 3, + items: [ + { + type: 'array', + $id: '/0/0', + items: [ + { + type: 'array', + $id: '/0/0', + items: { + type: 'array', + items: [ + { + type: 'array', + items: [ + { + $id: '/0/0/0/0', + format: 'int256', + required: true, + }, + { + $id: '/0/0/0/1', + format: 'int256', + required: true, + } + ], + maxItems: 2, + minItems: 2, + $id: '/0/0/0', + }, + { + type: 'array', + items: [ + { + $id: '/0/0/1/0', + format: 'int256', + required: true, + }, + { + $id: '/0/0/1/1', + format: 'int256', + required: true, + } + ], + maxItems: 2, + minItems: 2, + $id: '/0/0/1', + } + ], + maxItems: 2, + minItems: 2, + } + } + ], + maxItems: 3, + minItems: 3, + }, + { + $id: '/0/1', + format: 'uint256', + required: true, + }, + { + $id: '/0/2', + format: 'bool', + required: true, + } + ], + maxItems: 3, + minItems: 3, }, data: [ [ @@ -1511,10 +1591,6 @@ const abiToJsonSchemaCases: AbiToJsonSchemaCase[] = [ { x: 2, y: 2 }, { x: 3, y: 3 }, ], - [ - { x: 4, y: 4 }, - { x: 5, y: 5 }, - ], ], [ [ @@ -1530,8 +1606,14 @@ const abiToJsonSchemaCases: AbiToJsonSchemaCase[] = [ { x: -5, y: -5 }, ], ], + [ + [ + { x: 4, y: 4 }, + { x: 5, y: 5 }, + ], + ], ], - -123, + 123, true, ], }, @@ -1569,67 +1651,71 @@ const abiToJsonSchemaCases: AbiToJsonSchemaCase[] = [ }, json: { fullSchema: { - type: 'array', - items: [ - { - type: 'array', - items: [ - { - type: 'array', - $id: 'x1', - items: { - format: 'uint', - required: true, - }, - }, - ], - }, - { - type: 'array', - items: [ - { - type: 'array', - $id: 'x2', - items: { - format: 'uint', - required: true, - }, - }, - ], - }, - { - $id: 'x3', - format: 'uint256', - required: true, - }, - ], - maxItems: 3, - minItems: 3, - }, + type: 'array', + items: [ + { + type: 'array', + $id: 'x1', + items: [ + { + type: 'array', + $id: 'x1', + items: { + format: 'uint256', + required: true, + } + } + ] + }, + { + type: 'array', + $id: 'x2', + items: [ + { + type: 'array', + $id: 'x2', + items: { + format: 'uint256', + required: true, + } + } + ] + }, + { + $id: 'x3', + format: 'uint256', + required: true, + } + ], + maxItems: 3, + minItems: 3, + }, shortSchema: { type: 'array', items: [ { + $id: '/0/0', type: 'array', items: [ { type: 'array', $id: '/0/0', items: { - format: 'uint', + format: 'uint256', required: true, }, }, ], }, { + $id: '/0/1', type: 'array', items: [ { type: 'array', $id: '/0/1', items: { - format: 'uint', + format: 'uint256', required: true, }, }, diff --git a/packages/web3-validator/test/unit/web3_validator.test.ts b/packages/web3-validator/test/unit/web3_validator.test.ts index 0b910c31f17..ae04d036762 100644 --- a/packages/web3-validator/test/unit/web3_validator.test.ts +++ b/packages/web3-validator/test/unit/web3_validator.test.ts @@ -33,9 +33,12 @@ describe('web3-validator', () => { }); describe('validate', () => { - it('should pass for valid data', () => { - expect(validator.validate(['uint'], [1])).toBeUndefined(); - }); + describe('should pass for valid data', () => { + it.each(abiToJsonSchemaCases)('$title', ({ abi }) => { + const arrayData: ReadonlyArray = abi.data as Array; + expect(validator.validate(abi.fullSchema, arrayData)).toBeUndefined(); + }); + }) it('should raise error with empty value', () => { expect(() => validator.validate(['string'], [])).toThrow(