Skip to content
Merged
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
102 changes: 102 additions & 0 deletions __tests__/lib/codec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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({
Expand Down
28 changes: 5 additions & 23 deletions components/builder/extrinsic-builder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,29 +110,11 @@ const ExtrinsicBuilder: React.FC<ExtrinsicBuilderProps> = ({
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") {
Expand Down
14 changes: 11 additions & 3 deletions lib/codec.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -541,7 +549,7 @@ export function decomposeArgHex(
if (fields.length > 0 && typeof value === "object" && value !== null && !Array.isArray(value)) {
const obj = value as Record<string, unknown>;
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 {
Expand Down Expand Up @@ -577,7 +585,7 @@ export function decomposeArgHex(
if (typeof enumObj.value === "object" && enumObj.value !== null) {
const obj = enumObj.value as Record<string, unknown>;
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 {
Expand Down
Loading