diff --git a/.changeset/midnight-contract-ingestion.md b/.changeset/midnight-contract-ingestion.md new file mode 100644 index 00000000..334d31e2 --- /dev/null +++ b/.changeset/midnight-contract-ingestion.md @@ -0,0 +1,14 @@ +--- +'@openzeppelin/ui-builder-adapter-midnight': minor +'@openzeppelin/ui-builder-app': patch +'@openzeppelin/ui-builder-storage': patch +'@openzeppelin/ui-builder-utils': patch +'@openzeppelin/ui-builder-ui': patch +--- + +Midnight adapter contract ingestion and shared gating + +- Midnight: move loading to contract/loader; return contractDefinitionArtifacts; keep adapter thin. +- Builder: replace local required-field gating with shared utils (getMissingRequiredContractInputs); remove redundant helper. +- Utils: add contractInputs shared helpers and tests. +- Storage/App/UI: persist and rehydrate contractDefinitionArtifacts; auto-save triggers on artifact changes. diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..3f59a946 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,16 @@ +node_modules/ +dist/ +build/ +out/ +coverage/ +*.min.js +.next/ +.nuxt/ +.output/ +.cache/ +.turbo/ +.vercel/ +.netlify/ +exports/ +packages/builder/test-results/ + diff --git a/.prettierignore b/.prettierignore index 7bf497c6..d98cdf43 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,19 @@ +node_modules/ +dist/ +build/ +out/ +coverage/ +exports/ +package-lock.json +yarn.lock +pnpm-lock.yaml +.next/ +.nuxt/ +.output/ +.cache/ +.turbo/ +.vercel/ +.netlify/ # Build outputs dist/ build/ diff --git a/packages/adapter-midnight/src/adapter.ts b/packages/adapter-midnight/src/adapter.ts index 48226989..f6b39781 100644 --- a/packages/adapter-midnight/src/adapter.ts +++ b/packages/adapter-midnight/src/adapter.ts @@ -29,7 +29,8 @@ import * as connection from './wallet/connection'; import { midnightFacadeHooks } from './wallet/hooks/facade-hooks'; import { testMidnightRpcConnection, validateMidnightRpcEndpoint } from './configuration'; -import { parseMidnightContractInterface, validateAndConvertMidnightArtifacts } from './utils'; +import { loadMidnightContract, loadMidnightContractWithMetadata } from './contract'; +import { validateAndConvertMidnightArtifacts } from './utils'; /** * Midnight-specific adapter. @@ -123,8 +124,8 @@ export class MidnightAdapter implements ContractAdapter { 'A unique identifier for your private state instance. This ID is used to manage your personal encrypted data.', }, { - id: 'contractSchema', - name: 'contractSchema', + id: 'contractDefinition', + name: 'contractDefinition', label: 'Contract Interface (.d.ts)', type: 'code-editor', validation: { required: true }, @@ -163,23 +164,44 @@ export class MidnightAdapter implements ContractAdapter { } public async loadContract(source: string | Record): Promise { - // Convert and validate the input const artifacts = validateAndConvertMidnightArtifacts(source); this.artifacts = artifacts; logger.info('MidnightAdapter', 'Contract artifacts stored.', this.artifacts); - const { functions, events } = parseMidnightContractInterface(artifacts.contractSchema); + const result = await loadMidnightContract(artifacts, this.networkConfig); + return result.schema; + } - const schema: ContractSchema = { - name: 'MyMidnightContract', // TODO: Extract from artifacts if possible - ecosystem: 'midnight', - address: artifacts.contractAddress, - functions, - events, + public async loadContractWithMetadata(source: string | Record): Promise<{ + schema: ContractSchema; + source: 'fetched' | 'manual'; + contractDefinitionOriginal?: string; + metadata?: { + fetchedFrom?: string; + contractName?: string; + verificationStatus?: 'verified' | 'unverified' | 'unknown'; + fetchTimestamp?: Date; + definitionHash?: string; }; + proxyInfo?: undefined; + contractDefinitionArtifacts?: Record; + }> { + const artifacts = validateAndConvertMidnightArtifacts(source); - return schema; + this.artifacts = artifacts; + logger.info('MidnightAdapter', 'Contract artifacts stored.', this.artifacts); + + const result = await loadMidnightContractWithMetadata(artifacts, this.networkConfig); + + return { + schema: result.schema, + source: result.source, + contractDefinitionOriginal: result.contractDefinitionOriginal, + metadata: result.metadata, + contractDefinitionArtifacts: result.contractDefinitionArtifacts, + proxyInfo: result.proxyInfo, + }; } public getWritableFunctions(contractSchema: ContractSchema): ContractFunction[] { diff --git a/packages/adapter-midnight/src/config.ts b/packages/adapter-midnight/src/config.ts index 1432d6b7..1b4edd44 100644 --- a/packages/adapter-midnight/src/config.ts +++ b/packages/adapter-midnight/src/config.ts @@ -16,33 +16,11 @@ export const midnightAdapterConfig = { * These will be included in exported projects that use this adapter */ dependencies: { - // TODO: Review and update with real, verified dependencies and versions before production release - - // Runtime dependencies + // FR-017: For v1, adapter export dependencies must match the export manifest. + // Only include runtime deps required by exported apps using the Midnight adapter. runtime: { - // Core Midnight protocol libraries - '@midnight-protocol/sdk': '^0.8.2', - '@midnight-protocol/client': '^0.7.0', - - // Encryption and privacy utilities - 'libsodium-wrappers': '^0.7.11', - '@openzeppelin/contracts-upgradeable': '^4.9.3', - - // Additional utilities for Midnight - 'js-sha256': '^0.9.0', - 'bn.js': '^5.2.1', - '@midnight-ntwrk/dapp-connector-api': '^3.0.0', }, - - // Development dependencies - dev: { - // Testing utilities for Midnight - '@midnight-protocol/testing': '^0.5.0', - - // Type definitions - '@types/libsodium-wrappers': '^0.7.10', - '@types/bn.js': '^5.1.1', - }, + dev: {}, }, }; diff --git a/packages/adapter-midnight/src/contract/__tests__/loader.test.ts b/packages/adapter-midnight/src/contract/__tests__/loader.test.ts new file mode 100644 index 00000000..2156cd03 --- /dev/null +++ b/packages/adapter-midnight/src/contract/__tests__/loader.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from 'vitest'; + +import type { MidnightContractArtifacts } from '../../types/artifacts'; +import { loadMidnightContract, loadMidnightContractWithMetadata } from '../loader'; + +const mockInterface = ` +export type Circuits = { + post(context: any, new_message: string): any; +}; +export declare class Contract { + readonly circuits: Circuits; +}`; + +describe('Midnight contract loader', () => { + it('loadMidnightContract returns schema and metadata with definitionHash', async () => { + const artifacts: MidnightContractArtifacts = { + contractAddress: 'ct1qexampleaddress', + privateStateId: 'state-1', + contractDefinition: mockInterface, + contractModule: 'module.exports = {}', + witnessCode: 'export const witnesses = {}', + }; + + const result = await loadMidnightContract(artifacts); + expect(result.schema).toBeDefined(); + expect(result.schema.ecosystem).toBe('midnight'); + expect(result.schema.address).toBe(artifacts.contractAddress); + expect(result.metadata).toBeDefined(); + expect(result.metadata?.definitionHash).toBeDefined(); + expect(result.contractDefinitionOriginal).toBe(artifacts.contractDefinition); + }); + + it('loadMidnightContractWithMetadata includes artifacts when provided', async () => { + const artifacts: MidnightContractArtifacts = { + contractAddress: 'ct1qexampleaddress2', + privateStateId: 'state-2', + contractDefinition: mockInterface, + contractModule: 'module.exports = {}', + witnessCode: 'export const witnesses = {}', + }; + + const result = await loadMidnightContractWithMetadata(artifacts); + expect(result.contractDefinitionArtifacts).toBeDefined(); + expect(result.contractDefinitionArtifacts).toMatchObject({ + privateStateId: 'state-2', + contractModule: 'module.exports = {}', + witnessCode: 'export const witnesses = {}', + }); + }); + + it('loadMidnightContractWithMetadata omits artifacts when none provided', async () => { + const artifacts: MidnightContractArtifacts = { + contractAddress: 'ct1qexampleaddress3', + privateStateId: '', + contractDefinition: mockInterface, + contractModule: '', + witnessCode: '', + }; + + const result = await loadMidnightContractWithMetadata(artifacts); + expect(result.contractDefinitionArtifacts).toBeUndefined(); + }); +}); diff --git a/packages/adapter-midnight/src/contract/index.ts b/packages/adapter-midnight/src/contract/index.ts new file mode 100644 index 00000000..3f3b9e66 --- /dev/null +++ b/packages/adapter-midnight/src/contract/index.ts @@ -0,0 +1 @@ +export * from './loader'; diff --git a/packages/adapter-midnight/src/contract/loader.ts b/packages/adapter-midnight/src/contract/loader.ts new file mode 100644 index 00000000..62609d2d --- /dev/null +++ b/packages/adapter-midnight/src/contract/loader.ts @@ -0,0 +1,70 @@ +import type { ContractSchema, MidnightNetworkConfig } from '@openzeppelin/ui-builder-types'; +import { logger, simpleHash } from '@openzeppelin/ui-builder-utils'; + +import type { MidnightContractArtifacts } from '../types/artifacts'; +import { parseMidnightContractInterface } from '../utils/schema-parser'; + +export interface MidnightContractLoadResult { + schema: ContractSchema; + source: 'fetched' | 'manual'; + contractDefinitionOriginal?: string; + metadata?: { + fetchedFrom?: string; + contractName?: string; + verificationStatus?: 'verified' | 'unverified' | 'unknown'; + fetchTimestamp?: Date; + definitionHash?: string; + }; + contractDefinitionArtifacts?: Record; + proxyInfo?: undefined; +} + +export async function loadMidnightContract( + artifacts: MidnightContractArtifacts, + _networkConfig?: MidnightNetworkConfig +): Promise { + logger.info('loadMidnightContract', 'Loading Midnight contract from artifacts'); + + const { functions, events } = parseMidnightContractInterface(artifacts.contractDefinition); + + const schema: ContractSchema = { + name: 'MyMidnightContract', + ecosystem: 'midnight', + address: artifacts.contractAddress, + functions, + events, + }; + + const definition = artifacts.contractDefinition || ''; + const metadata = { + fetchedFrom: 'local', + verificationStatus: 'unknown' as const, + fetchTimestamp: new Date(), + definitionHash: definition ? simpleHash(definition) : undefined, + }; + + return { + schema, + source: 'manual', + contractDefinitionOriginal: artifacts.contractDefinition, + metadata, + }; +} + +export async function loadMidnightContractWithMetadata( + artifacts: MidnightContractArtifacts, + networkConfig?: MidnightNetworkConfig +): Promise { + const base = await loadMidnightContract(artifacts, networkConfig); + + const artifactsRecord: Record = {}; + if (artifacts.privateStateId) artifactsRecord.privateStateId = artifacts.privateStateId; + if (artifacts.contractModule) artifactsRecord.contractModule = artifacts.contractModule; + if (artifacts.witnessCode) artifactsRecord.witnessCode = artifacts.witnessCode; + + return { + ...base, + contractDefinitionArtifacts: + Object.keys(artifactsRecord).length > 0 ? artifactsRecord : undefined, + }; +} diff --git a/packages/adapter-midnight/src/types/__tests__/artifacts.test.ts b/packages/adapter-midnight/src/types/__tests__/artifacts.test.ts index 56e219c5..4fdede18 100644 --- a/packages/adapter-midnight/src/types/__tests__/artifacts.test.ts +++ b/packages/adapter-midnight/src/types/__tests__/artifacts.test.ts @@ -11,7 +11,7 @@ describe('Midnight Contract Artifacts', () => { const artifacts: MidnightContractArtifacts = { contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0', privateStateId: 'my-unique-state-id', - contractSchema: 'export interface MyContract { test(): Promise; }', + contractDefinition: 'export interface MyContract { test(): Promise; }', }; const result = isMidnightContractArtifacts(artifacts); @@ -23,7 +23,7 @@ describe('Midnight Contract Artifacts', () => { const artifacts: MidnightContractArtifacts = { contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0', privateStateId: 'my-unique-state-id', - contractSchema: 'export interface MyContract { test(): Promise; }', + contractDefinition: 'export interface MyContract { test(): Promise; }', contractModule: 'module.exports = {};', witnessCode: 'export const witnesses = {};', }; @@ -53,7 +53,7 @@ describe('Midnight Contract Artifacts', () => { it('should return false for object without contractAddress', () => { const artifacts = { privateStateId: 'my-unique-state-id', - contractSchema: 'export interface MyContract { test(): Promise; }', + contractDefinition: 'export interface MyContract { test(): Promise; }', }; const result = isMidnightContractArtifacts(artifacts); @@ -64,7 +64,7 @@ describe('Midnight Contract Artifacts', () => { it('should return false for object without privateStateId', () => { const artifacts = { contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0', - contractSchema: 'export interface MyContract { test(): Promise; }', + contractDefinition: 'export interface MyContract { test(): Promise; }', }; const result = isMidnightContractArtifacts(artifacts); @@ -72,7 +72,7 @@ describe('Midnight Contract Artifacts', () => { expect(result).toBe(false); }); - it('should return false for object without contractSchema', () => { + it('should return false for object without contractDefinition', () => { const artifacts = { contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0', privateStateId: 'my-unique-state-id', @@ -87,7 +87,7 @@ describe('Midnight Contract Artifacts', () => { const artifacts = { contractAddress: 123, privateStateId: 'my-unique-state-id', - contractSchema: 'export interface MyContract { test(): Promise; }', + contractDefinition: 'export interface MyContract { test(): Promise; }', }; const result = isMidnightContractArtifacts(artifacts); @@ -99,7 +99,7 @@ describe('Midnight Contract Artifacts', () => { const artifacts = { contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0', privateStateId: 123, - contractSchema: 'export interface MyContract { test(): Promise; }', + contractDefinition: 'export interface MyContract { test(): Promise; }', }; const result = isMidnightContractArtifacts(artifacts); @@ -107,11 +107,11 @@ describe('Midnight Contract Artifacts', () => { expect(result).toBe(false); }); - it('should return false for object with non-string contractSchema', () => { + it('should return false for object with non-string contractDefinition', () => { const artifacts = { contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0', privateStateId: 'my-unique-state-id', - contractSchema: 123, + contractDefinition: 123, }; const result = isMidnightContractArtifacts(artifacts); @@ -123,7 +123,7 @@ describe('Midnight Contract Artifacts', () => { const artifacts = { contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0', privateStateId: 'my-unique-state-id', - contractSchema: 'export interface MyContract { test(): Promise; }', + contractDefinition: 'export interface MyContract { test(): Promise; }', extraProperty: 'should be ignored', anotherExtra: 42, }; @@ -137,7 +137,7 @@ describe('Midnight Contract Artifacts', () => { const artifacts = { contractAddress: '', privateStateId: '', - contractSchema: '', + contractDefinition: '', }; const result = isMidnightContractArtifacts(artifacts); diff --git a/packages/adapter-midnight/src/types/artifacts.ts b/packages/adapter-midnight/src/types/artifacts.ts index 4a11e163..88c11296 100644 --- a/packages/adapter-midnight/src/types/artifacts.ts +++ b/packages/adapter-midnight/src/types/artifacts.ts @@ -10,7 +10,7 @@ export interface MidnightContractArtifacts { privateStateId: string; /** TypeScript interface definition from contract.d.ts (required) */ - contractSchema: string; + contractDefinition: string; /** Optional compiled contract code from contract.cjs */ contractModule?: string; @@ -29,6 +29,6 @@ export function isMidnightContractArtifacts(obj: unknown): obj is MidnightContra obj !== null && typeof record.contractAddress === 'string' && typeof record.privateStateId === 'string' && - typeof record.contractSchema === 'string' + typeof record.contractDefinition === 'string' ); } diff --git a/packages/adapter-midnight/src/utils/__tests__/artifacts.test.ts b/packages/adapter-midnight/src/utils/__tests__/artifacts.test.ts index 0b8ff514..6e72bb8f 100644 --- a/packages/adapter-midnight/src/utils/__tests__/artifacts.test.ts +++ b/packages/adapter-midnight/src/utils/__tests__/artifacts.test.ts @@ -9,7 +9,7 @@ describe('validateAndConvertMidnightArtifacts', () => { const validArtifacts = { contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0', privateStateId: 'my-unique-state-id', - contractSchema: 'export interface MyContract { test(): Promise; }', + contractDefinition: 'export interface MyContract { test(): Promise; }', }; it('should return valid artifacts object as-is', () => { @@ -41,33 +41,33 @@ describe('validateAndConvertMidnightArtifacts', () => { it('should throw error for artifacts missing contractAddress', () => { const invalidArtifacts = { privateStateId: 'my-unique-state-id', - contractSchema: 'export interface MyContract { test(): Promise; }', + contractDefinition: 'export interface MyContract { test(): Promise; }', }; expect(() => validateAndConvertMidnightArtifacts(invalidArtifacts)).toThrow( - 'Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractSchema properties.' + 'Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractDefinition properties.' ); }); it('should throw error for artifacts missing privateStateId', () => { const invalidArtifacts = { contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0', - contractSchema: 'export interface MyContract { test(): Promise; }', + contractDefinition: 'export interface MyContract { test(): Promise; }', }; expect(() => validateAndConvertMidnightArtifacts(invalidArtifacts)).toThrow( - 'Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractSchema properties.' + 'Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractDefinition properties.' ); }); - it('should throw error for artifacts missing contractSchema', () => { + it('should throw error for artifacts missing contractDefinition', () => { const invalidArtifacts = { contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0', privateStateId: 'my-unique-state-id', }; expect(() => validateAndConvertMidnightArtifacts(invalidArtifacts)).toThrow( - 'Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractSchema properties.' + 'Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractDefinition properties.' ); }); @@ -75,11 +75,11 @@ describe('validateAndConvertMidnightArtifacts', () => { const invalidArtifacts = { contractAddress: 123, privateStateId: 'my-unique-state-id', - contractSchema: 'export interface MyContract { test(): Promise; }', + contractDefinition: 'export interface MyContract { test(): Promise; }', }; expect(() => validateAndConvertMidnightArtifacts(invalidArtifacts)).toThrow( - 'Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractSchema properties.' + 'Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractDefinition properties.' ); }); @@ -87,23 +87,23 @@ describe('validateAndConvertMidnightArtifacts', () => { const invalidArtifacts = { contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0', privateStateId: 123, - contractSchema: 'export interface MyContract { test(): Promise; }', + contractDefinition: 'export interface MyContract { test(): Promise; }', }; expect(() => validateAndConvertMidnightArtifacts(invalidArtifacts)).toThrow( - 'Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractSchema properties.' + 'Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractDefinition properties.' ); }); - it('should throw error for artifacts with non-string contractSchema', () => { + it('should throw error for artifacts with non-string contractDefinition', () => { const invalidArtifacts = { contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0', privateStateId: 'my-unique-state-id', - contractSchema: 123, + contractDefinition: 123, }; expect(() => validateAndConvertMidnightArtifacts(invalidArtifacts)).toThrow( - 'Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractSchema properties.' + 'Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractDefinition properties.' ); }); diff --git a/packages/adapter-midnight/src/utils/artifacts.ts b/packages/adapter-midnight/src/utils/artifacts.ts index 135e609a..fc61a82b 100644 --- a/packages/adapter-midnight/src/utils/artifacts.ts +++ b/packages/adapter-midnight/src/utils/artifacts.ts @@ -23,7 +23,7 @@ export function validateAndConvertMidnightArtifacts( // Validate that the object has the required structure if (!isMidnightContractArtifacts(source)) { throw new Error( - 'Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractSchema properties.' + 'Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractDefinition properties.' ); } diff --git a/packages/builder/src/components/UIBuilder/StepContractDefinition/StepContractDefinition.tsx b/packages/builder/src/components/UIBuilder/StepContractDefinition/StepContractDefinition.tsx index a6fcb6f1..6fc89407 100644 --- a/packages/builder/src/components/UIBuilder/StepContractDefinition/StepContractDefinition.tsx +++ b/packages/builder/src/components/UIBuilder/StepContractDefinition/StepContractDefinition.tsx @@ -71,6 +71,8 @@ export function StepContractDefinition({ contractAddressValue, currentContractAddress: contractState.address, networkId: networkConfig?.id, + adapter, + debouncedValues, }); // Automatic contract loading diff --git a/packages/builder/src/components/UIBuilder/StepContractDefinition/hooks/useAutoContractLoad.ts b/packages/builder/src/components/UIBuilder/StepContractDefinition/hooks/useAutoContractLoad.ts index 5cfa8aff..ce5357a6 100644 --- a/packages/builder/src/components/UIBuilder/StepContractDefinition/hooks/useAutoContractLoad.ts +++ b/packages/builder/src/components/UIBuilder/StepContractDefinition/hooks/useAutoContractLoad.ts @@ -34,9 +34,11 @@ export function useAutoContractLoad({ }: UseAutoContractLoadProps) { useEffect(() => { const attemptAutomaticLoad = async () => { - // Avoid duplicate loads: when the store indicates a load is needed, - // let the centralized store effect handle it instead of this hook. - if (needsContractDefinitionLoad) { + // Avoid duplicate loads during initial typing: if the store indicates a load is needed + // and the form isn't valid yet, let the centralized store effect handle it later. + // Once the form is valid (all required fields present), this hook should proceed + // to trigger the load with full, current form values. + if (needsContractDefinitionLoad && !formIsValid) { return; } diff --git a/packages/builder/src/components/UIBuilder/StepContractDefinition/hooks/useContractLoader.ts b/packages/builder/src/components/UIBuilder/StepContractDefinition/hooks/useContractLoader.ts index f10564c7..74144429 100644 --- a/packages/builder/src/components/UIBuilder/StepContractDefinition/hooks/useContractLoader.ts +++ b/packages/builder/src/components/UIBuilder/StepContractDefinition/hooks/useContractLoader.ts @@ -87,6 +87,8 @@ export function useContractLoader({ adapter, ignoreProxy }: UseContractLoaderPro // Do not attempt loads for invalid/partial addresses while the user is typing if (!adapter.isValidAddress(address)) return; + // Upstream gating prevents early loads; loader preflight enforces safety. + // Create unique key for this specific load attempt // This allows tracking failures per contract/definition combination const definitionHash = @@ -164,7 +166,6 @@ export function useContractLoader({ adapter, ignoreProxy }: UseContractLoaderPro }, [adapter] ); - /** * Check if we should attempt to load based on form values * Prevents duplicate loads for the same form state diff --git a/packages/builder/src/components/UIBuilder/StepContractDefinition/hooks/useFormSync.ts b/packages/builder/src/components/UIBuilder/StepContractDefinition/hooks/useFormSync.ts index c26ec2fe..e00e1262 100644 --- a/packages/builder/src/components/UIBuilder/StepContractDefinition/hooks/useFormSync.ts +++ b/packages/builder/src/components/UIBuilder/StepContractDefinition/hooks/useFormSync.ts @@ -1,5 +1,7 @@ import { useEffect } from 'react'; +import type { ContractAdapter, FormValues } from '@openzeppelin/ui-builder-types'; + import { contractDefinitionService } from '../../../../services/ContractDefinitionService'; import { uiBuilderStore } from '../../hooks/uiBuilderStore'; @@ -8,6 +10,8 @@ interface UseFormSyncProps { contractAddressValue: string | undefined; currentContractAddress: string | null; networkId?: string | null; + adapter?: ContractAdapter | null; + debouncedValues?: FormValues; } /** @@ -18,6 +22,8 @@ export function useFormSync({ contractAddressValue, currentContractAddress, networkId, + adapter, + debouncedValues, }: UseFormSyncProps) { // Sync manual definition changes to the store useEffect(() => { @@ -85,4 +91,37 @@ export function useFormSync({ uiBuilderStore.resetDownstreamSteps('contract'); } }, [contractAddressValue, currentContractAddress]); + + // Sync adapter-declared artifact inputs generically into contractDefinitionArtifacts + useEffect(() => { + if (!adapter || !debouncedValues) return; + if (typeof adapter.getContractDefinitionInputs !== 'function') return; + + try { + const inputs = adapter.getContractDefinitionInputs() || []; + // Collect values for inputs other than the canonical ones stored elsewhere + const artifacts: Record = {}; + for (const field of inputs as Array<{ name?: string; id?: string }>) { + const key = field.name || field.id || ''; + if (!key || key === 'contractAddress' || key === 'contractDefinition') continue; + const value = (debouncedValues as Record)[key]; + if (value !== undefined) artifacts[key] = value as unknown; + } + + // Update only if changed + const state = uiBuilderStore.getState(); + const prev = state.contractState.contractDefinitionArtifacts || {}; + const changed = JSON.stringify(prev) !== JSON.stringify(artifacts); + if (changed) { + uiBuilderStore.updateState((s) => ({ + contractState: { + ...s.contractState, + contractDefinitionArtifacts: artifacts, + }, + })); + } + } catch { + // no-op on adapter errors + } + }, [adapter, debouncedValues]); } diff --git a/packages/builder/src/components/UIBuilder/hooks/builder/useAutoSave.ts b/packages/builder/src/components/UIBuilder/hooks/builder/useAutoSave.ts index 6591437d..ea880617 100644 --- a/packages/builder/src/components/UIBuilder/hooks/builder/useAutoSave.ts +++ b/packages/builder/src/components/UIBuilder/hooks/builder/useAutoSave.ts @@ -73,7 +73,8 @@ async function prepareRecordWithDefinition( currentState.contractState.metadata || undefined, shouldPreserveStoredDefinition ? storedOriginal - : currentState.contractState.definitionOriginal || undefined + : currentState.contractState.definitionOriginal || undefined, + currentState.contractState.contractDefinitionArtifacts || undefined ); } @@ -153,7 +154,8 @@ export function useAutoSave(isLoadingSavedConfigRef: React.RefObject): currentState.contractState.definitionJson, currentState.contractState.source ?? undefined, currentState.contractState.metadata || undefined, - currentState.contractState.definitionOriginal || undefined + currentState.contractState.definitionOriginal || undefined, + currentState.contractState.contractDefinitionArtifacts || undefined ) : ({ ...configToSave, @@ -165,6 +167,7 @@ export function useAutoSave(isLoadingSavedConfigRef: React.RefObject): contractDefinitionMetadata: undefined, contractDefinitionOriginal: '', contractDefinitionSource: undefined, + contractDefinitionArtifacts: undefined, } as const); // Create new record @@ -220,6 +223,7 @@ export function useAutoSave(isLoadingSavedConfigRef: React.RefObject): contractDefinitionMetadata: undefined, contractDefinitionOriginal: '', contractDefinitionSource: undefined, + contractDefinitionArtifacts: undefined, } as const); // Save configuration @@ -261,6 +265,7 @@ export function useAutoSave(isLoadingSavedConfigRef: React.RefObject): contractDefinitionJson: state.contractState.definitionJson, contractDefinitionSource: state.contractState.source, contractDefinitionMetadata: state.contractState.metadata, + contractDefinitionArtifacts: state.contractState.contractDefinitionArtifacts, }); /** diff --git a/packages/builder/src/components/UIBuilder/hooks/builder/useBuilderLifecycle.ts b/packages/builder/src/components/UIBuilder/hooks/builder/useBuilderLifecycle.ts index dd73d378..46209d60 100644 --- a/packages/builder/src/components/UIBuilder/hooks/builder/useBuilderLifecycle.ts +++ b/packages/builder/src/components/UIBuilder/hooks/builder/useBuilderLifecycle.ts @@ -82,6 +82,7 @@ export function useBuilderLifecycle( contractDefinitionOriginal: savedUI.contractDefinitionOriginal, contractDefinitionSource: savedUI.contractDefinitionSource, contractDefinitionMetadata: savedUI.contractDefinitionMetadata, + contractDefinitionArtifacts: savedUI.contractDefinitionArtifacts, }); // Set the active network to trigger wallet connection and network switch diff --git a/packages/builder/src/components/UIBuilder/hooks/uiBuilderStore.ts b/packages/builder/src/components/UIBuilder/hooks/uiBuilderStore.ts index 8e6a09ab..cdd202ce 100644 --- a/packages/builder/src/components/UIBuilder/hooks/uiBuilderStore.ts +++ b/packages/builder/src/components/UIBuilder/hooks/uiBuilderStore.ts @@ -24,6 +24,7 @@ export interface ContractState { metadata: ContractDefinitionMetadata | null; proxyInfo: ProxyInfo | null; error: string | null; + contractDefinitionArtifacts: Record | null; } export interface UIBuilderState { @@ -85,6 +86,7 @@ export interface UIBuilderActions { contractDefinitionOriginal?: string; contractDefinitionSource?: 'fetched' | 'manual'; contractDefinitionMetadata?: ContractDefinitionMetadata; + contractDefinitionArtifacts?: Record; } ) => void; setManualContractDefinition: (definition: string) => void; @@ -96,6 +98,7 @@ export interface UIBuilderActions { metadata: ContractDefinitionMetadata; original: string; proxyInfo?: ProxyInfo | null; + contractDefinitionArtifacts?: Record | null; }) => void; setContractDefinitionError: (error: string) => void; acceptCurrentContractDefinition: () => void; @@ -111,6 +114,7 @@ const initialContractState: ContractState = { metadata: null, proxyInfo: null, error: null, + contractDefinitionArtifacts: null, }; const initialState: UIBuilderState = { @@ -205,6 +209,7 @@ export const uiBuilderStoreVanilla = createStore; } ) => { const determineStepFromSavedConfig = (config: typeof savedConfig): number => { @@ -254,6 +259,11 @@ export const uiBuilderStoreVanilla = createStore + hasMissingRequiredContractInputs(adapter, values), + [] + ); + // Contract definition loading hook with automatic deduplication const contractDefinition = useContractDefinition({ - onLoaded: (schema, formValues, source, metadata, originalDefinition) => { + onLoaded: (schema, formValues, source, metadata, originalDefinition, artifacts) => { // Update store with fresh contract definition uiBuilderStore.setContractDefinitionResult({ schema, @@ -60,6 +69,7 @@ export function useUIBuilderState() { source, metadata: metadata ?? {}, original: originalDefinition ?? '', + contractDefinitionArtifacts: artifacts ?? null, }); }, onError: (err) => { @@ -96,6 +106,15 @@ export function useUIBuilderState() { (state.selectedNetworkConfigId === activeAdapter.networkConfig.id || !state.selectedNetworkConfigId) ) { + // Prevent triggering loads for adapters (e.g., Midnight) that require multiple artifacts + const candidateValues = { + ...(state.contractState.formValues || ({} as FormValues)), + contractAddress: state.contractState.address, + } as FormValues; + if (hasMissingRequiredFields(activeAdapter, candidateValues)) { + return; + } + // Build form values using the latest address from store to avoid stale resets const baseFormValues = state.contractState.formValues || { contractAddress: '' }; const mergedFormValues = { @@ -111,6 +130,7 @@ export function useUIBuilderState() { state.isLoadingConfiguration, activeAdapter?.networkConfig.id, contractDefinition.load, + hasMissingRequiredFields, ]); const contractWidget = useContractWidgetState(); diff --git a/packages/builder/src/hooks/useContractDefinition.ts b/packages/builder/src/hooks/useContractDefinition.ts index 676ffc0f..3354a444 100644 --- a/packages/builder/src/hooks/useContractDefinition.ts +++ b/packages/builder/src/hooks/useContractDefinition.ts @@ -27,7 +27,8 @@ interface UseContractDefinitionOptions { formValues: FormValues, source: ContractLoadResult['source'], metadata?: ContractDefinitionMetadata, - originalDefinition?: string + originalDefinition?: string, + artifacts?: Record | null ) => void; /** @@ -98,7 +99,8 @@ export function useContractDefinition( formValues, result.source, metadata, - result.contractDefinitionOriginal + result.contractDefinitionOriginal, + result.contractDefinitionArtifacts || null ); } }, diff --git a/packages/builder/src/services/ContractLoader.ts b/packages/builder/src/services/ContractLoader.ts index 547bd2b9..183f98c1 100644 --- a/packages/builder/src/services/ContractLoader.ts +++ b/packages/builder/src/services/ContractLoader.ts @@ -10,7 +10,7 @@ import { FormValues, ProxyInfo, } from '@openzeppelin/ui-builder-types'; -import { logger } from '@openzeppelin/ui-builder-utils'; +import { getMissingRequiredContractInputs, logger } from '@openzeppelin/ui-builder-utils'; /** * Loads a contract definition using the provided chain adapter. @@ -50,6 +50,7 @@ export interface ContractLoadResult { schema: ContractSchema; source: 'fetched' | 'manual'; contractDefinitionOriginal?: string; + contractDefinitionArtifacts?: Record; metadata?: { fetchedFrom?: string; contractName?: string; @@ -79,6 +80,12 @@ export async function loadContractDefinitionWithMetadata( throw new Error('Contract definition input is empty.'); } + // Defensive preflight: ensure all adapter-declared required fields are present + const missing = getMissingRequiredContractInputs(adapter, artifacts); + if (missing.length > 0) { + throw new Error(`Missing required fields: ${missing.join(', ')}`); + } + // Use adapter's enhanced method if available if (adapter.loadContractWithMetadata) { logger.info('ContractLoader', 'Using adapter loadContractWithMetadata method...'); diff --git a/packages/storage/src/services/ContractUIStorage.ts b/packages/storage/src/services/ContractUIStorage.ts index 9127c7c8..18fee87f 100644 --- a/packages/storage/src/services/ContractUIStorage.ts +++ b/packages/storage/src/services/ContractUIStorage.ts @@ -152,7 +152,8 @@ export class ContractUIStorage extends DexieStorage { contractDefinition?: string, contractDefinitionSource?: 'fetched' | 'manual', contractDefinitionMetadata?: ContractDefinitionMetadata, - contractDefinitionOriginal?: string + contractDefinitionOriginal?: string, + contractDefinitionArtifacts?: Record ): Omit { const recordWithDefinition = { ...record, @@ -160,6 +161,7 @@ export class ContractUIStorage extends DexieStorage { contractDefinition, contractDefinitionOriginal, contractDefinitionMetadata, + contractDefinitionArtifacts, }; return recordWithDefinition; @@ -173,7 +175,8 @@ export class ContractUIStorage extends DexieStorage { contractDefinition: string, contractDefinitionSource: 'fetched' | 'manual', contractDefinitionMetadata?: ContractDefinitionMetadata, - contractDefinitionOriginal?: string + contractDefinitionOriginal?: string, + contractDefinitionArtifacts?: Record ): Promise { try { const existing = await this.get(id); @@ -186,6 +189,7 @@ export class ContractUIStorage extends DexieStorage { contractDefinitionOriginal, contractDefinitionSource, contractDefinitionMetadata, + contractDefinitionArtifacts, updatedAt: new Date(), }; diff --git a/packages/storage/src/types.ts b/packages/storage/src/types.ts index f077f90b..7d28de14 100644 --- a/packages/storage/src/types.ts +++ b/packages/storage/src/types.ts @@ -27,6 +27,12 @@ export interface ContractUIRecord extends BaseRecord { contractDefinitionOriginal?: string; // Original raw contract definition for data lineage (manual: same as contractDefinition, fetched: raw from explorer) contractDefinitionSource?: 'fetched' | 'manual'; contractDefinitionMetadata?: ContractDefinitionMetadata; // Metadata about fetch process (excludes definition content) + /** + * Chain-agnostic contract definition artifacts required for adapters that need + * additional inputs beyond a single definition. Example: Midnight requires + * private state ID, compiled module, and optional witness code. + */ + contractDefinitionArtifacts?: Record; } export interface ContractUIExportData { diff --git a/packages/utils/src/__tests__/contractInputs.test.ts b/packages/utils/src/__tests__/contractInputs.test.ts new file mode 100644 index 00000000..6bb526d6 --- /dev/null +++ b/packages/utils/src/__tests__/contractInputs.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, test } from 'vitest'; + +import type { + ContractAdapter, + ContractFunction, + ContractSchema, + FormFieldType, + FormValues, + FunctionParameter, + RelayerDetailsRich, +} from '@openzeppelin/ui-builder-types'; + +import { + getMissingRequiredContractInputs, + hasMissingRequiredContractInputs, +} from '../contractInputs'; + +function makeAdapter( + fields: Array & { id: string; name?: string; required?: boolean }> +): ContractAdapter { + const inputs: FormFieldType[] = fields.map((f) => ({ + id: f.id, + name: f.name ?? f.id, + label: f.name ?? f.id, + type: 'text', + validation: { required: f.required ?? false }, + })) as FormFieldType[]; + + return { + // Minimal stub for tests; only methods used by util need to exist + networkConfig: {} as unknown as ContractAdapter['networkConfig'], + initialAppServiceKitName: 'custom', + loadContract: async () => ({ + name: 'x', + ecosystem: 'evm', + address: '0x', + functions: [], + events: [], + }), + getWritableFunctions: (s: ContractSchema): ContractFunction[] => s.functions, + mapParameterTypeToFieldType: () => 'text', + getCompatibleFieldTypes: () => ['text'], + generateDefaultField: (p: FunctionParameter) => ({ + id: p.name, + name: p.name, + label: p.name, + type: 'text', + validation: {}, + }), + signAndBroadcast: async () => ({ txHash: '0x' }), + isValidAddress: () => true, + getSupportedExecutionMethods: async () => [], + validateExecutionConfig: async () => true, + isViewFunction: () => true, + queryViewFunction: async () => ({}), + formatFunctionResult: () => '', + getExplorerUrl: () => null, + getAvailableUiKits: async () => [], + getRelayers: async () => [], + getRelayer: async () => ({}) as unknown as RelayerDetailsRich, + getContractDefinitionInputs: () => inputs, + } as unknown as ContractAdapter; +} + +describe('contractInputs utils', () => { + test('returns empty when no required inputs', () => { + const adapter = makeAdapter([{ id: 'a' }, { id: 'b' }]); + const values: FormValues = { a: '', b: '' }; + expect(getMissingRequiredContractInputs(adapter, values)).toEqual([]); + expect(hasMissingRequiredContractInputs(adapter, values)).toBe(false); + }); + + test('detects missing required string fields', () => { + const adapter = makeAdapter([ + { id: 'contractAddress', required: true }, + { id: 'contractDefinition', required: true }, + { id: 'optionalX', required: false }, + ]); + const values: FormValues = { contractAddress: ' ', optionalX: 'ok' }; + expect(getMissingRequiredContractInputs(adapter, values)).toEqual([ + 'contractAddress', + 'contractDefinition', + ]); + expect(hasMissingRequiredContractInputs(adapter, values)).toBe(true); + }); + + test('passes when all required have non-empty values', () => { + const adapter = makeAdapter([ + { id: 'contractAddress', required: true }, + { id: 'contractDefinition', required: true }, + { id: 'privateStateId', required: true }, + ]); + const values: FormValues = { + contractAddress: '0xabc', + contractDefinition: '{ }', + privateStateId: 'state-1', + }; + expect(getMissingRequiredContractInputs(adapter, values)).toEqual([]); + expect(hasMissingRequiredContractInputs(adapter, values)).toBe(false); + }); + + test('tolerates adapters throwing/invalid inputs', () => { + const badAdapter = { + networkConfig: {} as unknown as ContractAdapter['networkConfig'], + initialAppServiceKitName: 'custom', + getContractDefinitionInputs: () => { + throw new Error('boom'); + }, + } as unknown as ContractAdapter; + expect(getMissingRequiredContractInputs(badAdapter, {})).toEqual([]); + expect(hasMissingRequiredContractInputs(badAdapter, {})).toBe(false); + }); +}); diff --git a/packages/utils/src/contractInputs.ts b/packages/utils/src/contractInputs.ts new file mode 100644 index 00000000..b970fa3b --- /dev/null +++ b/packages/utils/src/contractInputs.ts @@ -0,0 +1,43 @@ +import type { ContractAdapter, FormValues } from '@openzeppelin/ui-builder-types'; + +/** + * Returns names of adapter-declared required inputs that are missing/empty in values. + */ +export function getMissingRequiredContractInputs( + adapter: ContractAdapter, + values: FormValues +): string[] { + try { + const inputs = adapter.getContractDefinitionInputs ? adapter.getContractDefinitionInputs() : []; + const required = inputs.filter((field: unknown) => { + const f = field as { validation?: { required?: boolean } }; + return f?.validation?.required === true; + }); + const missing: string[] = []; + for (const field of required as Array<{ name?: string; id?: string }>) { + const key = field.name || field.id || ''; + const raw = (values as Record)[key]; + if (raw == null) { + missing.push(key); + continue; + } + if (typeof raw === 'string' && raw.trim().length === 0) { + missing.push(key); + } + } + return missing; + } catch { + return []; + } +} + +/** + * True if any adapter-declared required inputs are missing/empty. + */ +export function hasMissingRequiredContractInputs( + adapter: ContractAdapter | null | undefined, + values: FormValues +): boolean { + if (!adapter) return false; + return getMissingRequiredContractInputs(adapter, values).length > 0; +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 59b3ac5f..8af19ec8 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,3 +1,4 @@ +export * from './contractInputs'; export * from './logger'; export * from './AppConfigService'; export * from './UserRpcConfigService'; diff --git a/specs/004-add-midnight-adapter/checklists/architecture.md b/specs/004-add-midnight-adapter/checklists/architecture.md index 068010ea..79e2972a 100644 --- a/specs/004-add-midnight-adapter/checklists/architecture.md +++ b/specs/004-add-midnight-adapter/checklists/architecture.md @@ -6,47 +6,47 @@ ## Requirement Completeness -- [ ] CHK001 Are core packages explicitly stated to remain chain‑agnostic with no Midnight deps? [Completeness, Plan §Constitution Check] -- [ ] CHK002 Are all adapter responsibilities enumerated (wallet, ingestion, mapping, transaction, diagnostics)? [Completeness, Spec §FR-001–FR-016] -- [ ] CHK003 Do export requirements list all adapter runtime deps (incl. @midnight-ntwrk/dapp-connector-api)? [Completeness, Spec §FR-015, Export Manifest] +- [x] CHK001 Are core packages explicitly stated to remain chain‑agnostic with no Midnight deps? [Completeness, Plan §Constitution Check] +- [x] CHK002 Are all adapter responsibilities enumerated (wallet, ingestion, mapping, transaction, diagnostics)? [Completeness, Spec §FR-001–FR-016] +- [x] CHK003 Do export requirements list all adapter runtime deps (incl. @midnight-ntwrk/dapp-connector-api)? [Completeness, Spec §FR-015, Export Manifest] ## Requirement Clarity -- [ ] CHK004 Is “wallet‑only v1” execution clearly defined and exclusions listed? [Clarity, Spec §FR-011; Non‑Goals] -- [ ] CHK005 Are contract inputs unambiguously defined with required/optional flags? [Clarity, Spec §FR-012] -- [ ] CHK006 Is post‑submission status precisely defined (identifier + indexing summary)? [Clarity, Spec §FR-013] +- [x] CHK004 Is “wallet‑only v1” execution clearly defined and exclusions listed? [Clarity, Spec §FR-011; Non‑Goals] +- [x] CHK005 Are contract inputs unambiguously defined with required/optional flags? [Clarity, Spec §FR-012] +- [x] CHK006 Is post‑submission status precisely defined (identifier + indexing summary)? [Clarity, Spec §FR-013] ## Requirement Consistency -- [ ] CHK007 Do plan, adapter integration, and spec align on view capability (parameter‑less auto views only)? [Consistency, Spec §User Story 3] -- [ ] CHK008 Do execution method requirements match adapter integration (no relayer/multisig in v1)? [Consistency, Spec §FR-011, Adapter Integration] -- [ ] CHK009 Do export manifest and adapter config dependencies match each other? [Consistency, Export Manifest, Adapter Config] +- [x] CHK007 Do plan, adapter integration, and spec align on view capability (parameter‑less auto views only)? [Consistency, Spec §User Story 3] +- [x] CHK008 Do execution method requirements match adapter integration (no relayer/multisig in v1)? [Consistency, Spec §FR-011, Adapter Integration] +- [x] CHK009 Do export manifest and adapter config dependencies match each other? [Consistency, Export Manifest, Adapter Config] ## Acceptance Criteria Quality -- [ ] CHK010 Are success criteria measurable and technology‑agnostic (timings, rates)? [Acceptance, Spec §Success Criteria] -- [ ] CHK011 Do success criteria cover export app functioning equivalently to Builder? [Acceptance, Spec §User Story 4, §FR-015] +- [x] CHK010 Are success criteria measurable and technology‑agnostic (timings, rates)? [Acceptance, Spec §Success Criteria] +- [x] CHK011 Do success criteria cover export app functioning equivalently to Builder? [Acceptance, Spec §User Story 4, §FR-015] ## Scenario Coverage -- [ ] CHK012 Are primary flows defined for wallet connect, ingestion, auto views, write execute, export? [Coverage, Spec §User Stories] -- [ ] CHK013 Are exception flows covered (wallet cancel/lock, invalid inputs, submission reject)? [Coverage, Spec §Acceptance] +- [x] CHK012 Are primary flows defined for wallet connect, ingestion, auto views, write execute, export? [Coverage, Spec §User Stories] +- [x] CHK013 Are exception flows covered (wallet cancel/lock, invalid inputs, submission reject)? [Coverage, Spec §Acceptance] ## Edge Case Coverage -- [ ] CHK014 Are boundary cases listed (no explorer, indexing delays, missing deps in exports)? [Edge Case, Spec §Edge Cases] -- [ ] CHK015 Are network diagnostics behaviors specified (success/failure, latency)? [Edge Case, Spec §User Story 6] +- [x] CHK014 Are boundary cases listed (no explorer, indexing delays, missing deps in exports)? [Edge Case, Spec §Edge Cases] +- [x] CHK015 Are network diagnostics behaviors specified (success/failure, latency)? [Edge Case, Spec §User Story 6] ## Non‑Functional Requirements -- [ ] CHK016 Are architectural boundaries (no chain code in core) enforced as non‑negotiable? [NFR, Plan §Constitution Check] -- [ ] CHK017 Are design system and storage usage requirements documented? [NFR, Plan §Technical Context] +- [x] CHK016 Are architectural boundaries (no chain code in core) enforced as non‑negotiable? [NFR, Plan §Constitution Check] +- [x] CHK017 Are design system and storage usage requirements documented? [NFR, Plan §Technical Context] ## Dependencies & Assumptions -- [ ] CHK018 Are dependencies and assumptions listed (ecosystem registration, wallet capabilities)? [Dependencies, Spec §Dependencies & Assumptions] +- [x] CHK018 Are dependencies and assumptions listed (ecosystem registration, wallet capabilities)? [Dependencies, Spec §Dependencies & Assumptions] ## Ambiguities & Conflicts -- [ ] CHK019 Are any adapter methods marked placeholder documented with rationale and phase plan? [Ambiguity, Adapter Integration] -- [ ] CHK020 If adapter config deviates from export manifest, is resolution path defined? [Conflict, Export Manifest vs Adapter Config] +- [x] CHK019 Are any adapter methods marked placeholder documented with rationale and phase plan? [Ambiguity, Adapter Integration] +- [x] CHK020 If adapter config deviates from export manifest, is resolution path defined? [Conflict, Export Manifest vs Adapter Config] diff --git a/specs/004-add-midnight-adapter/checklists/parity.md b/specs/004-add-midnight-adapter/checklists/parity.md index b1cc010d..8ea5c368 100644 --- a/specs/004-add-midnight-adapter/checklists/parity.md +++ b/specs/004-add-midnight-adapter/checklists/parity.md @@ -6,36 +6,36 @@ ## Requirement Completeness -- [ ] CHK001 Are parity targets explicitly stated for each phase (wallet, ingestion, auto views, write + export)? [Completeness, Spec §FR-010; User Stories] -- [ ] CHK002 Are mapping/type handling expectations aligned with EVM/Stellar (field types, defaults)? [Completeness, Spec §FR-003–FR-006, §FR-018] -- [ ] CHK003 Are transaction lifecycle requirements aligned (status messaging, identifiers)? [Completeness, Spec §FR-007, FR-013] +- [x] CHK001 Are parity targets explicitly stated for each phase (wallet, ingestion, auto views, write + export)? [Completeness, Spec §FR-010; User Stories] +- [x] CHK002 Are mapping/type handling expectations aligned with EVM/Stellar (field types, defaults)? [Completeness, Spec §FR-003–FR-006, §FR-018] +- [x] CHK003 Are transaction lifecycle requirements aligned (status messaging, identifiers)? [Completeness, Spec §FR-007, FR-013] ## Requirement Clarity -- [ ] CHK004 Is the scope of v1 limitations (wallet-only, no param views) clearly compared to EVM/Stellar? [Clarity, Spec §FR-011; User Story 3] -- [ ] CHK005 Is export parity defined (packages, config, behavior equivalence)? [Clarity, Spec §FR-015] +- [x] CHK004 Is the scope of v1 limitations (wallet-only, no param views) clearly compared to EVM/Stellar? [Clarity, Spec §FR-011; User Story 3] +- [x] CHK005 Is export parity defined (packages, config, behavior equivalence)? [Clarity, Spec §FR-015] ## Requirement Consistency -- [ ] CHK006 Do acceptance scenarios mirror EVM/Stellar flow patterns where applicable? [Consistency, Spec §User Stories] -- [ ] CHK007 Do plan test structures mirror EVM/Stellar test layout? [Consistency, Plan §Project Structure/tests] +- [x] CHK006 Do acceptance scenarios mirror EVM/Stellar flow patterns where applicable? [Consistency, Spec §User Stories] +- [x] CHK007 Do plan test structures mirror EVM/Stellar test layout? [Consistency, Plan §Project Structure/tests] ## Acceptance Criteria Quality -- [ ] CHK008 Are cross-adapter measurable outcomes consistent (timings, success rates)? [Acceptance, Spec §Success Criteria] +- [x] CHK008 Are cross-adapter measurable outcomes consistent (timings, success rates)? [Acceptance, Spec §Success Criteria] ## Scenario Coverage -- [ ] CHK009 Are alternate/exception flows covered similarly to EVM/Stellar (cancel, reject, invalid inputs)? [Coverage, Spec §Acceptance] +- [x] CHK009 Are alternate/exception flows covered similarly to EVM/Stellar (cancel, reject, invalid inputs)? [Coverage, Spec §Acceptance] ## Edge Case Coverage -- [ ] CHK010 Are adapter-specific edge cases documented without breaking parity (no explorer, indexing delays)? [Edge Case, Spec §Edge Cases] +- [x] CHK010 Are adapter-specific edge cases documented without breaking parity (no explorer, indexing delays)? [Edge Case, Spec §Edge Cases] ## Dependencies & Assumptions -- [ ] CHK011 Are parity dependencies documented (e.g., dependency sync policy vs. export manifest)? [Dependencies, Spec §FR-017, Export Manifest] +- [x] CHK011 Are parity dependencies documented (e.g., dependency sync policy vs. export manifest)? [Dependencies, Spec §FR-017, Export Manifest] ## Ambiguities & Conflicts -- [ ] CHK012 Are any intentional deviations from EVM/Stellar documented with rationale (e.g., no relayer in v1)? [Ambiguity, Spec §FR-011] +- [x] CHK012 Are any intentional deviations from EVM/Stellar documented with rationale (e.g., no relayer in v1)? [Ambiguity, Spec §FR-011] diff --git a/specs/004-add-midnight-adapter/tasks.md b/specs/004-add-midnight-adapter/tasks.md index 258417ef..11185b79 100644 --- a/specs/004-add-midnight-adapter/tasks.md +++ b/specs/004-add-midnight-adapter/tasks.md @@ -23,22 +23,22 @@ ## Phase 1: Setup (Shared) -- [ ] T001 [US0] Configure feature branch context for developers [P] +- [x] T001 [US0] Configure feature branch context for developers [P] - Path: N/A -- [ ] T002 [US0] Document dependency sync policy in README notes [P] +- [x] T002 [US0] Document dependency sync policy in README notes [P] - Path: specs/004-add-midnight-adapter/plan.md - Notes: Align adapter config vs export manifest per FR-017 ## Phase 2: Foundational (Blocking) -- [ ] T003 [US0] Ensure ecosystem registration for Midnight exists and loads adapter [seq] +- [x] T003 [US0] Ensure ecosystem registration for Midnight exists and loads adapter [seq] - Path: packages/builder/src/core/ecosystemManager.ts -- [ ] T004 [US0] Persist contract definition inputs scaffolding [seq] +- [x] T004 [US0] Persist contract definition inputs scaffolding [seq] - Path: packages/builder (storage service) + adapter-midnight integration -- [ ] T005 [US0] Export manifest wiring for Midnight [seq] +- [x] T005 [US0] Export manifest wiring for Midnight [seq] - Path: specs/004-add-midnight-adapter/contracts/export-manifest.md -- [ ] Checkpoint: Foundation ready – user story implementation can begin +- [x] Checkpoint: Foundation ready – user story implementation can begin ## Phase 3: [US1] Wallet connect + account (P1) @@ -59,14 +59,14 @@ Independent Test: Select Midnight network, click Connect, see connected address. Goal: Provide inputs and load contract schema; render functions list. Independent Test: Submit valid inputs and see callable functions list. -- [ ] T009 [US2] Implement persistence of `getContractDefinitionInputs()` values [seq] +- [x] T009 [US2] Implement persistence of `getContractDefinitionInputs()` values [seq] - Path: packages/builder storage + adapter-midnight bridge -- [ ] T010 [US2] Build `loadContract` conversion to `ContractSchema` with validation [seq] - - Path: packages/adapter-midnight/src/adapter.ts + utils -- [ ] T011 [US2] Add validation errors for malformed inputs [P] - - Path: packages/adapter-midnight/src/adapter.ts +- [x] T010 [US2] Build `loadContract` conversion to `ContractSchema` with validation [seq] + - Path: packages/adapter-midnight/src/contract/loader.ts + adapter.ts +- [x] T011 [US2] Add validation errors for malformed inputs [P] + - Path: packages/adapter-midnight/src/utils/artifacts.ts + adapter.ts -- [ ] Checkpoint: User Stories 1 and 2 work independently +- [x] Checkpoint: User Stories 1 and 2 work independently ## Phase 5: [US3] Auto simple view rendering (P3)