Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b5d1565
Add dynamic client type infrastructure for Asset Hub chains
itsyogesh Mar 19, 2026
102cc72
Add gas estimation hook, fee display, and struct external value sync
itsyogesh Mar 19, 2026
8be9f6b
Add Contract Studio page with three-panel resizable layout
itsyogesh Mar 19, 2026
a0fdf4a
Add gas estimation to builder and fix info pane object serialization
itsyogesh Mar 19, 2026
eb4e24f
Add Studio link to desktop and mobile navigation
itsyogesh Mar 19, 2026
c4c0d3c
Add regression tests for gas estimation, struct sync, and builder
itsyogesh Mar 19, 2026
7248ad7
Fix hasReviveApi to catch Dedot proxy chain errors on non-Asset Hub c…
itsyogesh Mar 19, 2026
6bb2e14
Fix Studio panel sizing and layout
itsyogesh Mar 19, 2026
ed81a23
Fix Studio layout height to fill full viewport
itsyogesh Mar 19, 2026
f51720f
Add Studio state foundation with workspace provider and artifact trac…
itsyogesh Mar 20, 2026
df8c969
Add compile race protection and artifact tracking to builder
itsyogesh Mar 20, 2026
36d60f2
Add IDE layout with file explorer, editor tabs, and compile/deploy si…
itsyogesh Mar 20, 2026
9c1714c
Add multi-file Solidity compilation support
itsyogesh Mar 20, 2026
64e9323
Add tests for Studio reducer and compile/upload state transitions
itsyogesh Mar 20, 2026
6c57013
Remove old Studio components replaced by IDE layout
itsyogesh Mar 20, 2026
ca48e6b
Fix stale gas estimates, CDN shadowing, and filename validation
itsyogesh Mar 20, 2026
b990140
Clear stale bytecode on builder compile failure and invalidate gas on…
itsyogesh Mar 20, 2026
44c9ed7
Fix Sign and Submit button getting stuck in submitting state
itsyogesh Mar 20, 2026
36506a0
Handle transaction dispatch errors and improve lifecycle feedback
itsyogesh Mar 20, 2026
0914f9d
Make editor and output panel resizable with drag handle
itsyogesh Mar 20, 2026
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
208 changes: 205 additions & 3 deletions __tests__/components/builder/extrinsic-builder.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,69 @@ jest.mock("dedot/utils", () => ({
if (!s) return s;
return s.charAt(0).toLowerCase() + s.slice(1);
},
assert: jest.fn(),
}));

// Mock gas estimation hook
const mockEstimate = jest.fn();
const mockGasEstimation = {
estimating: false,
weightRequired: null as any,
storageDeposit: null as any,
gasConsumed: null,
deployedAddress: null,
error: null as string | null,
estimate: mockEstimate,
};

jest.mock("@/hooks/use-gas-estimation", () => ({
useGasEstimation: jest.fn().mockImplementation(() => mockGasEstimation),
}));

// Mock use-chain-token
jest.mock("@/hooks/use-chain-token", () => ({
useChainToken: jest.fn().mockReturnValue({
symbol: "DOT",
decimals: 10,
denominations: [],
existentialDeposit: BigInt(0),
loading: false,
}),
}));

// Mock fee-display
jest.mock("@/lib/fee-display", () => ({
formatFee: jest.fn().mockReturnValue("0.001 DOT"),
formatWeight: jest.fn().mockReturnValue("refTime: 1.1K, proofSize: 2.2K"),
}));

// Mock chain-types
jest.mock("@/lib/chain-types", () => ({
hasReviveApi: jest.fn().mockReturnValue(false),
GenericChainClient: {} as any,
}));

// Mock next/link — needed for the Studio link
jest.mock("next/link", () => {
const React = require("react");
return {
__esModule: true,
default: ({ children, href, ...props }: any) =>
React.createElement("a", { href, ...props }, children),
};
});

// Mock lucide-react icons — proxy to actual icons but add our test ones
jest.mock("lucide-react", () => {
const actual = jest.requireActual("lucide-react");
return {
...actual,
Loader2: (props: any) => <span data-testid="loader2" {...props} />,
Zap: (props: any) => <span data-testid="zap" {...props} />,
ArrowRight: (props: any) => <span data-testid="arrow-right" {...props} />,
};
});

