diff --git a/__tests__/lib/codec.test.ts b/__tests__/lib/codec.test.ts index 25f4663..751b3de 100644 --- a/__tests__/lib/codec.test.ts +++ b/__tests__/lib/codec.test.ts @@ -11,9 +11,35 @@ import { decodeArgLegacy, decodeAllArgs, decomposeArgHex, + normalizeFieldName, } from "../../lib/codec"; import { createMockDedotClient } from "../helpers/mock-client"; +// --------------------------------------------------------------------------- +// normalizeFieldName +// --------------------------------------------------------------------------- +describe("normalizeFieldName", () => { + it("converts snake_case to camelCase", () => { + expect(normalizeFieldName("ref_time")).toBe("refTime"); + expect(normalizeFieldName("proof_size")).toBe("proofSize"); + expect(normalizeFieldName("storage_deposit_limit")).toBe("storageDepositLimit"); + }); + + it("preserves already camelCase names", () => { + expect(normalizeFieldName("refTime")).toBe("refTime"); + expect(normalizeFieldName("proofSize")).toBe("proofSize"); + }); + + it("handles hash prefix replacement", () => { + expect(normalizeFieldName("#field_name")).toBe("fieldName"); + }); + + it("handles single word names", () => { + expect(normalizeFieldName("value")).toBe("value"); + expect(normalizeFieldName("dest")).toBe("dest"); + }); +}); + // --------------------------------------------------------------------------- // encodeArg // --------------------------------------------------------------------------- @@ -827,6 +853,82 @@ describe("decomposeArgHex", () => { } }); + it("normalizes snake_case metadata field names to camelCase for value lookup", () => { + // This is the exact scenario from gas estimation auto-fill: + // Metadata has snake_case fields (ref_time, proof_size) but the auto-filled + // value object uses camelCase (refTime, proofSize) matching Dedot's convention. + const tryEncode = jest.fn().mockReturnValue(new Uint8Array([0x01])); + const client = createMockDedotClient({ + registry: { + findCodec: jest.fn().mockReturnValue({ + tryEncode, + tryDecode: jest.fn(), + }), + findType: jest.fn().mockReturnValue({ + typeDef: { + type: "Struct", + value: { + fields: [ + { name: "ref_time", typeId: 10 }, + { name: "proof_size", typeId: 20 }, + ], + }, + }, + }), + }, + }); + // Value uses camelCase keys (as produced by gas estimation auto-fill) + const result = decomposeArgHex(client, 1, { + refTime: "117234441", + proofSize: "135850", + }); + expect(result.kind).toBe("compound"); + if (result.kind === "compound") { + expect(result.compoundType).toBe("Struct"); + expect(result.children).toHaveLength(2); + // Labels should be normalized to camelCase + expect(result.children[0].label).toBe("refTime"); + expect(result.children[1].label).toBe("proofSize"); + // Encoder should have been called with coerced values (BigInt), not undefined + expect(tryEncode).toHaveBeenCalledWith(BigInt("117234441")); + expect(tryEncode).toHaveBeenCalledWith(BigInt("135850")); + } + }); + + it("handles already-camelCase metadata field names without double-normalizing", () => { + const tryEncode = jest.fn().mockReturnValue(new Uint8Array([0x01])); + const client = createMockDedotClient({ + registry: { + findCodec: jest.fn().mockReturnValue({ + tryEncode, + tryDecode: jest.fn(), + }), + findType: jest.fn().mockReturnValue({ + typeDef: { + type: "Struct", + value: { + fields: [ + { name: "refTime", typeId: 10 }, + { name: "proofSize", typeId: 20 }, + ], + }, + }, + }), + }, + }); + const result = decomposeArgHex(client, 1, { + refTime: "100", + proofSize: "200", + }); + expect(result.kind).toBe("compound"); + if (result.kind === "compound") { + expect(result.children[0].label).toBe("refTime"); + expect(result.children[1].label).toBe("proofSize"); + expect(tryEncode).toHaveBeenCalledWith(BigInt("100")); + expect(tryEncode).toHaveBeenCalledWith(BigInt("200")); + } + }); + it("decomposes Enum single-field variant", () => { const tryEncode = jest.fn().mockReturnValue(new Uint8Array([0x01])); const client = createMockDedotClient({ diff --git a/components/builder/extrinsic-builder.tsx b/components/builder/extrinsic-builder.tsx index 247f05d..344a74e 100644 --- a/components/builder/extrinsic-builder.tsx +++ b/components/builder/extrinsic-builder.tsx @@ -110,29 +110,11 @@ const ExtrinsicBuilder: React.FC = ({ useEffect(() => { if (!isReviveInstantiate || !gasEstimation.weightRequired || !tx) return; - // Resolve weight field names from metadata - const weightField = tx.meta?.fields?.find((f) => f.name === "weight_limit"); - if (weightField) { - try { - const weightType = client.registry.findType(weightField.typeId); - const { typeDef } = weightType; - if (typeDef.type === "Struct" && typeDef.value.fields.length >= 2) { - const [field0, field1] = typeDef.value.fields; - const name0 = String(field0.name); - const name1 = String(field1.name); - builderForm.setValue("weight_limit", { - [name0]: String(gasEstimation.weightRequired.refTime), - [name1]: String(gasEstimation.weightRequired.proofSize), - }); - } - } catch { - // Fallback: use camelCase names - builderForm.setValue("weight_limit", { - refTime: String(gasEstimation.weightRequired.refTime), - proofSize: String(gasEstimation.weightRequired.proofSize), - }); - } - } + // Auto-fill weight_limit with camelCase keys (Dedot codec convention) + builderForm.setValue("weight_limit", { + refTime: String(gasEstimation.weightRequired.refTime), + proofSize: String(gasEstimation.weightRequired.proofSize), + }); // Auto-fill storage deposit only for Charge if (gasEstimation.storageDeposit?.type === "Charge") { diff --git a/lib/codec.ts b/lib/codec.ts index 010ef53..a2989b7 100644 --- a/lib/codec.ts +++ b/lib/codec.ts @@ -1,5 +1,13 @@ import { DedotClient } from "dedot"; -import { u8aToHex, hexToU8a, hexStripPrefix, hexAddPrefix, decodeAddress } from "dedot/utils"; +import { u8aToHex, hexToU8a, hexStripPrefix, hexAddPrefix, decodeAddress, stringCamelCase } from "dedot/utils"; + +/** + * Normalize a metadata field name to camelCase, matching Dedot's internal convention. + * Metadata uses snake_case (ref_time, proof_size), but Dedot codecs expect camelCase. + */ +export function normalizeFieldName(name: string): string { + return stringCamelCase(name.replace("#", "_")); +} /** * Result type for encoding operations @@ -541,7 +549,7 @@ export function decomposeArgHex( if (fields.length > 0 && typeof value === "object" && value !== null && !Array.isArray(value)) { const obj = value as Record; const children: HexChildItem[] = fields.map((field) => { - const fieldName = field.name || ""; + const fieldName = normalizeFieldName(field.name || ""); const fieldValue = obj[fieldName]; const result = encodeArg(client, field.typeId, fieldValue); return { @@ -577,7 +585,7 @@ export function decomposeArgHex( if (typeof enumObj.value === "object" && enumObj.value !== null) { const obj = enumObj.value as Record; const children: HexChildItem[] = variant.fields.map((field) => { - const fieldName = field.name || ""; + const fieldName = normalizeFieldName(field.name || ""); const fieldValue = obj[fieldName]; const result = encodeArg(client, field.typeId, fieldValue); return {