From 1b1449e5a61378bb283641408c15eeec7a7ff1b7 Mon Sep 17 00:00:00 2001 From: yug49 <148035793+yug49@users.noreply.github.com> Date: Mon, 8 Dec 2025 20:35:10 +0530 Subject: [PATCH] feat(adapter-midnight): auto-generate private state id for improved ux --- .../heavy-artifact-integration.test.ts | 1 - packages/adapter-midnight/src/adapter.ts | 10 --- .../src/contract/__tests__/loader.test.ts | 2 - .../adapter-midnight/src/export/bootstrap.ts | 9 +-- .../adapter-midnight/src/transaction/eoa.ts | 8 ++- .../src/transaction/execution-strategy.ts | 3 +- .../src/transaction/sender.ts | 5 +- .../src/types/__tests__/artifacts.test.ts | 15 ++--- .../adapter-midnight/src/types/artifacts.ts | 11 +++- .../src/utils/__tests__/artifacts.test.ts | 36 +++++------ .../utils/__tests__/private-state-id.test.ts | 62 +++++++++++++++++++ .../src/utils/artifact-preparation.ts | 1 - .../adapter-midnight/src/utils/artifacts.ts | 25 ++++---- packages/adapter-midnight/src/utils/index.ts | 1 + .../src/utils/private-state-id.ts | 49 +++++++++++++++ 15 files changed, 169 insertions(+), 69 deletions(-) create mode 100644 packages/adapter-midnight/src/utils/__tests__/private-state-id.test.ts create mode 100644 packages/adapter-midnight/src/utils/private-state-id.ts diff --git a/packages/adapter-midnight/src/__tests__/heavy-artifact-integration.test.ts b/packages/adapter-midnight/src/__tests__/heavy-artifact-integration.test.ts index c75918db..6832d9b1 100644 --- a/packages/adapter-midnight/src/__tests__/heavy-artifact-integration.test.ts +++ b/packages/adapter-midnight/src/__tests__/heavy-artifact-integration.test.ts @@ -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'); }); diff --git a/packages/adapter-midnight/src/adapter.ts b/packages/adapter-midnight/src/adapter.ts index c1f32e75..8577a2ec 100644 --- a/packages/adapter-midnight/src/adapter.ts +++ b/packages/adapter-midnight/src/adapter.ts @@ -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', diff --git a/packages/adapter-midnight/src/contract/__tests__/loader.test.ts b/packages/adapter-midnight/src/contract/__tests__/loader.test.ts index 2156cd03..d72e978d 100644 --- a/packages/adapter-midnight/src/contract/__tests__/loader.test.ts +++ b/packages/adapter-midnight/src/contract/__tests__/loader.test.ts @@ -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 = {}', @@ -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: '', diff --git a/packages/adapter-midnight/src/export/bootstrap.ts b/packages/adapter-midnight/src/export/bootstrap.ts index fca95539..6576f9cf 100644 --- a/packages/adapter-midnight/src/export/bootstrap.ts +++ b/packages/adapter-midnight/src/export/bootstrap.ts @@ -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 */ @@ -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; } @@ -48,7 +50,6 @@ export async function getMidnightExportBootstrapFiles( const artifactsFileContent = ` export const midnightArtifactsSource = { contractAddress: '${contractAddress}', - privateStateId: '${privateStateId}', contractArtifactsUrl: '/midnight/contract.zip', }; `; diff --git a/packages/adapter-midnight/src/transaction/eoa.ts b/packages/adapter-midnight/src/transaction/eoa.ts index 907214c1..abfeaeda 100644 --- a/packages/adapter-midnight/src/transaction/eoa.ts +++ b/packages/adapter-midnight/src/transaction/eoa.ts @@ -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'; @@ -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 || ''); diff --git a/packages/adapter-midnight/src/transaction/execution-strategy.ts b/packages/adapter-midnight/src/transaction/execution-strategy.ts index 3c484652..b1013a79 100644 --- a/packages/adapter-midnight/src/transaction/execution-strategy.ts +++ b/packages/adapter-midnight/src/transaction/execution-strategy.ts @@ -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; diff --git a/packages/adapter-midnight/src/transaction/sender.ts b/packages/adapter-midnight/src/transaction/sender.ts index c4f0e42f..f251f429 100644 --- a/packages/adapter-midnight/src/transaction/sender.ts +++ b/packages/adapter-midnight/src/transaction/sender.ts @@ -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; } diff --git a/packages/adapter-midnight/src/types/__tests__/artifacts.test.ts b/packages/adapter-midnight/src/types/__tests__/artifacts.test.ts index 4fdede18..882d221e 100644 --- a/packages/adapter-midnight/src/types/__tests__/artifacts.test.ts +++ b/packages/adapter-midnight/src/types/__tests__/artifacts.test.ts @@ -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; }', }; @@ -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; }', contractModule: 'module.exports = {};', witnessCode: 'export const witnesses = {};', @@ -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; }', }; @@ -61,7 +59,7 @@ 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; }', @@ -69,13 +67,12 @@ describe('Midnight Contract Artifacts', () => { 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); @@ -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; }', }; @@ -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, @@ -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, }; @@ -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; }', extraProperty: 'should be ignored', anotherExtra: 42, @@ -136,7 +130,6 @@ describe('Midnight Contract Artifacts', () => { it('should handle empty string values', () => { const artifacts = { contractAddress: '', - privateStateId: '', contractDefinition: '', }; diff --git a/packages/adapter-midnight/src/types/artifacts.ts b/packages/adapter-midnight/src/types/artifacts.ts index 12114e5c..b31a6e01 100644 --- a/packages/adapter-midnight/src/types/artifacts.ts +++ b/packages/adapter-midnight/src/types/artifacts.ts @@ -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). @@ -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' ); } diff --git a/packages/adapter-midnight/src/utils/__tests__/artifacts.test.ts b/packages/adapter-midnight/src/utils/__tests__/artifacts.test.ts index 84f045e1..e3683d49 100644 --- a/packages/adapter-midnight/src/utils/__tests__/artifacts.test.ts +++ b/packages/adapter-midnight/src/utils/__tests__/artifacts.test.ts @@ -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; }', }; @@ -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; }', }; 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; }', }; - 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; }', }; 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; }', }; - 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' ); }); diff --git a/packages/adapter-midnight/src/utils/__tests__/private-state-id.test.ts b/packages/adapter-midnight/src/utils/__tests__/private-state-id.test.ts new file mode 100644 index 00000000..4e1fe63b --- /dev/null +++ b/packages/adapter-midnight/src/utils/__tests__/private-state-id.test.ts @@ -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 + }); +}); diff --git a/packages/adapter-midnight/src/utils/artifact-preparation.ts b/packages/adapter-midnight/src/utils/artifact-preparation.ts index d27bf370..5167af35 100644 --- a/packages/adapter-midnight/src/utils/artifact-preparation.ts +++ b/packages/adapter-midnight/src/utils/artifact-preparation.ts @@ -62,7 +62,6 @@ export async function prepareArtifactsForFunction( }, bootstrapSource: { contractAddress: currentArtifacts.contractAddress, - privateStateId: currentArtifacts.privateStateId, identitySecretKeyPropertyName: currentArtifacts.identitySecretKeyPropertyName, contractArtifactsUrl: '/midnight/contract.zip', }, diff --git a/packages/adapter-midnight/src/utils/artifacts.ts b/packages/adapter-midnight/src/utils/artifacts.ts index d9832b81..8867e160 100644 --- a/packages/adapter-midnight/src/utils/artifacts.ts +++ b/packages/adapter-midnight/src/utils/artifacts.ts @@ -59,13 +59,13 @@ export async function validateAndConvertMidnightArtifacts( } const sourceRecord = source as Record; - if (!sourceRecord.contractAddress || !sourceRecord.privateStateId) { - throw new Error('Contract address and private state ID are required.'); + if (!sourceRecord.contractAddress) { + throw new Error('Contract address is required.'); } const artifacts: MidnightContractArtifacts = { contractAddress: sourceRecord.contractAddress as string, - privateStateId: sourceRecord.privateStateId as string, + privateStateId: sourceRecord.privateStateId as string | undefined, contractDefinition: extractedArtifacts.contractDefinition, contractModule: extractedArtifacts.contractModule, witnessCode: extractedArtifacts.witnessCode, @@ -108,13 +108,13 @@ export async function validateAndConvertMidnightArtifacts( } const sourceRecord = source as Record; - if (!sourceRecord.contractAddress || !sourceRecord.privateStateId) { - throw new Error('Contract address and private state ID are required.'); + if (!sourceRecord.contractAddress) { + throw new Error('Contract address is required.'); } const artifacts: MidnightContractArtifacts = { contractAddress: sourceRecord.contractAddress as string, - privateStateId: sourceRecord.privateStateId as string, + privateStateId: sourceRecord.privateStateId as string | undefined, contractDefinition: extractedArtifacts.contractDefinition, contractModule: extractedArtifacts.contractModule, witnessCode: extractedArtifacts.witnessCode, @@ -171,15 +171,16 @@ export async function validateAndConvertMidnightArtifacts( ); } - // Validate required form fields - if (!source.contractAddress || !source.privateStateId) { - throw new Error('Contract address and private state ID are required.'); + // Validate required form fields (privateStateId is auto-generated at transaction time) + if (!source.contractAddress) { + throw new Error('Contract address is required.'); } - // Combine with address and privateStateId (all fields are now validated) + // Combine with address (privateStateId will be auto-generated at transaction time) const artifacts: MidnightContractArtifacts = { contractAddress: source.contractAddress as string, - privateStateId: source.privateStateId as string, + // privateStateId is optional - auto-generated at transaction time from contract + wallet address + privateStateId: source.privateStateId as string | undefined, contractDefinition: extractedArtifacts.contractDefinition, contractModule: extractedArtifacts.contractModule, witnessCode: extractedArtifacts.witnessCode, @@ -195,7 +196,7 @@ export async function validateAndConvertMidnightArtifacts( // Legacy path: direct artifacts object if (!isMidnightContractArtifacts(source)) { throw new Error( - 'Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractDefinition properties.' + 'Invalid contract artifacts provided. Expected an object with contractAddress and contractDefinition properties.' ); } diff --git a/packages/adapter-midnight/src/utils/index.ts b/packages/adapter-midnight/src/utils/index.ts index 58dd0f37..4fe7ab7e 100644 --- a/packages/adapter-midnight/src/utils/index.ts +++ b/packages/adapter-midnight/src/utils/index.ts @@ -1,6 +1,7 @@ // Barrel file for utils module export * from './artifacts'; export * from './circuit-type-utils'; +export * from './private-state-id'; export * from './schema-parser'; export * from './zip-extractor'; export { getNetworkId, getNumericNetworkId } from './network-id'; diff --git a/packages/adapter-midnight/src/utils/private-state-id.ts b/packages/adapter-midnight/src/utils/private-state-id.ts new file mode 100644 index 00000000..350f88eb --- /dev/null +++ b/packages/adapter-midnight/src/utils/private-state-id.ts @@ -0,0 +1,49 @@ +/** + * Utility for generating deterministic Private State IDs + * + * Private State ID is an implementation detail used by the Midnight SDK to manage + * user's encrypted state data in browser storage. Instead of requiring manual input + * from users, we generate it deterministically based on contract address and wallet + * address, ensuring: + * + * 1. Same user + same contract = same private state (automatic state restoration) + * 2. Different users get isolated private state (user privacy) + * 3. No manual input needed (reduced friction and errors) + */ + +import { simpleHash } from '@openzeppelin/ui-builder-utils'; + +/** + * Generates a deterministic Private State ID from contract address and wallet address. + * + * The generated ID is: + * - Deterministic: Same inputs always produce the same output + * - Unique: Different contract/wallet combinations produce different IDs + * - Human-readable prefix: Helps with debugging and transparency + * + * @param contractAddress - The deployed Midnight contract address + * @param walletAddress - The connected wallet's address + * @returns A deterministic private state ID string + * + * @example + * ```typescript + * const privateStateId = generatePrivateStateId( + * '0200326c95873182775840764ae28e8750f73a68f236800171ebd92520e96a9fffb6', + * 'addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp' + * ); + * // Result: 'ps_a1b2c3d4' (deterministic hash-based ID) + * ``` + */ +export function generatePrivateStateId(contractAddress: string, walletAddress: string): string { + // Normalize inputs to ensure consistency + const normalizedContract = contractAddress.toLowerCase().trim(); + const normalizedWallet = walletAddress.toLowerCase().trim(); + + // Create a combined string for hashing + const combined = `${normalizedContract}:${normalizedWallet}`; + + // Generate hash and prefix with 'ps_' for clarity + const hash = simpleHash(combined); + + return `ps_${hash}`; +}