import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { useForm, FormProvider } from "react-hook-form";
Expand Down Expand Up @@ -199,16 +260,16 @@ describe("ExtrinsicBuilder", () => {
expect(screen.getByText("Sign and Submit")).toBeInTheDocument();
});

it("shows Submitting... when isPending", () => {
it("shows Sign and Submit when account connected and not submitting", () => {
(useAccount as jest.Mock).mockReturnValue({
account: { address: "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" },
});
(useSendTransaction as jest.Mock).mockReturnValue({
sendTransactionAsync: jest.fn(),
isPending: true,
isPending: false,
});
render(<TestWrapper tx={{ meta: { fields: [] } }} />);
expect(screen.getByText("Submitting...")).toBeInTheDocument();
expect(screen.getByText("Sign and Submit")).toBeInTheDocument();
});

it("submit button disabled when no tx", () => {
Expand Down Expand Up @@ -253,4 +314,145 @@ describe("ExtrinsicBuilder", () => {
render(<TestWrapper />);
}).not.toThrow();
});

describe("gas estimation for Revive", () => {
const reviveTx = {
meta: {
fields: [
{ name: "value", typeId: 6, typeName: "BalanceOf" },
{ name: "weight_limit", typeId: 10, typeName: "Weight" },
{ name: "storage_deposit_limit", typeId: 6, typeName: "BalanceOf" },
{ name: "code", typeId: 14, typeName: "Vec<u8>" },
{ name: "data", typeId: 14, typeName: "Vec<u8>" },
{ name: "salt", typeId: 15, typeName: "Option<[u8; 32]>" },
],
docs: ["Instantiate a contract with code"],
},
};

beforeEach(() => {
// Override section/method options to include Revive
const { createSectionOptions, createMethodOptions } = require("@/lib/parser");
createSectionOptions.mockReturnValue([
{ value: 60, text: "Revive", docs: ["Contracts"] },
{ value: 0, text: "System", docs: ["System pallet"] },
]);
createMethodOptions.mockReturnValue([
{ value: 0, text: "instantiate_with_code" },
]);
});

function ReviveTestWrapper({ tx = reviveTx }: { tx?: any }) {
const form = useForm({
defaultValues: {
section: "60:Revive",
method: "", // useEffect clears this on mount anyway
value: "0",
weight_limit: "",
storage_deposit_limit: "",
code: "0x1234",
data: "0x",
salt: "",
},
});

// Simulate method selection after mount
React.useEffect(() => {
form.setValue("method", "0:instantiate_with_code");
}, [form]);

const mockClient = {
metadata: {
latest: {
pallets: [
{
index: 60,
name: "Revive",
calls: { typeId: 100 },
docs: ["Contracts"],
},
],
},
},
registry: {
findCodec: jest.fn(),
findType: jest.fn().mockReturnValue({
typeDef: {
type: "Struct",
value: {
fields: [
{ name: "refTime", typeId: 6 },
{ name: "proofSize", typeId: 6 },
],
},
},
}),
},
tx: {
revive: {
instantiateWithCode: {
meta: reviveTx.meta,
},
},
},
} as any;

return (
<ExtrinsicBuilder
client={mockClient}
tx={tx}
onTxChange={jest.fn()}
builderForm={form}
/>
);
}

it("shows Estimate Gas button when pallet=Revive, method=instantiate_with_code", () => {
render(<ReviveTestWrapper />);
expect(screen.getByText("Estimate Gas")).toBeInTheDocument();
});

it("shows Open in Contract Studio link when Revive selected", () => {
const { container } = render(<ReviveTestWrapper />);
// The link is rendered by next/link mock as an <a> tag
const link = container.querySelector('a[href="/studio"]');
expect(link).toBeInTheDocument();
});

it("does NOT show Estimate Gas for non-Revive pallet", () => {
const systemTx = {
meta: {
fields: [{ name: "remark", typeId: 14, typeName: "Vec<u8>" }],
docs: ["Make a remark"],
},
};

function SystemTestWrapper() {
const form = useForm({
defaultValues: {
section: "0:System",
method: "",
},
});

const mockClient = {
metadata: { latest: { pallets: [] } },
registry: { findCodec: jest.fn(), findType: jest.fn() },
tx: { system: { remark: systemTx } },
} as any;

return (
<ExtrinsicBuilder
client={mockClient}
tx={systemTx}
onTxChange={jest.fn()}
builderForm={form}
/>
);
}

render(<SystemTestWrapper />);
expect(screen.queryByText("Estimate Gas")).not.toBeInTheDocument();
});
});
});
50 changes: 50 additions & 0 deletions __tests__/components/builder/information-pane.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -317,4 +317,54 @@ describe("InformationPane", () => {
expect(screen.getByTestId("field-hex-dest")).toBeInTheDocument();
expect(screen.getByTestId("field-hex-value")).toBeInTheDocument();
});

