Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ describe('MidnightAdapter - Heavy Artifact Handling Integration', () => {

expect(result.bootstrapSource).toBeDefined();
expect(result.bootstrapSource?.contractAddress).toBe('0x123');
expect(result.bootstrapSource?.privateStateId).toBe('test-state');
expect(result.bootstrapSource?.contractArtifactsUrl).toBe('/midnight/contract.zip');
});

Expand Down
10 changes: 0 additions & 10 deletions packages/adapter-midnight/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,16 +169,6 @@ export class MidnightAdapter implements ContractAdapter {
helperText:
'Enter the deployed Midnight contract address (68-character hex string starting with 0200).',
},
{
id: 'privateStateId',
name: 'privateStateId',
label: 'Private State ID',
type: 'text',
validation: { required: true },
placeholder: 'my-unique-state-id',
helperText:
'A unique identifier for your private state instance. This ID is used to manage your personal encrypted data.',
},
{
id: 'contractArtifactsZip',
name: 'contractArtifactsZip',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ 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 = {}',
Expand Down Expand Up @@ -51,7 +50,6 @@ describe('Midnight contract loader', () => {
it('loadMidnightContractWithMetadata omits artifacts when none provided', async () => {
const artifacts: MidnightContractArtifacts = {
contractAddress: 'ct1qexampleaddress3',
privateStateId: '',
contractDefinition: mockInterface,
contractModule: '',
witnessCode: '',
Expand Down
9 changes: 5 additions & 4 deletions packages/adapter-midnight/src/export/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ const SYSTEM_LOG_TAG = 'MidnightAdapter:ExportBootstrap';
* Instead of embedding massive artifact files, this bundles the original ZIP file
* as a base64 string and lets the adapter parse it using the same logic as the builder.
*
* Note: privateStateId is auto-generated at transaction time from contract + wallet address,
* so it's not included in the exported artifacts configuration.
*
* @param context - Export context with form config, schema, network, and artifacts
* @returns Bootstrap configuration with ZIP data and initialization code
*/
Expand All @@ -24,10 +27,9 @@ export async function getMidnightExportBootstrapFiles(

const contractAddress =
context.formConfig.contractAddress || context.contractSchema.address || '';
const privateStateId = (artifacts['privateStateId'] as string) || '';

if (!contractAddress || !privateStateId) {
logger.error(SYSTEM_LOG_TAG, 'Missing contract address or private state ID.');
if (!contractAddress) {
logger.error(SYSTEM_LOG_TAG, 'Missing contract address.');
return null;
}

Expand All @@ -48,7 +50,6 @@ export async function getMidnightExportBootstrapFiles(
const artifactsFileContent = `
export const midnightArtifactsSource = {
contractAddress: '${contractAddress}',
privateStateId: '${privateStateId}',
contractArtifactsUrl: '/midnight/contract.zip',
};
`;
Expand Down
8 changes: 6 additions & 2 deletions packages/adapter-midnight/src/transaction/eoa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { TransactionStatusUpdate } from '@openzeppelin/ui-builder-types';
import { hexToBytes, logger } from '@openzeppelin/ui-builder-utils';

import type { WriteContractParameters } from '../types';
import { generatePrivateStateId } from '../utils/private-state-id';
import { resolveSecretPropertyName } from '../utils/secret-property-helpers';
import type { LaceWalletImplementation } from '../wallet/implementation/lace-implementation';
import { callCircuit } from './call-circuit';
Expand Down Expand Up @@ -170,10 +171,13 @@ export class EoaExecutionStrategy implements ExecutionStrategy {
if (!contractAddress) {
throw new Error('Invalid contract address: empty or non-string value');
}
const privateStateId =

// Auto-generate privateStateId if not provided (deterministic from contract + wallet address)
let privateStateId =
typeof artifacts.privateStateId === 'string' ? artifacts.privateStateId.trim() : '';
if (!privateStateId) {
throw new Error('Invalid Private State ID: empty or non-string value');
privateStateId = generatePrivateStateId(contractAddress, walletState.address);
logger.info(SYSTEM_LOG_TAG, `Auto-generated privateStateId: ${privateStateId}`);
}

const witnesses = evaluateWitnessCode(artifacts.witnessCode || '');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export interface MidnightExecutionConfig {
executionConfig: ExecutionConfig;
// Add Midnight-specific fields
artifacts?: {
privateStateId: string;
// privateStateId is optional - auto-generated at execution time from contract + wallet address
privateStateId?: string;
contractModule: string;
witnessCode?: string;
verifierKeys?: Record<string, unknown>;
Expand Down
5 changes: 3 additions & 2 deletions packages/adapter-midnight/src/transaction/sender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,14 @@ export async function signAndBroadcastMidnightTransaction(
const left = a ?? ({} as MidnightContractArtifacts);
const right = b ?? ({} as MidnightContractArtifacts);

// privateStateId is optional - will be auto-generated at execution time if not provided
const privateStateId = left.privateStateId || right.privateStateId;
const contractModule = left.contractModule || right.contractModule;
const witnessCode = left.witnessCode || right.witnessCode;
const verifierKeys = left.verifierKeys || right.verifierKeys;

// Must have at least contractModule and privateStateId to be valid
if (!contractModule || !privateStateId) {
// Must have at least contractModule to be valid (privateStateId is auto-generated if missing)
if (!contractModule) {
return undefined;
}

Expand Down
15 changes: 4 additions & 11 deletions packages/adapter-midnight/src/types/__tests__/artifacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ describe('Midnight Contract Artifacts', () => {
it('should return true for valid artifacts with required properties', () => {
const artifacts: MidnightContractArtifacts = {
contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0',
privateStateId: 'my-unique-state-id',
contractDefinition: 'export interface MyContract { test(): Promise<void>; }',
};

Expand All @@ -22,7 +21,7 @@ describe('Midnight Contract Artifacts', () => {
it('should return true for valid artifacts with all properties', () => {
const artifacts: MidnightContractArtifacts = {
contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0',
privateStateId: 'my-unique-state-id',
privateStateId: 'my-unique-state-id', // Optional but can be provided
contractDefinition: 'export interface MyContract { test(): Promise<void>; }',
contractModule: 'module.exports = {};',
witnessCode: 'export const witnesses = {};',
Expand Down Expand Up @@ -52,7 +51,6 @@ describe('Midnight Contract Artifacts', () => {

it('should return false for object without contractAddress', () => {
const artifacts = {
privateStateId: 'my-unique-state-id',
contractDefinition: 'export interface MyContract { test(): Promise<void>; }',
};

Expand All @@ -61,21 +59,20 @@ describe('Midnight Contract Artifacts', () => {
expect(result).toBe(false);
});

it('should return false for object without privateStateId', () => {
it('should return true for object without privateStateId (optional field)', () => {
const artifacts = {
contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0',
contractDefinition: 'export interface MyContract { test(): Promise<void>; }',
};

const result = isMidnightContractArtifacts(artifacts);

expect(result).toBe(false);
expect(result).toBe(true);
});

it('should return false for object without contractDefinition', () => {
const artifacts = {
contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0',
privateStateId: 'my-unique-state-id',
};

const result = isMidnightContractArtifacts(artifacts);
Expand All @@ -86,7 +83,6 @@ describe('Midnight Contract Artifacts', () => {
it('should return false for object with non-string contractAddress', () => {
const artifacts = {
contractAddress: 123,
privateStateId: 'my-unique-state-id',
contractDefinition: 'export interface MyContract { test(): Promise<void>; }',
};

Expand All @@ -95,7 +91,7 @@ describe('Midnight Contract Artifacts', () => {
expect(result).toBe(false);
});

it('should return false for object with non-string privateStateId', () => {
it('should return false for object with non-string privateStateId (must be string or undefined)', () => {
const artifacts = {
contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0',
privateStateId: 123,
Expand All @@ -110,7 +106,6 @@ describe('Midnight Contract Artifacts', () => {
it('should return false for object with non-string contractDefinition', () => {
const artifacts = {
contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0',
privateStateId: 'my-unique-state-id',
contractDefinition: 123,
};

Expand All @@ -122,7 +117,6 @@ describe('Midnight Contract Artifacts', () => {
it('should return true even with extra properties', () => {
const artifacts = {
contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0',
privateStateId: 'my-unique-state-id',
contractDefinition: 'export interface MyContract { test(): Promise<void>; }',
extraProperty: 'should be ignored',
anotherExtra: 42,
Expand All @@ -136,7 +130,6 @@ describe('Midnight Contract Artifacts', () => {
it('should handle empty string values', () => {
const artifacts = {
contractAddress: '',
privateStateId: '',
contractDefinition: '',
};

Expand Down
11 changes: 8 additions & 3 deletions packages/adapter-midnight/src/types/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ export interface MidnightContractArtifacts {
/** The deployed contract address (required, Bech32m format) */
contractAddress: string;

/** Unique identifier for private state instance (required) */
privateStateId: string;
/**
* Unique identifier for private state instance.
* Auto-generated deterministically from contract address + wallet address.
* Only required at transaction execution time, not during contract loading.
*/
privateStateId?: string;

/**
* Contract-level identity secret key property name (derived from artifacts).
Expand Down Expand Up @@ -44,7 +48,8 @@ export function isMidnightContractArtifacts(obj: unknown): obj is MidnightContra
typeof obj === 'object' &&
obj !== null &&
typeof record.contractAddress === 'string' &&
typeof record.privateStateId === 'string' &&
// privateStateId is optional - auto-generated at transaction time
(record.privateStateId === undefined || typeof record.privateStateId === 'string') &&
typeof record.contractDefinition === 'string'
);
}
36 changes: 16 additions & 20 deletions packages/adapter-midnight/src/utils/__tests__/artifacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { validateAndConvertMidnightArtifacts } from '../artifacts';
describe('validateAndConvertMidnightArtifacts', () => {
const validArtifacts = {
contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0',
privateStateId: 'my-unique-state-id',
contractDefinition: 'export interface MyContract { test(): Promise<void>; }',
};

Expand Down Expand Up @@ -40,70 +39,67 @@ describe('validateAndConvertMidnightArtifacts', () => {

it('should throw error for artifacts missing contractAddress', async () => {
const invalidArtifacts = {
privateStateId: 'my-unique-state-id',
contractDefinition: 'export interface MyContract { test(): Promise<void>; }',
};

await expect(validateAndConvertMidnightArtifacts(invalidArtifacts)).rejects.toThrow(
'Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractDefinition properties.'
'Invalid contract artifacts provided. Expected an object with contractAddress'
);
});

it('should throw error for artifacts missing privateStateId', async () => {
const invalidArtifacts = {
it('should accept artifacts without privateStateId (auto-generated at transaction time)', async () => {
const artifactsWithoutPrivateStateId = {
contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0',
contractDefinition: 'export interface MyContract { test(): Promise<void>; }',
};

await expect(validateAndConvertMidnightArtifacts(invalidArtifacts)).rejects.toThrow(
'Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractDefinition properties.'
);
const result = await validateAndConvertMidnightArtifacts(artifactsWithoutPrivateStateId);

expect(result.contractAddress).toBe(artifactsWithoutPrivateStateId.contractAddress);
expect(result.privateStateId).toBeUndefined();
});

it('should throw error for artifacts missing contractDefinition', async () => {
const invalidArtifacts = {
contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0',
privateStateId: 'my-unique-state-id',
};

await expect(validateAndConvertMidnightArtifacts(invalidArtifacts)).rejects.toThrow(
'Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractDefinition properties.'
'Invalid contract artifacts provided. Expected an object with contractAddress'
);
});

it('should throw error for artifacts with non-string contractAddress', async () => {
const invalidArtifacts = {
contractAddress: 123,
privateStateId: 'my-unique-state-id',
contractDefinition: 'export interface MyContract { test(): Promise<void>; }',
};

await expect(validateAndConvertMidnightArtifacts(invalidArtifacts)).rejects.toThrow(
'Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractDefinition properties.'
'Invalid contract artifacts provided. Expected an object with contractAddress'
);
});

it('should throw error for artifacts with non-string privateStateId', async () => {
const invalidArtifacts = {
it('should accept artifacts with privateStateId when provided', async () => {
const artifactsWithPrivateStateId = {
contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0',
privateStateId: 123,
privateStateId: 'custom-state-id',
contractDefinition: 'export interface MyContract { test(): Promise<void>; }',
};

await expect(validateAndConvertMidnightArtifacts(invalidArtifacts)).rejects.toThrow(
'Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractDefinition properties.'
);
const result = await validateAndConvertMidnightArtifacts(artifactsWithPrivateStateId);

expect(result.privateStateId).toBe('custom-state-id');
});

it('should throw error for artifacts with non-string contractDefinition', async () => {
const invalidArtifacts = {
contractAddress: 'ct1q8ej4px2k3z9x5y6w7v8u9t0r1s2q3p4o5n6m7l8k9j0h1g2f3e4d5c6b7a8z9x0',
privateStateId: 'my-unique-state-id',
contractDefinition: 123,
};

await expect(validateAndConvertMidnightArtifacts(invalidArtifacts)).rejects.toThrow(
'Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractDefinition properties.'
'Invalid contract artifacts provided. Expected an object with contractAddress'
);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Unit tests for Private State ID generation utility
*/
import { describe, expect, it } from 'vitest';

import { generatePrivateStateId } from '../private-state-id';

describe('generatePrivateStateId', () => {
const contractAddress = '0200326c95873182775840764ae28e8750f73a68f236800171ebd92520e96a9fffb6';
const walletAddress =
'addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp';

it('should generate a deterministic ID from contract and wallet addresses', () => {
const id1 = generatePrivateStateId(contractAddress, walletAddress);
const id2 = generatePrivateStateId(contractAddress, walletAddress);

expect(id1).toBe(id2);
});

it('should generate ID with correct prefix', () => {
const id = generatePrivateStateId(contractAddress, walletAddress);

expect(id.startsWith('ps_')).toBe(true);
});

it('should generate different IDs for different contract addresses', () => {
const id1 = generatePrivateStateId(contractAddress, walletAddress);
const id2 = generatePrivateStateId(
'0200aaaabbbbccccdddd0000111122223333444455556666777788889999aaaabbbbcc',
walletAddress
);

expect(id1).not.toBe(id2);
});

it('should generate different IDs for different wallet addresses', () => {
const id1 = generatePrivateStateId(contractAddress, walletAddress);
const id2 = generatePrivateStateId(contractAddress, 'addr_test1different_wallet_address_here');

expect(id1).not.toBe(id2);
});

it('should handle case-insensitive contract addresses', () => {
const id1 = generatePrivateStateId(contractAddress.toLowerCase(), walletAddress);
const id2 = generatePrivateStateId(contractAddress.toUpperCase(), walletAddress);

expect(id1).toBe(id2);
});

it('should handle whitespace in addresses', () => {
const id1 = generatePrivateStateId(contractAddress, walletAddress);
const id2 = generatePrivateStateId(` ${contractAddress} `, ` ${walletAddress} `);

expect(id1).toBe(id2);
});

it('should generate non-empty ID', () => {
const id = generatePrivateStateId(contractAddress, walletAddress);

expect(id.length).toBeGreaterThan(3); // 'ps_' + at least 1 char
});
});
Loading