Skip to content
14 changes: 14 additions & 0 deletions .changeset/midnight-contract-ingestion.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 16 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
node_modules/
dist/
build/
out/
coverage/
*.min.js
.next/
.nuxt/
.output/
.cache/
.turbo/
.vercel/
.netlify/
exports/
packages/builder/test-results/

16 changes: 16 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -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/
Expand Down
46 changes: 34 additions & 12 deletions packages/adapter-midnight/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -163,23 +164,44 @@ export class MidnightAdapter implements ContractAdapter {
}

public async loadContract(source: string | Record<string, unknown>): Promise<ContractSchema> {
// 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<string, unknown>): 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<string, unknown>;
}> {
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[] {
Expand Down
28 changes: 3 additions & 25 deletions packages/adapter-midnight/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {},
},
};
63 changes: 63 additions & 0 deletions packages/adapter-midnight/src/contract/__tests__/loader.test.ts
Original file line number Diff line number Diff line change
@@ -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<T> = {
post(context: any, new_message: string): any;
};
export declare class Contract<T> {
readonly circuits: Circuits<T>;
}`;

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();
});
});
1 change: 1 addition & 0 deletions packages/adapter-midnight/src/contract/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './loader';
70 changes: 70 additions & 0 deletions packages/adapter-midnight/src/contract/loader.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
proxyInfo?: undefined;
}

export async function loadMidnightContract(
artifacts: MidnightContractArtifacts,
_networkConfig?: MidnightNetworkConfig
): Promise<MidnightContractLoadResult> {
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<MidnightContractLoadResult> {
const base = await loadMidnightContract(artifacts, networkConfig);

const artifactsRecord: Record<string, unknown> = {};
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,
};
}
22 changes: 11 additions & 11 deletions packages/adapter-midnight/src/types/__tests__/artifacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe('Midnight Contract Artifacts', () => {
const artifacts: MidnightContractArtifacts = {
contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0',
privateStateId: 'my-unique-state-id',
contractSchema: 'export interface MyContract { test(): Promise<void>; }',
contractDefinition: 'export interface MyContract { test(): Promise<void>; }',
};

const result = isMidnightContractArtifacts(artifacts);
Expand All @@ -23,7 +23,7 @@ describe('Midnight Contract Artifacts', () => {
const artifacts: MidnightContractArtifacts = {
contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0',
privateStateId: 'my-unique-state-id',
contractSchema: 'export interface MyContract { test(): Promise<void>; }',
contractDefinition: 'export interface MyContract { test(): Promise<void>; }',
contractModule: 'module.exports = {};',
witnessCode: 'export const witnesses = {};',
};
Expand Down Expand Up @@ -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<void>; }',
contractDefinition: 'export interface MyContract { test(): Promise<void>; }',
};

const result = isMidnightContractArtifacts(artifacts);
Expand All @@ -64,15 +64,15 @@ describe('Midnight Contract Artifacts', () => {
it('should return false for object without privateStateId', () => {
const artifacts = {
contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0',
contractSchema: 'export interface MyContract { test(): Promise<void>; }',
contractDefinition: 'export interface MyContract { test(): Promise<void>; }',
};

const result = isMidnightContractArtifacts(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',
Expand All @@ -87,7 +87,7 @@ describe('Midnight Contract Artifacts', () => {
const artifacts = {
contractAddress: 123,
privateStateId: 'my-unique-state-id',
contractSchema: 'export interface MyContract { test(): Promise<void>; }',
contractDefinition: 'export interface MyContract { test(): Promise<void>; }',
};

const result = isMidnightContractArtifacts(artifacts);
Expand All @@ -99,19 +99,19 @@ describe('Midnight Contract Artifacts', () => {
const artifacts = {
contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0',
privateStateId: 123,
contractSchema: 'export interface MyContract { test(): Promise<void>; }',
contractDefinition: 'export interface MyContract { test(): Promise<void>; }',
};

const result = isMidnightContractArtifacts(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);
Expand All @@ -123,7 +123,7 @@ describe('Midnight Contract Artifacts', () => {
const artifacts = {
contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0',
privateStateId: 'my-unique-state-id',
contractSchema: 'export interface MyContract { test(): Promise<void>; }',
contractDefinition: 'export interface MyContract { test(): Promise<void>; }',
extraProperty: 'should be ignored',
anotherExtra: 42,
};
Expand All @@ -137,7 +137,7 @@ describe('Midnight Contract Artifacts', () => {
const artifacts = {
contractAddress: '',
privateStateId: '',
contractSchema: '',
contractDefinition: '',
};

const result = isMidnightContractArtifacts(artifacts);
Expand Down
Loading