it("re-encodes when object-valued arg changes (argValuesKey serialization)", () => {
const { encodeAllArgs } = require("@/lib/codec");

const mockTx = {
meta: {
index: 1,
fields: [
{ name: "weight_limit", typeId: 10, typeName: "Weight" },
],
docs: [],
},
} as any;

// First render with object value
const form1 = createMockForm({
section: "60:Revive",
weight_limit: { refTime: "1000", proofSize: "2000" },
});

const { rerender } = render(
<InformationPane
client={mockClient}
tx={mockTx}
builderForm={form1}
onTxChange={mockOnTxChange}
/>
);

const firstCallCount = encodeAllArgs.mock.calls.length;

// Rerender with different object value
const form2 = createMockForm({
section: "60:Revive",
weight_limit: { refTime: "3000", proofSize: "4000" },
});

rerender(
<InformationPane
client={mockClient}
tx={mockTx}
builderForm={form2}
onTxChange={mockOnTxChange}
/>
);

// encodeAllArgs should have been called again because the argValuesKey
// now uses JSON.stringify for object values instead of "[object Object]"
expect(encodeAllArgs.mock.calls.length).toBeGreaterThan(firstCallCount);
});
});
71 changes: 71 additions & 0 deletions __tests__/components/params/inputs/struct.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,75 @@ describe("Struct", () => {
render(<Struct {...baseProps} fields={[]} error="Missing fields" />);
expect(screen.getByText("Missing fields")).toBeInTheDocument();
});

it("hydrates child inputs from external value prop", () => {
const onChange = jest.fn();
const fields = [
createField("refTime", "Ref Time"),
createField("proofSize", "Proof Size"),
];

const { rerender } = render(
<Struct {...baseProps} fields={fields} onChange={onChange} />
);

// Now set external value — simulating gas estimation auto-fill
rerender(
<Struct
{...baseProps}
fields={fields}
onChange={onChange}
value={{ refTime: "1000", proofSize: "2000" }}
/>
);

// The child inputs should receive the values via cloneElement
const refTimeInput = screen.getByTestId("field-testStruct-refTime");
const proofSizeInput = screen.getByTestId("field-testStruct-proofSize");

// The TestFieldInput component doesn't render value, but onChange should
// have been called with the synced values. Since useEffect triggers
// setValues but not onChange (only handleFieldChange calls onChange),
// we verify the internal state was updated by checking that a subsequent
// field change includes the externally-set values
fireEvent.change(refTimeInput, { target: { value: "3000" } });
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({ refTime: "3000", proofSize: "2000" })
);
});

it("does not loop when external value matches internal state", () => {
const onChange = jest.fn();
const fields = [createField("a", "A"), createField("b", "B")];
const value = { a: "1", b: "2" };

const { rerender } = render(
<Struct {...baseProps} fields={fields} onChange={onChange} value={value} />
);

// Rerender with same value — should not cause extra renders
rerender(
<Struct {...baseProps} fields={fields} onChange={onChange} value={value} />
);

// Verify the component didn't explode — no infinite loop
expect(screen.getByText("Transfer Info")).toBeInTheDocument();
});

it("emits object-valued onChange (not string)", () => {
const onChange = jest.fn();
const fields = [
createField("x", "X"),
createField("y", "Y"),
];
render(<Struct {...baseProps} fields={fields} onChange={onChange} />);

const xInput = screen.getByTestId("field-testStruct-x");
fireEvent.change(xInput, { target: { value: "hello" } });

// onChange should receive an object, not a string
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ x: "hello" }));
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0];
expect(typeof lastCall).toBe("object");
});
});
Loading
Loading