From 174cf21f7618a1b5d7f6d639a93d8cf54e244ce5 Mon Sep 17 00:00:00 2001 From: Adarsh Ron Date: Wed, 15 Oct 2025 02:36:59 +0530 Subject: [PATCH 01/18] feat: multiuser --- bin/index.mjs | 5 + src/boilerplate/common/api.mjs | 3 + src/boilerplate/common/commitment-storage.mjs | 61 ++- src/boilerplate/common/config/default.js | 1 + src/boilerplate/common/contract.mjs | 70 ++-- .../common/encrypted-data-listener.mjs | 31 +- .../key-management/DatabaseKeyStorage.mjs | 368 ++++++++++++++++++ .../common/key-management/FileKeyStorage.mjs | 259 ++++++++++++ .../common/key-management/IKeyStorage.mjs | 110 ++++++ .../common/key-management/KeyManager.mjs | 212 ++++++++++ .../common/key-management/encryption.mjs | 208 ++++++++++ .../common/key-management/index.mjs | 79 ++++ .../common/middleware/saas-context.mjs | 194 +++++++++ .../common/services/generic-api_services.mjs | 3 +- .../generic-read-only-api_services.mjs | 3 +- .../services/genericpublic-api_services.mjs | 3 +- .../javascript/nodes/boilerplate-generator.ts | 2 + .../javascript/raw/boilerplate-generator.ts | 82 +++- .../javascript/raw/toOrchestration.ts | 6 +- src/codeGenerators/common.ts | 13 +- .../orchestration/files/toOrchestration.ts | 50 ++- src/transformers/toOrchestration.ts | 29 +- .../visitors/toOrchestrationVisitor.ts | 1 + 23 files changed, 1674 insertions(+), 119 deletions(-) create mode 100644 src/boilerplate/common/key-management/DatabaseKeyStorage.mjs create mode 100644 src/boilerplate/common/key-management/FileKeyStorage.mjs create mode 100644 src/boilerplate/common/key-management/IKeyStorage.mjs create mode 100644 src/boilerplate/common/key-management/KeyManager.mjs create mode 100644 src/boilerplate/common/key-management/encryption.mjs create mode 100644 src/boilerplate/common/key-management/index.mjs create mode 100644 src/boilerplate/common/middleware/saas-context.mjs diff --git a/bin/index.mjs b/bin/index.mjs index a1a035b8..760ed43e 100755 --- a/bin/index.mjs +++ b/bin/index.mjs @@ -43,6 +43,10 @@ program .option( '-m, --modify ', 'Ovewrite the file from truezapps folder', + ) + .option( + '--multi-tenant', + 'Enable multi-user mode for zapps' ); program.parse(process.argv); @@ -73,6 +77,7 @@ const options = { contractsDirPath, orchestrationDirPath, modifyAST, + multiTenant: opts.multiTenant || false, }; const validateOptions = ({ diff --git a/src/boilerplate/common/api.mjs b/src/boilerplate/common/api.mjs index 4ed05160..e9af4ddf 100644 --- a/src/boilerplate/common/api.mjs +++ b/src/boilerplate/common/api.mjs @@ -4,6 +4,7 @@ import { ServiceManager } from './api_services.mjs'; import { Router } from './api_routes.mjs'; import Web3 from './common/web3.mjs'; ENCRYPTEDLISTENER_IMPORT +SAAS_MIDDLEWARE_IMPORT function gracefulshutdown() { console.log('Shutting down'); @@ -19,6 +20,8 @@ process.on('SIGINT', gracefulshutdown); const app = express(); app.use(express.json()); +SAAS_MIDDLEWARE_USAGE + const web3 = Web3.connection(); const serviceMgr = new ServiceManager(web3); serviceMgr.init().then(async () => { diff --git a/src/boilerplate/common/commitment-storage.mjs b/src/boilerplate/common/commitment-storage.mjs index da54e583..6a337478 100644 --- a/src/boilerplate/common/commitment-storage.mjs +++ b/src/boilerplate/common/commitment-storage.mjs @@ -10,16 +10,12 @@ import mongo from './mongo.mjs'; import logger from './logger.mjs'; import utils from 'zkp-utils'; import { poseidonHash } from './number-theory.mjs'; -import { sharedSecretKey } from './number-theory.mjs'; import { generateProof } from './zokrates.mjs'; -import { hlt } from './hash-lookup.mjs'; -import { registerKey } from './contract.mjs'; +import { KeyManager } from './key-management/KeyManager.mjs'; const { MONGO_URL, COMMITMENTS_DB, COMMITMENTS_COLLECTION } = config; const { generalise } = gen; -const keyDb = '/app/orchestration/common/db/key.json'; - export function formatCommitment (commitment) { let data try { @@ -671,39 +667,30 @@ export async function splitCommitments( export async function getSharedSecretskeys( _recipientAddress, _recipientPublicKey = 0, + context, ) { - if (!fs.existsSync(keyDb)) - await registerKey(utils.randomHex(31), null, false); - const keys = JSON.parse( - fs.readFileSync(keyDb, 'utf-8', err => { - console.log(err); - }), - ); - const secretKey = generalise(keys.secretKey); - const publicKey = generalise(keys.publicKey); - let recipientPublicKey = generalise(_recipientPublicKey); - const recipientAddress = generalise(_recipientAddress); - if (_recipientPublicKey === 0) { - recipientPublicKey = await this.instance.methods - .zkpPublicKeys(recipientAddress.hex(20)) - .call(); - recipientPublicKey = generalise(recipientPublicKey); - - if (recipientPublicKey.length === 0) { - throw new Error('WARNING: Public key for given eth address not found.'); - } - } + try { + // Use KeyManager for shared secret key management + const keyManager = KeyManager.getInstance(); + + logger.debug('Getting shared secret keys via KeyManager', { + recipientAddress: _recipientAddress, + multiTenant: !!context?.accountId + }); + + const sharedPublicKey = await keyManager.getSharedSecretKeys( + _recipientAddress, + _recipientPublicKey, + context + ); - const sharedKey = sharedSecretKey(secretKey, recipientPublicKey); - console.log('sharedKey:', sharedKey); - console.log('sharedKey:', sharedKey[1]); - const keyJson = { - secretKey: secretKey.integer, - publicKey: publicKey.integer, - sharedSecretKey: sharedKey[0].integer, - sharedPublicKey: sharedKey[1].integer, // not req - }; - fs.writeFileSync(keyDb, JSON.stringify(keyJson, null, 4)); + logger.info('Shared secret keys retrieved successfully', { + multiTenant: !!context?.accountId + }); - return sharedKey[1]; + return sharedPublicKey; + } catch (error) { + logger.error('Failed to get shared secret keys:', error); + throw error; + } } diff --git a/src/boilerplate/common/config/default.js b/src/boilerplate/common/config/default.js index a483d2fd..eb9732d0 100644 --- a/src/boilerplate/common/config/default.js +++ b/src/boilerplate/common/config/default.js @@ -1,5 +1,6 @@ module.exports = { log_level: 'info', + multiTenant: MULTI_TENANT_MODE, zokrates: { url: process.env.ZOKRATES_URL || 'http://zokrates:80', }, diff --git a/src/boilerplate/common/contract.mjs b/src/boilerplate/common/contract.mjs index 787e06e5..749375c1 100644 --- a/src/boilerplate/common/contract.mjs +++ b/src/boilerplate/common/contract.mjs @@ -4,16 +4,10 @@ import GN from 'general-number'; import utils from 'zkp-utils'; import Web3 from './web3.mjs'; import logger from './logger.mjs'; - -import { - scalarMult, - compressStarlightKey, - poseidonHash, -} from './number-theory.mjs'; +import { KeyManager } from './key-management/KeyManager.mjs'; const web3 = Web3.connection(); const { generalise } = GN; -const keyDb = '/app/orchestration/common/db/key.json'; export const contractPath = (contractName) => { return `/app/build/contracts/${contractName}.json`; @@ -118,41 +112,35 @@ export async function registerKey( _secretKey, contractName, registerWithContract, + context, ) { - let secretKey = generalise(_secretKey); - let publicKeyPoint = generalise( - scalarMult(secretKey.hex(32), config.BABYJUBJUB.GENERATOR), - ); - let publicKey = compressStarlightKey(publicKeyPoint); - while (publicKey === null) { - logger.warn(`your secret key created a large public key - resetting`); - secretKey = generalise(utils.randomHex(31)); - publicKeyPoint = generalise( - scalarMult(secretKey.hex(32), config.BABYJUBJUB.GENERATOR), + try { + // Use KeyManager for key registration + const keyManager = KeyManager.getInstance(); + + logger.debug('Registering key via KeyManager', { + contractName, + registerWithContract, + multiTenant: !!context?.accountId + }); + + const publicKeyInteger = await keyManager.registerKey( + _secretKey, + contractName, + registerWithContract, + context ); - publicKey = compressStarlightKey(publicKeyPoint); - } - if (registerWithContract) { - const instance = await getContractInstance(contractName); - const contractAddr = await getContractAddress(contractName); - const txData = await instance.methods.registerZKPPublicKey(publicKey.integer).encodeABI(); - let txParams = { - from: config.web3.options.defaultAccount, - to: contractAddr, - gas: config.web3.options.defaultGas, - gasPrice: config.web3.options.defaultGasPrice, - data: txData, - chainId: await web3.eth.net.getId(), - }; - const key = config.web3.key; - const signed = await web3.eth.accounts.signTransaction(txParams, key); - const sendTxn = await web3.eth.sendSignedTransaction(signed.rawTransaction); - } - const keyJson = { - secretKey: secretKey.integer, - publicKey: publicKey.integer, // not req - }; - fs.writeFileSync(keyDb, JSON.stringify(keyJson, null, 4)); + + const publicKey = generalise(publicKeyInteger); + + logger.info('Key registered successfully', { + publicKey: publicKey.integer, + multiTenant: !!context?.accountId + }); - return publicKey; + return publicKey; + } catch (error) { + logger.error('Failed to register key:', error); + throw error; + } } \ No newline at end of file diff --git a/src/boilerplate/common/encrypted-data-listener.mjs b/src/boilerplate/common/encrypted-data-listener.mjs index c8f89b52..ad30e7ae 100644 --- a/src/boilerplate/common/encrypted-data-listener.mjs +++ b/src/boilerplate/common/encrypted-data-listener.mjs @@ -5,8 +5,7 @@ import { generalise } from 'general-number'; import { getContractAddress, getContractInstance, registerKey } from './common/contract.mjs'; import { storeCommitment, formatCommitment, persistCommitment } from './common/commitment-storage.mjs'; import { decrypt, poseidonHash, } from './common/number-theory.mjs'; - -const keyDb = '/app/orchestration/common/db/key.json'; +import { KeyManager } from './key-management/KeyManager.mjs'; function decodeCommitmentData(decrypted){ @@ -19,10 +18,11 @@ function decodeCommitmentData(decrypted){ } export default class EncryptedDataEventListener { - constructor(web3) { + constructor(web3, context) { this.web3 = web3; this.ethAddress = generalise(config.web3.options.defaultAccount); this.contractMetadata = {}; + this.context = context; } async init() { @@ -36,12 +36,29 @@ export default class EncryptedDataEventListener { contractAddr, ); - if (!fs.existsSync(keyDb)) await registerKey(utils.randomHex(31), 'CONTRACT_NAME', true); + // Use KeyManager for key retrieval + const keyManager = KeyManager.getInstance(); + + // Check if keys exist, if not register new ones + const hasKeys = await keyManager.hasKeys(this.context); + if (!hasKeys) { + console.log('No keys found, registering new key pair...'); + await registerKey(utils.randomHex(31), 'CONTRACT_NAME', true, this.context); + } - const { secretKey, publicKey } = JSON.parse(fs.readFileSync(keyDb)); + // Retrieve keys via KeyManager + const keys = await keyManager.getKeys(this.context); - this.secretKey = generalise(secretKey); - this.publicKey = generalise(publicKey); + if (!keys) { + throw new Error('Failed to retrieve keys after registration'); + } + + this.secretKey = generalise(keys.secretKey); + this.publicKey = generalise(keys.publicKey); + + console.log('Keys loaded successfully', { + multiTenant: !!this.context?.accountId + }); } catch (error) { console.error( 'encrypted-data-listener', diff --git a/src/boilerplate/common/key-management/DatabaseKeyStorage.mjs b/src/boilerplate/common/key-management/DatabaseKeyStorage.mjs new file mode 100644 index 00000000..d297710f --- /dev/null +++ b/src/boilerplate/common/key-management/DatabaseKeyStorage.mjs @@ -0,0 +1,368 @@ +/** + * @file DatabaseKeyStorage.mjs + * @description MongoDB-based key storage implementation for multi-tenant deployments. + * Provides complete isolation between users based on accountId. + */ + +import config from 'config'; +import GN from 'general-number'; +import utils from 'zkp-utils'; +import mongo from '../mongo.mjs'; +import logger from '../logger.mjs'; +import { IKeyStorage } from './IKeyStorage.mjs'; +import { encryptIfEnabled, decryptIfEncrypted } from './encryption.mjs'; +import { + scalarMult, + compressStarlightKey, + sharedSecretKey, +} from '../number-theory.mjs'; + +const { generalise } = GN; + +// Configuration +const MONGO_URL = process.env.MONGO_URL || config.MONGO_URL || 'mongodb://localhost:27017'; +const KEYS_DB = process.env.KEYS_DB || config.KEYS_DB || config.COMMITMENTS_DB || 'starlight_db'; +const USER_KEYS_COLLECTION = 'user_keys'; + +/** + * Database-based key storage implementation. + * Stores keys in MongoDB with complete isolation between users. + * Supports encryption at rest for sensitive key material. + * + * @extends IKeyStorage + */ +export class DatabaseKeyStorage extends IKeyStorage { + constructor() { + super(); + this.mongoUrl = MONGO_URL; + this.dbName = KEYS_DB; + this.collectionName = USER_KEYS_COLLECTION; + } + + /** + * Get MongoDB collection instance. + * + * @returns {Promise} + * @private + */ + async getCollection() { + const connection = await mongo.connection(this.mongoUrl); + const db = connection.db(this.dbName); + return db.collection(this.collectionName); + } + + /** + * Validate that context is provided and contains accountId. + * + * @param {import('./IKeyStorage.mjs').SaaSContext} context + * @throws {Error} If context is missing or invalid + * @private + */ + validateContext(context) { + if (!context || !context.accountId) { + throw new Error( + 'DatabaseKeyStorage requires a valid SaaS context with accountId. ' + + 'Ensure x-saas-context header is present in the request.' + ); + } + + // Validate accountId format (alphanumeric, hyphens, underscores only) + if (!/^[a-zA-Z0-9_-]+$/.test(context.accountId)) { + throw new Error('Invalid accountId format. Must contain only alphanumeric characters, hyphens, and underscores.'); + } + } + + /** + * Retrieve keys for a user from the database. + * + * @param {import('./IKeyStorage.mjs').SaaSContext} context - Required SaaS context + * @returns {Promise} User keys or null if not found + */ + async getKeys(context) { + this.validateContext(context); + + try { + const collection = await this.getCollection(); + const doc = await collection.findOne({ accountId: context.accountId }); + + if (!doc) { + logger.debug(`No keys found for accountId: ${context.accountId}`); + return null; + } + + // Update lastUsed timestamp + await collection.updateOne( + { accountId: context.accountId }, + { + $set: { + 'metadata.lastUsed': new Date(), + updatedAt: new Date() + } + } + ); + + // Decrypt sensitive keys + const keys = { + secretKey: decryptIfEncrypted(doc.secretKey), + publicKey: doc.publicKey, // Public key doesn't need decryption + }; + + if (doc.sharedSecretKey) { + keys.sharedSecretKey = decryptIfEncrypted(doc.sharedSecretKey); + } + if (doc.sharedPublicKey) { + keys.sharedPublicKey = doc.sharedPublicKey; + } + + logger.debug(`Keys retrieved for accountId: ${context.accountId}`); + return keys; + } catch (error) { + logger.error(`Error retrieving keys for accountId ${context.accountId}:`, error); + throw new Error(`Failed to retrieve keys: ${error.message}`); + } + } + + /** + * Save keys for a user to the database. + * + * @param {import('./IKeyStorage.mjs').UserKeys} keys - User keys to save + * @param {import('./IKeyStorage.mjs').SaaSContext} context - Required SaaS context + * @returns {Promise} + */ + async saveKeys(keys, context) { + this.validateContext(context); + + try { + const collection = await this.getCollection(); + const now = new Date(); + + // Encrypt sensitive keys + const doc = { + accountId: context.accountId, + secretKey: encryptIfEnabled(keys.secretKey), + publicKey: keys.publicKey, // Public key doesn't need encryption + updatedAt: now, + }; + + // Include optional shared keys if present + if (keys.sharedSecretKey) { + doc.sharedSecretKey = encryptIfEnabled(keys.sharedSecretKey); + } + if (keys.sharedPublicKey) { + doc.sharedPublicKey = keys.sharedPublicKey; + } + + // Upsert: update if exists, insert if not + const result = await collection.updateOne( + { accountId: context.accountId }, + { + $set: doc, + $setOnInsert: { + createdAt: now, + metadata: { + keyVersion: 1, + registeredOnChain: false, + } + } + }, + { upsert: true } + ); + + if (result.upsertedCount > 0) { + logger.info(`Keys created for accountId: ${context.accountId}`); + } else { + logger.debug(`Keys updated for accountId: ${context.accountId}`); + } + } catch (error) { + logger.error(`Error saving keys for accountId ${context.accountId}:`, error); + throw new Error(`Failed to save keys: ${error.message}`); + } + } + + /** + * Register a new key pair. + * + * @param {string} _secretKey - Secret key to register (hex string) + * @param {string} contractName - Associated contract name + * @param {boolean} registerWithContract - Whether to register the key on-chain + * @param {import('./IKeyStorage.mjs').SaaSContext} context - Required SaaS context + * @returns {Promise} Public key (as integer string) + */ + async registerKey(_secretKey, contractName, registerWithContract, context) { + this.validateContext(context); + + try { + let secretKey = generalise(_secretKey); + let publicKeyPoint = generalise( + scalarMult(secretKey.hex(32), config.BABYJUBJUB.GENERATOR) + ); + let publicKey = compressStarlightKey(publicKeyPoint); + + // Regenerate if public key is too large + while (publicKey === null) { + logger.warn('Secret key created a large public key - regenerating'); + secretKey = generalise(utils.randomHex(31)); + publicKeyPoint = generalise( + scalarMult(secretKey.hex(32), config.BABYJUBJUB.GENERATOR) + ); + publicKey = compressStarlightKey(publicKeyPoint); + } + + // Register on-chain if requested + if (registerWithContract) { + const { getContractInstance, getContractAddress } = await import('../contract.mjs'); + const Web3 = await import('../web3.mjs'); + const web3 = Web3.default.connection(); + + const instance = await getContractInstance(contractName); + const contractAddr = await getContractAddress(contractName); + const txData = await instance.methods + .registerZKPPublicKey(publicKey.integer) + .encodeABI(); + + const txParams = { + from: config.web3.options.defaultAccount, + to: contractAddr, + gas: config.web3.options.defaultGas, + gasPrice: config.web3.options.defaultGasPrice, + data: txData, + chainId: await web3.eth.net.getId(), + }; + + const key = config.web3.key; + const signed = await web3.eth.accounts.signTransaction(txParams, key); + await web3.eth.sendSignedTransaction(signed.rawTransaction); + logger.info(`Key registered on-chain for accountId: ${context.accountId}`); + } + + // Save keys to database with metadata + await this.saveKeys({ + secretKey: secretKey.integer, + publicKey: publicKey.integer, + }, context); + + // Update metadata + const collection = await this.getCollection(); + await collection.updateOne( + { accountId: context.accountId }, + { + $set: { + 'metadata.contractName': contractName, + 'metadata.registeredOnChain': registerWithContract, + } + } + ); + + logger.info(`Key registered successfully for accountId: ${context.accountId}`); + return publicKey.integer; + } catch (error) { + logger.error(`Error registering key for accountId ${context.accountId}:`, error); + throw new Error(`Failed to register key: ${error.message}`); + } + } + + /** + * Get or create shared secret keys for encrypted communication. + * + * @param {string} _recipientAddress - Recipient's Ethereum address + * @param {string|number} _recipientPublicKey - Recipient's public key (0 to fetch from contract) + * @param {import('./IKeyStorage.mjs').SaaSContext} context - Required SaaS context + * @returns {Promise} Shared public key + */ + async getSharedSecretKeys(_recipientAddress, _recipientPublicKey = 0, context) { + this.validateContext(context); + + try { + // Ensure keys exist + let keys = await this.getKeys(context); + if (!keys) { + await this.registerKey(utils.randomHex(31), null, false, context); + keys = await this.getKeys(context); + } + + const secretKey = generalise(keys.secretKey); + const publicKey = generalise(keys.publicKey); + let recipientPublicKey = generalise(_recipientPublicKey); + const recipientAddress = generalise(_recipientAddress); + + // Fetch recipient's public key from contract if not provided + if (_recipientPublicKey === 0) { + const { getContractInstance } = await import('../contract.mjs'); + const instance = await getContractInstance('CONTRACT_NAME'); + + recipientPublicKey = await instance.methods + .zkpPublicKeys(recipientAddress.hex(20)) + .call(); + recipientPublicKey = generalise(recipientPublicKey); + + if (recipientPublicKey.length === 0) { + throw new Error('Public key for given eth address not found'); + } + } + + // Generate shared secret + const sharedKey = sharedSecretKey(secretKey, recipientPublicKey); + logger.debug(`Shared key generated for accountId: ${context.accountId}`); + + // Update keys with shared secret + await this.saveKeys({ + secretKey: secretKey.integer, + publicKey: publicKey.integer, + sharedSecretKey: sharedKey[0].integer, + sharedPublicKey: sharedKey[1].integer, + }, context); + + return sharedKey[1]; + } catch (error) { + logger.error(`Error getting shared secret keys for accountId ${context.accountId}:`, error); + throw new Error(`Failed to get shared secret keys: ${error.message}`); + } + } + + /** + * Check if keys exist for a user in the database. + * + * @param {import('./IKeyStorage.mjs').SaaSContext} context - Required SaaS context + * @returns {Promise} True if keys exist + */ + async hasKeys(context) { + this.validateContext(context); + + try { + const collection = await this.getCollection(); + const count = await collection.countDocuments({ accountId: context.accountId }); + return count > 0; + } catch (error) { + logger.error(`Error checking keys for accountId ${context.accountId}:`, error); + throw new Error(`Failed to check keys: ${error.message}`); + } + } + + /** + * Delete keys for a user from the database. + * + * @param {import('./IKeyStorage.mjs').SaaSContext} context - Required SaaS context + * @returns {Promise} True if keys were deleted, false if they didn't exist + */ + async deleteKeys(context) { + this.validateContext(context); + + try { + const collection = await this.getCollection(); + const result = await collection.deleteOne({ accountId: context.accountId }); + + if (result.deletedCount > 0) { + logger.info(`Keys deleted for accountId: ${context.accountId}`); + return true; + } + + logger.debug(`No keys found to delete for accountId: ${context.accountId}`); + return false; + } catch (error) { + logger.error(`Error deleting keys for accountId ${context.accountId}:`, error); + throw new Error(`Failed to delete keys: ${error.message}`); + } + } +} + +export default DatabaseKeyStorage; diff --git a/src/boilerplate/common/key-management/FileKeyStorage.mjs b/src/boilerplate/common/key-management/FileKeyStorage.mjs new file mode 100644 index 00000000..7be63bdc --- /dev/null +++ b/src/boilerplate/common/key-management/FileKeyStorage.mjs @@ -0,0 +1,259 @@ +/** + * @file FileKeyStorage.mjs + * @description File-based key storage implementation. + * This wraps the existing key.json file-based logic for backward compatibility. + */ + +import fs from 'fs'; +import config from 'config'; +import GN from 'general-number'; +import utils from 'zkp-utils'; +import logger from '../logger.mjs'; +import { IKeyStorage } from './IKeyStorage.mjs'; +import { + scalarMult, + compressStarlightKey, + sharedSecretKey, +} from '../number-theory.mjs'; + +const { generalise } = GN; +const keyDb = '/app/orchestration/common/db/key.json'; + +/** + * File-based key storage implementation. + * Stores keys in a single JSON file at /app/orchestration/common/db/key.json + * This is the legacy/default storage mechanism for single-tenant deployments. + * + * @extends IKeyStorage + */ +export class FileKeyStorage extends IKeyStorage { + constructor() { + super(); + this.keyFilePath = keyDb; + } + + /** + * Retrieve keys from the key.json file. + * + * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Ignored for file storage + * @returns {Promise} User keys or null if file doesn't exist + */ + async getKeys(context) { + try { + if (!fs.existsSync(this.keyFilePath)) { + logger.debug('Key file does not exist'); + return null; + } + + const keyData = fs.readFileSync(this.keyFilePath, 'utf-8'); + const keys = JSON.parse(keyData); + + logger.debug('Keys retrieved from file'); + return { + secretKey: keys.secretKey, + publicKey: keys.publicKey, + sharedSecretKey: keys.sharedSecretKey, + sharedPublicKey: keys.sharedPublicKey, + }; + } catch (error) { + logger.error('Error reading keys from file:', error); + throw new Error(`Failed to read keys from file: ${error.message}`); + } + } + + /** + * Save keys to the key.json file. + * + * @param {import('./IKeyStorage.mjs').UserKeys} keys - User keys to save + * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Ignored for file storage + * @returns {Promise} + */ + async saveKeys(keys, context) { + try { + // Ensure directory exists + const dir = '/app/orchestration/common/db'; + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + const keyJson = { + secretKey: keys.secretKey, + publicKey: keys.publicKey, + }; + + // Include optional shared keys if present + if (keys.sharedSecretKey) { + keyJson.sharedSecretKey = keys.sharedSecretKey; + } + if (keys.sharedPublicKey) { + keyJson.sharedPublicKey = keys.sharedPublicKey; + } + + fs.writeFileSync(this.keyFilePath, JSON.stringify(keyJson, null, 4)); + logger.debug('Keys saved to file'); + } catch (error) { + logger.error('Error saving keys to file:', error); + throw new Error(`Failed to save keys to file: ${error.message}`); + } + } + + /** + * Register a new key pair. + * This replicates the logic from contract.mjs registerKey() function. + * + * @param {string} _secretKey - Secret key to register (hex string) + * @param {string} contractName - Associated contract name + * @param {boolean} registerWithContract - Whether to register the key on-chain + * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Ignored for file storage + * @returns {Promise} Public key (as integer string) + */ + async registerKey(_secretKey, contractName, registerWithContract, context) { + try { + let secretKey = generalise(_secretKey); + let publicKeyPoint = generalise( + scalarMult(secretKey.hex(32), config.BABYJUBJUB.GENERATOR) + ); + let publicKey = compressStarlightKey(publicKeyPoint); + + // Regenerate if public key is too large + while (publicKey === null) { + logger.warn('Secret key created a large public key - regenerating'); + secretKey = generalise(utils.randomHex(31)); + publicKeyPoint = generalise( + scalarMult(secretKey.hex(32), config.BABYJUBJUB.GENERATOR) + ); + publicKey = compressStarlightKey(publicKeyPoint); + } + + // Register on-chain if requested + if (registerWithContract) { + // Import here to avoid circular dependency + const { getContractInstance, getContractAddress } = await import('../contract.mjs'); + const Web3 = await import('../web3.mjs'); + const web3 = Web3.default.connection(); + + const instance = await getContractInstance(contractName); + const contractAddr = await getContractAddress(contractName); + const txData = await instance.methods + .registerZKPPublicKey(publicKey.integer) + .encodeABI(); + + const txParams = { + from: config.web3.options.defaultAccount, + to: contractAddr, + gas: config.web3.options.defaultGas, + gasPrice: config.web3.options.defaultGasPrice, + data: txData, + chainId: await web3.eth.net.getId(), + }; + + const key = config.web3.key; + const signed = await web3.eth.accounts.signTransaction(txParams, key); + await web3.eth.sendSignedTransaction(signed.rawTransaction); + logger.info('Key registered on-chain'); + } + + // Save keys to file + await this.saveKeys({ + secretKey: secretKey.integer, + publicKey: publicKey.integer, + }); + + logger.info('Key registered successfully'); + return publicKey.integer; + } catch (error) { + logger.error('Error registering key:', error); + throw new Error(`Failed to register key: ${error.message}`); + } + } + + /** + * Get or create shared secret keys for encrypted communication. + * This replicates the logic from commitment-storage.mjs getSharedSecretskeys() function. + * + * @param {string} _recipientAddress - Recipient's Ethereum address + * @param {string|number} _recipientPublicKey - Recipient's public key (0 to fetch from contract) + * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Ignored for file storage + * @returns {Promise} Shared public key + */ + async getSharedSecretKeys(_recipientAddress, _recipientPublicKey = 0, context) { + try { + // Ensure keys exist + if (!fs.existsSync(this.keyFilePath)) { + await this.registerKey(utils.randomHex(31), null, false); + } + + const keys = await this.getKeys(); + const secretKey = generalise(keys.secretKey); + const publicKey = generalise(keys.publicKey); + let recipientPublicKey = generalise(_recipientPublicKey); + const recipientAddress = generalise(_recipientAddress); + + // Fetch recipient's public key from contract if not provided + if (_recipientPublicKey === 0) { + // Import here to avoid circular dependency + const { getContractInstance } = await import('../contract.mjs'); + const instance = await getContractInstance('CONTRACT_NAME'); + + recipientPublicKey = await instance.methods + .zkpPublicKeys(recipientAddress.hex(20)) + .call(); + recipientPublicKey = generalise(recipientPublicKey); + + if (recipientPublicKey.length === 0) { + throw new Error('Public key for given eth address not found'); + } + } + + // Generate shared secret + const sharedKey = sharedSecretKey(secretKey, recipientPublicKey); + logger.debug('Shared key generated:', sharedKey[1]); + + // Update keys with shared secret + await this.saveKeys({ + secretKey: secretKey.integer, + publicKey: publicKey.integer, + sharedSecretKey: sharedKey[0].integer, + sharedPublicKey: sharedKey[1].integer, + }); + + return sharedKey[1]; + } catch (error) { + logger.error('Error getting shared secret keys:', error); + throw new Error(`Failed to get shared secret keys: ${error.message}`); + } + } + + /** + * Check if keys exist in the file. + * + * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Ignored for file storage + * @returns {Promise} True if key file exists + */ + async hasKeys(context) { + return fs.existsSync(this.keyFilePath); + } + + /** + * Delete the key file. + * + * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Ignored for file storage + * @returns {Promise} True if file was deleted, false if it didn't exist + */ + async deleteKeys(context) { + try { + if (fs.existsSync(this.keyFilePath)) { + fs.unlinkSync(this.keyFilePath); + logger.info('Key file deleted'); + return true; + } + return false; + } catch (error) { + logger.error('Error deleting key file:', error); + throw new Error(`Failed to delete key file: ${error.message}`); + } + } +} + +export default FileKeyStorage; + diff --git a/src/boilerplate/common/key-management/IKeyStorage.mjs b/src/boilerplate/common/key-management/IKeyStorage.mjs new file mode 100644 index 00000000..c84510b0 --- /dev/null +++ b/src/boilerplate/common/key-management/IKeyStorage.mjs @@ -0,0 +1,110 @@ +/** + * @file IKeyStorage.mjs + * @description Interface definition for key storage implementations. + * This file defines the contract that both FileKeyStorage and DatabaseKeyStorage must implement. + */ + +/** + * @typedef {Object} SaaSContext + * @property {string} accountId - Unique identifier for the user/account in multi-tenant mode + */ + +/** + * @typedef {Object} UserKeys + * @property {string} secretKey - User's secret key (as integer string) + * @property {string} publicKey - User's public key (as integer string) + * @property {string} [sharedSecretKey] - Optional shared secret key for encrypted communication + * @property {string} [sharedPublicKey] - Optional shared public key + */ + +/** + * @typedef {Object} KeyMetadata + * @property {number} keyVersion - Version number for key rotation support + * @property {string} contractName - Associated contract name + * @property {boolean} registeredOnChain - Whether the key is registered on-chain + * @property {Date} [lastUsed] - Last time the key was accessed + */ + +/** + * Base class for key storage implementations. + * This class defines the interface that all key storage implementations must follow. + * + * @abstract + */ +export class IKeyStorage { + /** + * Retrieve keys for a user. + * + * @param {SaaSContext} [context] - Optional SaaS context for multi-tenant mode + * @returns {Promise} User keys or null if not found + * @abstract + */ + async getKeys(context) { + throw new Error('getKeys() must be implemented by subclass'); + } + + /** + * Save keys for a user. + * + * @param {UserKeys} keys - User keys to save + * @param {SaaSContext} [context] - Optional SaaS context for multi-tenant mode + * @returns {Promise} + * @abstract + */ + async saveKeys(keys, context) { + throw new Error('saveKeys() must be implemented by subclass'); + } + + /** + * Register a new key pair. + * Generates a public key from the secret key and optionally registers it on-chain. + * + * @param {string} secretKey - Secret key to register (hex string) + * @param {string} contractName - Associated contract name + * @param {boolean} registerWithContract - Whether to register the key on-chain + * @param {SaaSContext} [context] - Optional SaaS context for multi-tenant mode + * @returns {Promise} Public key (as integer string) + * @abstract + */ + async registerKey(secretKey, contractName, registerWithContract, context) { + throw new Error('registerKey() must be implemented by subclass'); + } + + /** + * Get or create shared secret keys for encrypted communication with another user. + * + * @param {string} recipientAddress - Recipient's Ethereum address + * @param {string|number} recipientPublicKey - Recipient's public key + * @param {SaaSContext} [context] - Optional SaaS context for multi-tenant mode + * @returns {Promise} Shared public key + * @abstract + */ + async getSharedSecretKeys(recipientAddress, recipientPublicKey, context) { + throw new Error('getSharedSecretKeys() must be implemented by subclass'); + } + + /** + * Check if keys exist for a user. + * + * @param {SaaSContext} [context] - Optional SaaS context for multi-tenant mode + * @returns {Promise} True if keys exist, false otherwise + * @abstract + */ + async hasKeys(context) { + throw new Error('hasKeys() must be implemented by subclass'); + } + + /** + * Delete keys for a user (optional, for key rotation or cleanup). + * + * @param {SaaSContext} [context] - Optional SaaS context for multi-tenant mode + * @returns {Promise} True if keys were deleted, false if they didn't exist + * @abstract + */ + async deleteKeys(context) { + throw new Error('deleteKeys() must be implemented by subclass'); + } +} + +export default IKeyStorage; + diff --git a/src/boilerplate/common/key-management/KeyManager.mjs b/src/boilerplate/common/key-management/KeyManager.mjs new file mode 100644 index 00000000..f93dece3 --- /dev/null +++ b/src/boilerplate/common/key-management/KeyManager.mjs @@ -0,0 +1,212 @@ +/** + * @file KeyManager.mjs + * @description Singleton key manager that routes to appropriate storage based on context. + * Provides a unified interface for key management that works in both single-tenant (file-based) + * and multi-tenant (database-based) modes. + */ + +import logger from '../logger.mjs'; +import FileKeyStorage from './FileKeyStorage.mjs'; +import DatabaseKeyStorage from './DatabaseKeyStorage.mjs'; + +/** + * KeyManager singleton class. + * Routes key operations to FileKeyStorage or DatabaseKeyStorage based on context. + * + * Usage: + * const keyManager = KeyManager.getInstance(); + * + * // Single-tenant mode (no context) + * const keys = await keyManager.getKeys(); + * + * // Multi-tenant mode (with context) + * const keys = await keyManager.getKeys({ accountId: 'user-123' }); + */ +export class KeyManager { + /** + * @private + * @type {KeyManager} + */ + static instance = null; + + /** + * @private + */ + constructor() { + if (KeyManager.instance) { + throw new Error('KeyManager is a singleton. Use KeyManager.getInstance() instead.'); + } + + this.fileStorage = new FileKeyStorage(); + this.dbStorage = new DatabaseKeyStorage(); + + logger.debug('KeyManager initialized'); + } + + /** + * Get the singleton instance of KeyManager. + * + * @returns {KeyManager} + */ + static getInstance() { + if (!KeyManager.instance) { + KeyManager.instance = new KeyManager(); + } + return KeyManager.instance; + } + + /** + * Reset the singleton instance (useful for testing). + * + * @private + */ + static resetInstance() { + KeyManager.instance = null; + } + + /** + * Get the appropriate storage implementation based on context. + * + * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Optional SaaS context + * @returns {import('./IKeyStorage.mjs').IKeyStorage} Storage implementation + * @private + */ + getStorage(context) { + if (context && context.accountId) { + logger.debug(`Using DatabaseKeyStorage for accountId: ${context.accountId}`); + return this.dbStorage; + } + + logger.debug('Using FileKeyStorage (single-tenant mode)'); + return this.fileStorage; + } + + /** + * Retrieve keys for a user. + * + * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Optional SaaS context for multi-tenant mode + * @returns {Promise} User keys or null if not found + */ + async getKeys(context) { + try { + const storage = this.getStorage(context); + return await storage.getKeys(context); + } catch (error) { + logger.error('KeyManager.getKeys failed:', error); + throw error; + } + } + + /** + * Save keys for a user. + * + * @param {import('./IKeyStorage.mjs').UserKeys} keys - User keys to save + * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Optional SaaS context for multi-tenant mode + * @returns {Promise} + */ + async saveKeys(keys, context) { + try { + const storage = this.getStorage(context); + return await storage.saveKeys(keys, context); + } catch (error) { + logger.error('KeyManager.saveKeys failed:', error); + throw error; + } + } + + /** + * Register a new key pair. + * + * @param {string} secretKey - Secret key to register (hex string) + * @param {string} contractName - Associated contract name + * @param {boolean} registerWithContract - Whether to register the key on-chain + * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Optional SaaS context for multi-tenant mode + * @returns {Promise} Public key (as integer string) + */ + async registerKey(secretKey, contractName, registerWithContract, context) { + try { + const storage = this.getStorage(context); + return await storage.registerKey(secretKey, contractName, registerWithContract, context); + } catch (error) { + logger.error('KeyManager.registerKey failed:', error); + throw error; + } + } + + /** + * Get or create shared secret keys for encrypted communication. + * + * @param {string} recipientAddress - Recipient's Ethereum address + * @param {string|number} recipientPublicKey - Recipient's public key (0 to fetch from contract) + * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Optional SaaS context for multi-tenant mode + * @returns {Promise} Shared public key + */ + async getSharedSecretKeys(recipientAddress, recipientPublicKey, context) { + try { + const storage = this.getStorage(context); + return await storage.getSharedSecretKeys(recipientAddress, recipientPublicKey, context); + } catch (error) { + logger.error('KeyManager.getSharedSecretKeys failed:', error); + throw error; + } + } + + /** + * Check if keys exist for a user. + * + * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Optional SaaS context for multi-tenant mode + * @returns {Promise} True if keys exist, false otherwise + */ + async hasKeys(context) { + try { + const storage = this.getStorage(context); + return await storage.hasKeys(context); + } catch (error) { + logger.error('KeyManager.hasKeys failed:', error); + throw error; + } + } + + /** + * Delete keys for a user. + * + * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Optional SaaS context for multi-tenant mode + * @returns {Promise} True if keys were deleted, false if they didn't exist + */ + async deleteKeys(context) { + try { + const storage = this.getStorage(context); + return await storage.deleteKeys(context); + } catch (error) { + logger.error('KeyManager.deleteKeys failed:', error); + throw error; + } + } + + /** + * Get storage mode information (for debugging/monitoring). + * + * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Optional SaaS context + * @returns {Object} Storage mode information + */ + getStorageInfo(context) { + const storage = this.getStorage(context); + return { + mode: storage instanceof DatabaseKeyStorage ? 'database' : 'file', + multiTenant: !!context?.accountId, + accountId: context?.accountId || null, + }; + } +} + +/** + * Convenience function to get the KeyManager instance. + * + * @returns {KeyManager} + */ +export function getKeyManager() { + return KeyManager.getInstance(); +} + +export default KeyManager; + diff --git a/src/boilerplate/common/key-management/encryption.mjs b/src/boilerplate/common/key-management/encryption.mjs new file mode 100644 index 00000000..4d866551 --- /dev/null +++ b/src/boilerplate/common/key-management/encryption.mjs @@ -0,0 +1,208 @@ +/** + * @file encryption.mjs + * @description Encryption utilities for securing sensitive key data at rest. + * Uses AES-256-GCM for authenticated encryption. + */ + +import crypto from 'crypto'; +import logger from '../logger.mjs'; + +const ALGORITHM = 'aes-256-gcm'; +const KEY_LENGTH = 32; // 256 bits +const IV_LENGTH = 16; // 128 bits +const AUTH_TAG_LENGTH = 16; // 128 bits +const SALT_LENGTH = 32; // 256 bits + +/** + * Get or generate the encryption key from environment variable. + * The key should be a 64-character hex string (32 bytes). + * + * @returns {Buffer} Encryption key + * @throws {Error} If KEY_ENCRYPTION_KEY is not set or invalid + */ +function getEncryptionKey() { + const keyHex = process.env.KEY_ENCRYPTION_KEY; + + if (!keyHex) { + // In development/single-tenant mode, we can use a default key + // In production multi-tenant mode, this MUST be set + const defaultKey = '0'.repeat(64); // 32 bytes of zeros + logger.warn( + 'KEY_ENCRYPTION_KEY environment variable not set. Using default key. ' + + 'THIS IS INSECURE FOR PRODUCTION USE!' + ); + return Buffer.from(defaultKey, 'hex'); + } + + // Validate key format + if (!/^[0-9a-fA-F]{64}$/.test(keyHex)) { + throw new Error( + 'KEY_ENCRYPTION_KEY must be a 64-character hexadecimal string (32 bytes)' + ); + } + + return Buffer.from(keyHex, 'hex'); +} + +/** + * Check if encryption is enabled. + * Encryption is enabled if KEY_ENCRYPTION_ENABLED is set to 'true' or if running in multi-tenant mode. + * + * @returns {boolean} True if encryption is enabled + */ +export function isEncryptionEnabled() { + return process.env.KEY_ENCRYPTION_ENABLED === 'true'; +} + +/** + * Encrypt a plaintext value using AES-256-GCM. + * + * Format: encrypted:AES256GCM:iv:authTag:ciphertext + * All components are hex-encoded. + * + * @param {string} plaintext - The value to encrypt + * @returns {string} Encrypted value in the format above + * @throws {Error} If encryption fails + */ +export function encrypt(plaintext) { + try { + const key = getEncryptionKey(); + const iv = crypto.randomBytes(IV_LENGTH); + + const cipher = crypto.createCipheriv(ALGORITHM, key, iv); + + let ciphertext = cipher.update(plaintext, 'utf8', 'hex'); + ciphertext += cipher.final('hex'); + + const authTag = cipher.getAuthTag(); + + // Format: encrypted:AES256GCM:iv:authTag:ciphertext + const encrypted = [ + 'encrypted', + 'AES256GCM', + iv.toString('hex'), + authTag.toString('hex'), + ciphertext + ].join(':'); + + logger.debug('Value encrypted successfully'); + return encrypted; + } catch (error) { + logger.error('Encryption failed:', error); + throw new Error(`Encryption failed: ${error.message}`); + } +} + +/** + * Decrypt a value that was encrypted with the encrypt() function. + * + * @param {string} encryptedValue - The encrypted value to decrypt + * @returns {string} Decrypted plaintext + * @throws {Error} If decryption fails or format is invalid + */ +export function decrypt(encryptedValue) { + try { + // Check if value is encrypted + if (!encryptedValue.startsWith('encrypted:AES256GCM:')) { + throw new Error('Invalid encrypted value format: missing prefix'); + } + + const parts = encryptedValue.split(':'); + if (parts.length !== 5) { + throw new Error( + `Invalid encrypted value format: expected 5 parts, got ${parts.length}` + ); + } + + const [prefix, algorithm, ivHex, authTagHex, ciphertext] = parts; + + // Validate components + if (prefix !== 'encrypted' || algorithm !== 'AES256GCM') { + throw new Error('Invalid encrypted value format: invalid prefix or algorithm'); + } + + const key = getEncryptionKey(); + const iv = Buffer.from(ivHex, 'hex'); + const authTag = Buffer.from(authTagHex, 'hex'); + + // Validate lengths + if (iv.length !== IV_LENGTH) { + throw new Error(`Invalid IV length: expected ${IV_LENGTH}, got ${iv.length}`); + } + if (authTag.length !== AUTH_TAG_LENGTH) { + throw new Error(`Invalid auth tag length: expected ${AUTH_TAG_LENGTH}, got ${authTag.length}`); + } + + const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); + decipher.setAuthTag(authTag); + + let plaintext = decipher.update(ciphertext, 'hex', 'utf8'); + plaintext += decipher.final('utf8'); + + logger.debug('Value decrypted successfully'); + return plaintext; + } catch (error) { + logger.error('Decryption failed:', error); + throw new Error(`Decryption failed: ${error.message}`); + } +} + +/** + * Check if a value is encrypted. + * + * @param {string} value - The value to check + * @returns {boolean} True if the value appears to be encrypted + */ +export function isEncrypted(value) { + return typeof value === 'string' && value.startsWith('encrypted:AES256GCM:'); +} + +/** + * Conditionally encrypt a value based on whether encryption is enabled. + * If encryption is disabled, returns the value as-is. + * + * @param {string} value - The value to potentially encrypt + * @returns {string} Encrypted value or original value + */ +export function encryptIfEnabled(value) { + if (isEncryptionEnabled()) { + return encrypt(value); + } + return value; +} + +/** + * Conditionally decrypt a value if it's encrypted. + * If the value is not encrypted, returns it as-is. + * + * @param {string} value - The value to potentially decrypt + * @returns {string} Decrypted value or original value + */ +export function decryptIfEncrypted(value) { + if (isEncrypted(value)) { + return decrypt(value); + } + return value; +} + +/** + * Generate a random encryption key suitable for KEY_ENCRYPTION_KEY. + * This is a utility function for initial setup. + * + * @returns {string} 64-character hex string (32 bytes) + */ +export function generateEncryptionKey() { + const key = crypto.randomBytes(KEY_LENGTH); + return key.toString('hex'); +} + +export default { + encrypt, + decrypt, + isEncrypted, + encryptIfEnabled, + decryptIfEncrypted, + isEncryptionEnabled, + generateEncryptionKey +}; + diff --git a/src/boilerplate/common/key-management/index.mjs b/src/boilerplate/common/key-management/index.mjs new file mode 100644 index 00000000..9761cd7d --- /dev/null +++ b/src/boilerplate/common/key-management/index.mjs @@ -0,0 +1,79 @@ +/** + * @file index.mjs + * @description Main entry point for the key management system. + * Exports all key management components for easy importing. + */ + +// Core components +export { IKeyStorage } from './IKeyStorage.mjs'; +export { FileKeyStorage } from './FileKeyStorage.mjs'; +export { DatabaseKeyStorage } from './DatabaseKeyStorage.mjs'; +export { KeyManager, getKeyManager } from './KeyManager.mjs'; + +// Encryption utilities +export { + encrypt, + decrypt, + isEncrypted, + encryptIfEnabled, + decryptIfEncrypted, + isEncryptionEnabled, + generateEncryptionKey +} from './encryption.mjs'; + +// Middleware +export { + saasContextMiddleware, + requireSaasContext, + forbidSaasContext, + getSaasContext, + isMultiTenant +} from '../middleware/saas-context.mjs'; + +// Migration utilities +export { migrate as setupUserKeysCollection } from './migrations/setup-user-keys.mjs'; + +/** + * Convenience function to get a configured KeyManager instance. + * This is the recommended way to access key management functionality. + * + * @returns {KeyManager} + * + * @example + * import { getKeyManager } from './key-management/index.mjs'; + * + * const keyManager = getKeyManager(); + * const keys = await keyManager.getKeys(req.saasContext); + */ +export function getKeyManager() { + return KeyManager.getInstance(); +} + +export default { + // Core + IKeyStorage, + FileKeyStorage, + DatabaseKeyStorage, + KeyManager, + getKeyManager, + + // Encryption + encrypt, + decrypt, + isEncrypted, + encryptIfEnabled, + decryptIfEncrypted, + isEncryptionEnabled, + generateEncryptionKey, + + // Middleware + saasContextMiddleware, + requireSaasContext, + forbidSaasContext, + getSaasContext, + isMultiTenant, + + // Migration + setupUserKeysCollection +}; + diff --git a/src/boilerplate/common/middleware/saas-context.mjs b/src/boilerplate/common/middleware/saas-context.mjs new file mode 100644 index 00000000..f6cea6e9 --- /dev/null +++ b/src/boilerplate/common/middleware/saas-context.mjs @@ -0,0 +1,194 @@ +/** + * @file saas-context.mjs + * @description Express middleware for parsing and validating x-saas-context header. + * This middleware enables multi-tenant mode by extracting the accountId from the request header. + */ + +import logger from '../logger.mjs'; +import config from 'config'; + +/** + * Middleware to parse and validate the x-saas-context header. + * + * Header format: + * x-saas-context: {"accountId": "user-123"} + * + * Behavior depends on config.multiTenant setting: + * - If config.multiTenant is true (strict mode): + * * Header is REQUIRED - returns 400 if missing + * * All requests must include valid x-saas-context header + * - If config.multiTenant is false (permissive mode): + * * Header is optional - proceeds in single-tenant mode if missing + * * Backward compatible with single-tenant deployments + * + * If the header is present and valid, attaches req.saasContext with the parsed data. + * If the header is present but invalid, returns a 400 error. + * + * @param {import('express').Request} req - Express request object + * @param {import('express').Response} res - Express response object + * @param {import('express').NextFunction} next - Express next function + */ +export function saasContextMiddleware(req, res, next) { + try { + const headerValue = req.headers['x-saas-context']; + const isStrictMode = config.multiTenant === true; + + // If header is not present + if (!headerValue) { + // In strict multi-tenant mode, header is required + if (isStrictMode) { + logger.warn('x-saas-context header required in multi-tenant mode but not provided'); + return res.status(400).json({ + error: 'SaaS context required', + message: 'This application is running in multi-tenant mode and requires the x-saas-context header', + example: '{"accountId": "user-123"}', + hint: 'Add the x-saas-context header to your request' + }); + } + + // In permissive mode, proceed in single-tenant mode + logger.debug('No x-saas-context header - using single-tenant mode'); + req.saasContext = undefined; + return next(); + } + + // Parse the header value + let context; + try { + context = JSON.parse(headerValue); + } catch (parseError) { + logger.warn('Invalid JSON in x-saas-context header:', parseError); + return res.status(400).json({ + error: 'Invalid x-saas-context header', + message: 'Header value must be valid JSON', + example: '{"accountId": "user-123"}' + }); + } + + // Validate accountId is present + if (!context.accountId) { + logger.warn('x-saas-context header missing accountId'); + return res.status(400).json({ + error: 'Invalid x-saas-context header', + message: 'accountId is required', + example: '{"accountId": "user-123"}' + }); + } + + // Validate accountId is a string + if (typeof context.accountId !== 'string') { + logger.warn('x-saas-context accountId is not a string:', typeof context.accountId); + return res.status(400).json({ + error: 'Invalid x-saas-context header', + message: 'accountId must be a string', + received: typeof context.accountId + }); + } + + // Validate accountId format (alphanumeric, hyphens, underscores only) + // This prevents injection attacks and ensures compatibility with database queries + if (!/^[a-zA-Z0-9_-]+$/.test(context.accountId)) { + logger.warn('x-saas-context accountId has invalid format:', context.accountId); + return res.status(400).json({ + error: 'Invalid x-saas-context header', + message: 'accountId must contain only alphanumeric characters, hyphens, and underscores', + pattern: '^[a-zA-Z0-9_-]+$', + received: context.accountId + }); + } + + // Validate accountId length (prevent excessively long IDs) + if (context.accountId.length > 128) { + logger.warn('x-saas-context accountId too long:', context.accountId.length); + return res.status(400).json({ + error: 'Invalid x-saas-context header', + message: 'accountId must be 128 characters or less', + received: context.accountId.length + }); + } + + // Attach validated context to request + req.saasContext = { + accountId: context.accountId + }; + + logger.debug(`SaaS context set for accountId: ${context.accountId}`); + next(); + } catch (error) { + // Catch any unexpected errors + logger.error('Unexpected error in saasContextMiddleware:', error); + return res.status(500).json({ + error: 'Internal server error', + message: 'Failed to process x-saas-context header' + }); + } +} + +/** + * Middleware to require SaaS context (multi-tenant mode). + * Use this middleware on routes that MUST have a SaaS context. + * + * @param {import('express').Request} req - Express request object + * @param {import('express').Response} res - Express response object + * @param {import('express').NextFunction} next - Express next function + */ +export function requireSaasContext(req, res, next) { + if (!req.saasContext || !req.saasContext.accountId) { + logger.warn('SaaS context required but not provided'); + return res.status(400).json({ + error: 'SaaS context required', + message: 'This endpoint requires the x-saas-context header', + example: '{"accountId": "user-123"}' + }); + } + next(); +} + +/** + * Middleware to forbid SaaS context (single-tenant mode only). + * Use this middleware on routes that should NOT accept a SaaS context. + * + * @param {import('express').Request} req - Express request object + * @param {import('express').Response} res - Express response object + * @param {import('express').NextFunction} next - Express next function + */ +export function forbidSaasContext(req, res, next) { + if (req.saasContext && req.saasContext.accountId) { + logger.warn('SaaS context provided but not allowed on this endpoint'); + return res.status(400).json({ + error: 'SaaS context not allowed', + message: 'This endpoint does not support multi-tenant mode' + }); + } + next(); +} + +/** + * Get the SaaS context from a request object. + * Returns undefined if no context is present (single-tenant mode). + * + * @param {import('express').Request} req - Express request object + * @returns {import('../key-management/IKeyStorage.mjs').SaaSContext|undefined} + */ +export function getSaasContext(req) { + return req.saasContext; +} + +/** + * Check if a request is in multi-tenant mode. + * + * @param {import('express').Request} req - Express request object + * @returns {boolean} + */ +export function isMultiTenant(req) { + return !!(req.saasContext && req.saasContext.accountId); +} + +export default { + saasContextMiddleware, + requireSaasContext, + forbidSaasContext, + getSaasContext, + isMultiTenant +}; + diff --git a/src/boilerplate/common/services/generic-api_services.mjs b/src/boilerplate/common/services/generic-api_services.mjs index 0a7ed169..6d5a5b69 100644 --- a/src/boilerplate/common/services/generic-api_services.mjs +++ b/src/boilerplate/common/services/generic-api_services.mjs @@ -38,7 +38,8 @@ export class ServiceManager{ try { await startEventFilter('CONTRACT_NAME'); const FUNCTION_SIG; - const { tx , encEvent, encBackupEvent, _RESPONSE_} = await this.FUNCTION_NAME.FUNCTION_NAME(FUNCTION_SIG); + SAAS_CONTEXT_HANDLING + const { tx , encEvent, encBackupEvent, _RESPONSE_} = await this.FUNCTION_NAME.FUNCTION_NAME(FUNCTION_SIG SAAS_CONTEXT_PARAM); // prints the tx console.log(tx); const txSerialized = serializeBigInt(tx); diff --git a/src/boilerplate/common/services/generic-read-only-api_services.mjs b/src/boilerplate/common/services/generic-read-only-api_services.mjs index 2a441a3e..8f79f32f 100644 --- a/src/boilerplate/common/services/generic-read-only-api_services.mjs +++ b/src/boilerplate/common/services/generic-read-only-api_services.mjs @@ -38,7 +38,8 @@ export class ServiceManager{ try { await startEventFilter('CONTRACT_NAME'); const FUNCTION_SIG; - const { _RESPONSE_} = await this.FUNCTION_NAME.FUNCTION_NAME(FUNCTION_SIG); + SAAS_CONTEXT_HANDLING + const { _RESPONSE_} = await this.FUNCTION_NAME.FUNCTION_NAME(FUNCTION_SIG SAAS_CONTEXT_PARAM); res.send({_RESPONSE_ }); await sleep(10); } catch (err) { diff --git a/src/boilerplate/common/services/genericpublic-api_services.mjs b/src/boilerplate/common/services/genericpublic-api_services.mjs index cef1cf98..dea5775e 100644 --- a/src/boilerplate/common/services/genericpublic-api_services.mjs +++ b/src/boilerplate/common/services/genericpublic-api_services.mjs @@ -11,7 +11,8 @@ let encryption = {}; // eslint-disable-next-line func-names async service_FUNCTION_NAME (req, res, next){ const FUNCTION_SIG; - const { tx , _RESPONSE_} = await this.FUNCTION_NAME.FUNCTION_NAME(FUNCTION_SIG); + SAAS_CONTEXT_HANDLING + const { tx , _RESPONSE_} = await this.FUNCTION_NAME.FUNCTION_NAME(FUNCTION_SIG SAAS_CONTEXT_PARAM); // prints the tx console.log(tx); const txSerialized = serializeBigInt(tx); diff --git a/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts b/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts index 2b7796c4..fe4703a7 100644 --- a/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts +++ b/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts @@ -404,6 +404,7 @@ export function buildBoilerplateNode(nodeType: string, fields: any = {}): any { functions = [], constructorParams = [], contractImports = [], + multiTenant = false, } = fields; return { nodeType, @@ -412,6 +413,7 @@ export function buildBoilerplateNode(nodeType: string, fields: any = {}): any { functions, constructorParams, contractImports, + multiTenant, }; } case 'IntegrationApiRoutesBoilerplate': { diff --git a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts index 73e9321f..ebe4ca78 100644 --- a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts +++ b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts @@ -73,17 +73,18 @@ class BoilerplateGenerator { postStatements(contractName, onChainKeyRegistry): string[] { return [ ` - \n\n// Read dbs for keys and previous commitment values: - \nif (!fs.existsSync(keyDb)) await registerKey(utils.randomHex(31), '${contractName}', ${onChainKeyRegistry}); - const keys = JSON.parse( - fs.readFileSync(keyDb, 'utf-8', err => { - console.log(err); - }), - ); - const secretKey = generalise(keys.secretKey); - const publicKey = generalise(keys.publicKey); - const sharedPublicKey = generalise(keys.sharedPublicKey); - const sharedSecretKey = generalise(keys.sharedSecretKey); + \n\n// Read keys using KeyManager + \nconst keyManager = KeyManager.getInstance(); + \nlet keys = await keyManager.getKeys(context); + \nif (!keys) { + \n // No keys found, register new ones + \n await registerKey(utils.randomHex(31), '${contractName}', ${onChainKeyRegistry}, context); + \n keys = await keyManager.getKeys(context); + \n} + \nconst secretKey = generalise(keys.secretKey); + \nconst publicKey = generalise(keys.publicKey); + \nconst sharedPublicKey = keys.sharedPublicKey ? generalise(keys.sharedPublicKey) : null; + \nconst sharedSecretKey = keys.sharedSecretKey ? generalise(keys.sharedSecretKey) : null; ` ]; }, @@ -423,10 +424,12 @@ class BoilerplateGenerator { `\nimport { storeCommitment, getCurrentWholeCommitment, getCommitmentsById, getAllCommitments, getInputCommitments, joinCommitments, splitCommitments, markNullified} from './common/commitment-storage.mjs';`, `\nimport { generateProof } from './common/zokrates.mjs';`, `\nimport { getMembershipWitness, getRoot } from './common/timber.mjs';`, - `\nimport { decompressStarlightKey, compressStarlightKey, encrypt, decrypt, poseidonHash, scalarMult } from './common/number-theory.mjs'; + `\nimport { decompressStarlightKey, compressStarlightKey, encrypt, decrypt, poseidonHash, scalarMult } from './common/number-theory.mjs';`, + `\nimport { KeyManager } from './common/key-management/KeyManager.mjs'; \n`, `\nconst { generalise } = GN;`, `\nconst db = '/app/orchestration/common/db/preimage.json';`, + `\n// Legacy keyDb path - keys now managed through KeyManager`, `\nconst keyDb = '/app/orchestration/common/db/key.json';\n\n`, ]; }, @@ -860,7 +863,8 @@ integrationApiServicesBoilerplate = { export async function service_backupData(req, res, next) { try { - await backupDataRetriever(); + SAAS_CONTEXT_HANDLING + await backupDataRetriever(SAAS_CONTEXT_DIRECT); res.send("Complete"); await sleep(10); } catch (err) { @@ -871,7 +875,8 @@ integrationApiServicesBoilerplate = { export async function service_backupVariable(req, res, next) { try { const { name } = req.body; - await backupVariable(name); + SAAS_CONTEXT_HANDLING + await backupVariable(name SAAS_CONTEXT_PARAM); res.send("Complete"); await sleep(10); } catch (err) { @@ -883,7 +888,8 @@ integrationApiServicesBoilerplate = { try { const { recipientAddress } = req.body; const recipientPubKey = req.body.recipientPubKey || 0 - const SharedKeys = await getSharedSecretskeys(recipientAddress, recipientPubKey ); + SAAS_CONTEXT_HANDLING + const SharedKeys = await getSharedSecretskeys(recipientAddress, recipientPubKey SAAS_CONTEXT_PARAM); res.send({ SharedKeys }); await sleep(10); } catch (err) { @@ -929,8 +935,8 @@ integrationApiRoutesBoilerplate = { } }; -zappFilesBoilerplate = () => { - return [ +zappFilesBoilerplate = (multiTenant = false) => { + const baseFiles = [ { readPath: pathPrefix + '/config/default.js', writePath: '/config/default.js', @@ -1022,6 +1028,48 @@ zappFilesBoilerplate = () => { generic: false, }, ]; + +if (multiTenant) { + baseFiles.push( + { + readPath: pathPrefix + '/middleware/saas-context.mjs', + writePath: './orchestration/common/middleware/saas-context.mjs', + generic: false, + }, + { + readPath: pathPrefix + '/key-management/IKeyStorage.mjs', + writePath: './orchestration/common/key-management/IKeyStorage.mjs', + generic: false, + }, + { + readPath: pathPrefix + '/key-management/FileKeyStorage.mjs', + writePath: './orchestration/common/key-management/FileKeyStorage.mjs', + generic: false, + }, + { + readPath: pathPrefix + '/key-management/DatabaseKeyStorage.mjs', + writePath: './orchestration/common/key-management/DatabaseKeyStorage.mjs', + generic: false, + }, + { + readPath: pathPrefix + '/key-management/KeyManager.mjs', + writePath: './orchestration/common/key-management/KeyManager.mjs', + generic: false, + }, + { + readPath: pathPrefix + '/key-management/encryption.mjs', + writePath: './orchestration/common/key-management/encryption.mjs', + generic: false, + }, + { + readPath: pathPrefix + '/key-management/index.mjs', + writePath: './orchestration/common/key-management/index.mjs', + generic: false, + } + ); + } + + return baseFiles; } } diff --git a/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts b/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts index c4f927ab..f79e31a9 100644 --- a/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts +++ b/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts @@ -573,7 +573,7 @@ export const OrchestrationCodeBoilerPlate: any = (node: any) => { return { signature: [ `${functionSig} - \n async ${node.name}(${params} ${states}) {`, + \n async ${node.name}(${params} ${states}, context) {`, `\n return { ${txReturns} ${publicReturns}}; \n} \n}`, @@ -585,7 +585,7 @@ export const OrchestrationCodeBoilerPlate: any = (node: any) => { return { signature: [ ` - \n async ${node.name}(${params} ${states}) {`, + \n async ${node.name}(${params} ${states}, context) {`, `\n const bool = true; \n return { ${txReturns} ${rtnparams}, ${publicReturns} }; \n} \n}`, @@ -596,7 +596,7 @@ export const OrchestrationCodeBoilerPlate: any = (node: any) => { return { signature: [ ` ${functionSig} - \n async ${node.name}(${params} ${states}) {`, + \n async ${node.name}(${params} ${states}, context) {`, `\nreturn { ${txReturns} ${rtnparams}, ${publicReturns}}; \n} \n}`, diff --git a/src/codeGenerators/common.ts b/src/codeGenerators/common.ts index 0bcfcafc..ceb5c609 100644 --- a/src/codeGenerators/common.ts +++ b/src/codeGenerators/common.ts @@ -20,6 +20,7 @@ export const collectImportFiles = ( context: string, contextDirPath?: string, fileName: string = '', + visited: any = new Set(), ) => { const lines = file.split('\n'); let ImportStatementList: string[]; @@ -97,8 +98,16 @@ export const collectImportFiles = ( } const absPath = path.resolve(contextDirPath, p); const relPath = path.relative('.', absPath); + + if (visited.has(relPath)) { + continue; + } + const exists = fs.existsSync(relPath); if (!exists) continue; + + visited.add(relPath); + const f = fs.readFileSync(relPath, 'utf8'); const n = path.basename(absPath, path.extname(absPath)); const shortRelPath = path.relative(path.resolve(fileURLToPath(import.meta.url), '../../../'), absPath); @@ -129,12 +138,12 @@ export const collectImportFiles = ( file: f, }); - localFiles = localFiles.concat(collectImportFiles(f, context, path.dirname(relPath), context === 'contract' ? n : '')); + localFiles = localFiles.concat(collectImportFiles(f, context, path.dirname(relPath), context === 'contract' ? n : '', visited)); } // remove duplicate files after recursion: const uniqueLocalFiles = localFiles.filter((obj, i, self) => { - return self.indexOf(obj) === i; + return self.findIndex(item => item.filepath === obj.filepath) === i; }); return uniqueLocalFiles; diff --git a/src/codeGenerators/orchestration/files/toOrchestration.ts b/src/codeGenerators/orchestration/files/toOrchestration.ts index 96e7e6cd..05a82c23 100644 --- a/src/codeGenerators/orchestration/files/toOrchestration.ts +++ b/src/codeGenerators/orchestration/files/toOrchestration.ts @@ -213,6 +213,20 @@ const prepareIntegrationApiServices = (node: any) => { fnboilerplate = fnboilerplate.replace(/_RESPONSE_/g, returnParams + publicReturns); + // Handle SaaS context placeholders based on multi-tenant flag + fnboilerplate = fnboilerplate.replace( + /SAAS_CONTEXT_HANDLING/g, + node.multiTenant + ? `// Pass context for multi-tenant support (available via saasContextMiddleware) + const context = req.saasContext;` + : `// Single-tenant mode - no context needed`, + ); + + fnboilerplate = fnboilerplate.replace( + /SAAS_CONTEXT_PARAM/g, + node.multiTenant ? `, context` : ``, + ); + // replace function imports at top of file const fnimport = ` import { ${(fn.name).charAt(0).toUpperCase() + fn.name.slice(1)}Manager } from './${fn.name}.mjs' ;` @@ -222,7 +236,26 @@ const prepareIntegrationApiServices = (node: any) => { }); // add linting and config const preprefix = `/* eslint-disable prettier/prettier, camelcase, prefer-const, no-unused-vars */ \nimport config from 'config';\nimport assert from 'assert';\n`; - outputApiServiceFile = `${preprefix}\n${outputApiServiceFile}}\n ${genericApiServiceFile.commitments()}\n`; + + // Handle SaaS context in commitments functions + let commitmentsCode = genericApiServiceFile.commitments(); + commitmentsCode = commitmentsCode.replace( + /SAAS_CONTEXT_HANDLING/g, + node.multiTenant + ? `// Pass context for multi-tenant support + const context = req.saasContext;` + : `// Single-tenant mode - no context needed`, + ); + commitmentsCode = commitmentsCode.replace( + /SAAS_CONTEXT_PARAM/g, + node.multiTenant ? `, context` : ``, + ); + commitmentsCode = commitmentsCode.replace( + /SAAS_CONTEXT_DIRECT/g, + node.multiTenant ? `context` : `undefined`, + ); + + outputApiServiceFile = `${preprefix}\n${outputApiServiceFile}}\n ${commitmentsCode}\n`; return outputApiServiceFile; }; const prepareIntegrationApiRoutes = (node: any) => { @@ -829,6 +862,7 @@ const prepareBackupDataRetriever = (node: any) => { getContractInstance, getContractAddress, } from "./common/contract.mjs"; + import { KeyManager } from "./common/key-management/KeyManager.mjs"; import Web3 from "./common/web3.mjs"; import { @@ -847,7 +881,7 @@ const prepareBackupDataRetriever = (node: any) => { const { MONGO_URL, COMMITMENTS_DB, COMMITMENTS_COLLECTION } = config; - export async function backupDataRetriever() { + export async function backupDataRetriever(context) { const connection = await mongo.connection(MONGO_URL); const db = connection.db(COMMITMENTS_DB); @@ -871,11 +905,13 @@ const prepareBackupDataRetriever = (node: any) => { const backDataEvent = await instance.getPastEvents('EncryptedBackupData',{fromBlock: 0, toBlock: 'latest'} ); - const keys = JSON.parse( - fs.readFileSync(keyDb, "utf-8", (err) => { - console.log(err); - }) - ); + // Use KeyManager for key retrieval + const keyManager = KeyManager.getInstance(); + const keys = await keyManager.getKeys(context); + + if (!keys) { + throw new Error('No keys found. Please register keys first.'); + } const secretKey = generalise(keys.secretKey); const publicKey = generalise(keys.publicKey); const sharedPublicKey = generalise(keys.sharedPublicKey); diff --git a/src/transformers/toOrchestration.ts b/src/transformers/toOrchestration.ts index 4e4df63e..07d0e19f 100644 --- a/src/transformers/toOrchestration.ts +++ b/src/transformers/toOrchestration.ts @@ -24,7 +24,8 @@ export default function toOrchestration(ast: any, options: any) { snarkVerificationRequired: true, newCommitmentsRequired: true, nullifiersRequired: true, - circuitAST:options.circuitAST + circuitAST:options.circuitAST, + multiTenant: options.multiTenant }; logger.debug('Transforming the .zol AST to a .mjs AST...'); @@ -71,7 +72,7 @@ export default function toOrchestration(ast: any, options: any) { `Saving backend files to the zApp output directory ${options.outputDirPath}...`, ); // TODO merge this process with above - const zappFilesBP = Orchestrationbp.zappFilesBoilerplate(); + const zappFilesBP = Orchestrationbp.zappFilesBoilerplate(options.multiTenant); if (!(zappFilesBP instanceof Array)) throw new Error('Boilerplate files not read correctly!'); let fileObj: any; // we go through the below process in the codeGenerator for other files @@ -99,6 +100,30 @@ export default function toOrchestration(ast: any, options: any) { await eventListener.start()` : ` `, ); + + // Handle SaaS middleware based on multi-tenant flag + file = file.replace( + /SAAS_MIDDLEWARE_IMPORT/g, + options.multiTenant + ? `import { saasContextMiddleware } from './common/middleware/saas-context.mjs';` + : ``, + ); + + file = file.replace( + /SAAS_MIDDLEWARE_USAGE/g, + options.multiTenant + ? `// Add SaaS context middleware for multi-tenant support +// This middleware parses the x-saas-context header and attaches req.saasContext +// If no header is present, the app operates in single-tenant mode (backward compatible) +app.use(saasContextMiddleware);` + : ``, + ); + + // Replace multi-tenant mode configuration + file = file.replace( + /MULTI_TENANT_MODE/g, + options.multiTenant ? `true` : `false`, + ); } const dir = pathjs.dirname(filepath); logger.debug(`About to save to ${filepath}...`); diff --git a/src/transformers/visitors/toOrchestrationVisitor.ts b/src/transformers/visitors/toOrchestrationVisitor.ts index 6bd98710..a301e0c0 100644 --- a/src/transformers/visitors/toOrchestrationVisitor.ts +++ b/src/transformers/visitors/toOrchestrationVisitor.ts @@ -397,6 +397,7 @@ const visitor = { buildNode('IntegrationApiServicesBoilerplate', { contractName, contractImports: state.contractImports, + multiTenant: state.multiTenant, }), ], }); From 0b4923c7be02c742638a5328df38f69d4fc0a5c8 Mon Sep 17 00:00:00 2001 From: Adarsh Ron Date: Thu, 16 Oct 2025 15:46:58 +0530 Subject: [PATCH 02/18] feat: working deposit flow with multiuser kms --- mintAndApprove.mjs | 444 +++++++++ package-lock.json | 898 ++++++++++++++++++ package.json | 16 +- src/boilerplate/common/commitment-storage.mjs | 16 +- .../common/services/saas-key-services.mjs | 150 +++ .../javascript/raw/boilerplate-generator.ts | 9 +- 6 files changed, 1521 insertions(+), 12 deletions(-) create mode 100644 mintAndApprove.mjs create mode 100644 src/boilerplate/common/services/saas-key-services.mjs diff --git a/mintAndApprove.mjs b/mintAndApprove.mjs new file mode 100644 index 00000000..0d8a37f1 --- /dev/null +++ b/mintAndApprove.mjs @@ -0,0 +1,444 @@ +import fs from 'fs'; +import path from 'path'; +import { ethers } from 'ethers'; +import { fileURLToPath } from 'url'; +import { request } from 'http'; // For making HTTP requests + +// Get directory name properly in ESM +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Configuration (can be changed directly here) +// Parse arguments properly whether run directly or via npm run +function parseArgs() { + const args = process.argv.slice(2); + let tokenId = 3; + let rpcUrl = 'http://localhost:8545'; + let action = 'mint'; + let accountId = null; // For multi-tenant setup + + // Find the token ID (first numeric argument) + for (const arg of args) { + if (arg !== '--' && !isNaN(arg)) { + tokenId = parseInt(arg); + break; + } + } + + // Check for action type + if (args.includes('deposit')) { + action = 'deposit'; + } else if (args.includes('both')) { + action = 'both'; + } else if (args.includes('commitments')) { + action = 'commitments'; + } + + // Look for an URL argument + const urlArg = args.find(arg => arg.startsWith('http')); + if (urlArg) { + rpcUrl = urlArg; + } + + // Look for accountId argument (format: accountId=uuid) + const accountIdArg = args.find(arg => arg.startsWith('accountId=')); + if (accountIdArg) { + accountId = accountIdArg.split('=')[1]; + } + + console.log(`Parsed args - Token ID: ${tokenId}, RPC URL: ${rpcUrl}, Action: ${action}${accountId ? ', AccountId: ' + accountId : ''}`); + return { tokenId, rpcUrl, action, accountId }; +} + +const { tokenId: TOKEN_ID, rpcUrl: RPC_URL, action: ACTION, accountId: ACCOUNT_ID } = parseArgs(); + +async function mintAndApprove() { + try { + // Read contract ABIs and addresses + const erc721Path = path.join(__dirname, 'zapps/NFT_Escrow/build/contracts/ERC721.json'); + const shieldPath = path.join(__dirname, 'zapps/NFT_Escrow/build/contracts/NFT_EscrowShield.json'); + + console.log(`Reading ERC721 contract from ${erc721Path}`); + console.log(`Reading Shield contract from ${shieldPath}`); + + const erc721Json = JSON.parse(fs.readFileSync(erc721Path, 'utf8')); + const shieldJson = JSON.parse(fs.readFileSync(shieldPath, 'utf8')); + + // Get contract addresses from network 31337 (local hardhat network) + const ERC721_ADDRESS = erc721Json.networks['31337'].address; + const SHIELD_ADDRESS = shieldJson.networks['31337'].address; + + // Connect to local network + const provider = new ethers.providers.JsonRpcProvider(RPC_URL); + + // Get signer (using first account from local network) + const accounts = await provider.listAccounts(); + const signer = provider.getSigner(accounts[0]); + const signerAddress = accounts[0]; + + console.log('Connected to network with signer:', signerAddress); + console.log('ERC721 Contract Address:', ERC721_ADDRESS); + console.log('Shield Contract Address:', SHIELD_ADDRESS); + + // Create contract instance + const erc721Contract = new ethers.Contract(ERC721_ADDRESS, erc721Json.abi, signer); + + console.log('\n--- Minting NFT ---'); + console.log(`Minting token ID ${TOKEN_ID} to ${signerAddress}...`); + + // Mint NFT + const mintTx = await erc721Contract.mint(signerAddress, TOKEN_ID); + console.log('Mint transaction sent, waiting for confirmation...'); + const mintReceipt = await mintTx.wait(); + console.log('Mint transaction hash:', mintTx.hash); + console.log('NFT minted successfully! Gas used:', mintReceipt.gasUsed.toString()); + + // Verify ownership + const owner = await erc721Contract.ownerOf(TOKEN_ID); + console.log(`Token ${TOKEN_ID} owner:`, owner); + + console.log('\n--- Approving Shield Contract ---'); + console.log(`Approving shield contract ${SHIELD_ADDRESS} for token ${TOKEN_ID}...`); + + // Approve shield contract to transfer the NFT + const approveTx = await erc721Contract.approve(SHIELD_ADDRESS, TOKEN_ID); + console.log('Approve transaction sent, waiting for confirmation...'); + const approveReceipt = await approveTx.wait(); + console.log('Approve transaction hash:', approveTx.hash); + console.log('Shield contract approved successfully! Gas used:', approveReceipt.gasUsed.toString()); + + // Verify approval + const approvedAddress = await erc721Contract.getApproved(TOKEN_ID); + console.log(`Approved address for token ${TOKEN_ID}:`, approvedAddress); + + if (approvedAddress.toLowerCase() === SHIELD_ADDRESS.toLowerCase()) { + console.log('\n✅ Mint and approve completed successfully!'); + } else { + console.log('\n⚠️ Approval verification failed. Please check manually.'); + } + + } catch (error) { + console.error('Error:', error.message || error); + if (error.data) { + console.error('Error data:', error.data); + } + process.exit(1); + } +} + +async function depositToShield() { + try { + // Read contract ABIs and addresses + const erc721Path = path.join(__dirname, 'zapps/NFT_Escrow/build/contracts/ERC721.json'); + const shieldPath = path.join(__dirname, 'zapps/NFT_Escrow/build/contracts/NFT_EscrowShield.json'); + + console.log(`Reading contracts from build directory...`); + + const erc721Json = JSON.parse(fs.readFileSync(erc721Path, 'utf8')); + const shieldJson = JSON.parse(fs.readFileSync(shieldPath, 'utf8')); + + // Get contract addresses from network 31337 (local hardhat network) + const ERC721_ADDRESS = erc721Json.networks['31337'].address; + const SHIELD_ADDRESS = shieldJson.networks['31337'].address; + + // Connect to local network + const provider = new ethers.providers.JsonRpcProvider(RPC_URL); + + // Get signer (using first account from local network) + const accounts = await provider.listAccounts(); + const signer = provider.getSigner(accounts[0]); + const signerAddress = accounts[0]; + + console.log('Connected to network with signer:', signerAddress); + console.log('ERC721 Contract Address:', ERC721_ADDRESS); + console.log('Shield Contract Address:', SHIELD_ADDRESS); + + // Create ERC721 contract instance to check ownership and approval + const erc721Contract = new ethers.Contract(ERC721_ADDRESS, erc721Json.abi, signer); + const owner = await erc721Contract.ownerOf(TOKEN_ID); + const approvedAddress = await erc721Contract.getApproved(TOKEN_ID); + + if (owner.toLowerCase() !== signerAddress.toLowerCase()) { + console.error(`Error: You don't own token ID ${TOKEN_ID}`); + process.exit(1); + } + + if (approvedAddress.toLowerCase() !== SHIELD_ADDRESS.toLowerCase()) { + console.error(`Error: Shield contract is not approved to transfer token ID ${TOKEN_ID}`); + process.exit(1); + } + + console.log('\n--- Depositing NFT to Shield via Zapp API ---'); + console.log(`Preparing deposit for token ID ${TOKEN_ID}...`); + + // Generate a random secret for the deposit + const secret = ethers.utils.hexlify(ethers.utils.randomBytes(32)); + + // Define the Zapp API endpoint for deposit + const ZAPP_HOST = 'localhost'; + const ZAPP_PORT = 3000; // The Zapp is running on port 3000 + + // Based on the router.post("/deposit") configuration in the Zapp + const ZAPP_PATH = '/deposit'; + + console.log(`Calling Zapp API at http://${ZAPP_HOST}:${ZAPP_PORT}${ZAPP_PATH}...`); + console.log(`Depositing token ID ${TOKEN_ID} with secret: ${secret}`); + + // Create a deposit payload for the API - based on the service_deposit function + // The API only requires tokenId and optionally tokenOwners_tokenId_newOwnerPublicKey + const depositPayload = { + tokenId: TOKEN_ID, + tokenOwners_tokenId_newOwnerPublicKey: 0 // Optional parameter, using default value + }; + + // Create initial deposit info + let depositInfo = { + tokenId: TOKEN_ID, + secret: secret, + owner: signerAddress, + timestamp: new Date().toISOString() + }; + + // Make the actual HTTP request to the deposit endpoint + const depositResult = await new Promise((resolve, reject) => { + // Convert payload to JSON string + const postData = JSON.stringify(depositPayload); + + // Set up the request options + // Set up the request options + const options = { + hostname: ZAPP_HOST, + port: ZAPP_PORT, + path: ZAPP_PATH, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData) + } + }; + + // Add x-saas-context header for multi-tenant setups if accountId is provided + if (ACCOUNT_ID) { + console.log(`Using multi-tenant mode with accountId: ${ACCOUNT_ID}`); + options.headers['x-saas-context'] = JSON.stringify({ accountId: ACCOUNT_ID }); + } + + // Create the request + const req = request(options, (res) => { + let responseData = ''; + + // A chunk of data has been received + res.on('data', (chunk) => { + responseData += chunk; + }); + + // The whole response has been received + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + try { + const parsedData = JSON.parse(responseData); + resolve({ success: true, data: parsedData }); + } catch (e) { + resolve({ success: true, data: responseData }); + } + } else { + reject(new Error(`API request failed with status code ${res.statusCode}: ${responseData}`)); + } + }); + }); + + // Handle request errors + req.on('error', (error) => { + reject(new Error(`Error making API request: ${error.message}`)); + }); + + // Write post data and end the request + req.write(postData); + req.end(); + }); + + // Process the API response + if (depositResult.success) { + console.log('\nDeposit API call successful!'); + console.log('Response:', JSON.stringify(depositResult.data, null, 2)); + + // Update deposit info with transaction details and commitments from the response + if (depositResult.data.tx && depositResult.data.tx.transactionHash) { + depositInfo = { + ...depositInfo, + txHash: depositResult.data.tx.transactionHash, + blockNumber: depositResult.data.tx.blockNumber, + contractAddress: depositResult.data.tx.address, + commitments: depositResult.data.tx.returnValues?.leafValues || [] + }; + } + + // Also capture any direct commitment data returned by the API + if (depositResult.data.commitments) { + depositInfo.commitmentData = depositResult.data.commitments; + } else if (depositResult.data.commitment) { + depositInfo.commitmentData = depositResult.data.commitment; + } + } else { + console.error('\nDeposit API call failed!'); + throw new Error('Failed to complete deposit via API'); + } + + // Include timestamp and account information in the filename for better organization + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const accountSuffix = ACCOUNT_ID ? `-${ACCOUNT_ID}` : ''; + const depositInfoPath = path.join(__dirname, `nft-deposit-info-${TOKEN_ID}${accountSuffix}.json`); + fs.writeFileSync(depositInfoPath, JSON.stringify(depositInfo, null, 2)); + console.log(`\nDeposit information saved to: ${depositInfoPath}`); + console.log('Keep this file secure - you will need it to withdraw your NFT later!'); + + console.log('\n✅ Deposit completed successfully via API call.'); + + } catch (error) { + console.error('Error during deposit:', error.message || error); + if (error.data) { + console.error('Error data:', error.data); + } + + // Provide troubleshooting guidance for common API errors + console.log('\n--- Troubleshooting Tips ---'); + console.log('1. Ensure the NFT Escrow Zapp API is running (on port 3000)'); + console.log('2. Check that the token is properly minted and approved'); + console.log('3. The correct API endpoint is /deposit based on the Zapp router configuration'); + console.log('4. The API expects only tokenId in the request body'); + console.log('5. For multi-tenant setups, make sure to include a valid accountId (accountId=uuid)'); + console.log('6. You can also try using the Zapp UI at http://localhost:3000'); + + process.exit(1); + } +} + +// Run the appropriate script based on the action parameter +async function main() { + if (ACTION === 'mint' || ACTION === 'both') { + await mintAndApprove(); + } + + if (ACTION === 'deposit' || ACTION === 'both') { + await depositToShield(); + // After successful deposit, fetch commitments + await fetchUserCommitments(); + } + + if (ACTION === 'commitments') { + console.log('Fetching commitments only...'); + await fetchUserCommitments(); + } +} + +/** + * Fetch user commitments from the Zapp API + * This function will be called after a successful deposit + * to get all commitments associated with the user + */ +async function fetchUserCommitments() { + try { + console.log('\n--- Fetching User Commitments ---'); + + // Define the Zapp API endpoint for commitments + const ZAPP_HOST = 'localhost'; + const ZAPP_PORT = 3000; + const ZAPP_PATH = '/getAllCommitments'; // Correct endpoint from api_routes.mjs + + console.log(`Fetching commitments from http://${ZAPP_HOST}:${ZAPP_PORT}${ZAPP_PATH}...`); + + // Make HTTP request to get user's commitments + const commitmentsResult = await new Promise((resolve, reject) => { + // Set up the request options + const options = { + hostname: ZAPP_HOST, + port: ZAPP_PORT, + path: ZAPP_PATH, + method: 'GET', + headers: { + 'Accept': 'application/json' + } + }; + + // Add x-saas-context header for multi-tenant setups if accountId is provided + if (ACCOUNT_ID) { + console.log(`Using multi-tenant mode with accountId: ${ACCOUNT_ID} for fetching commitments`); + options.headers['x-saas-context'] = JSON.stringify({ accountId: ACCOUNT_ID }); + } + + // Create the request + const req = request(options, (res) => { + let responseData = ''; + + // A chunk of data has been received + res.on('data', (chunk) => { + responseData += chunk; + }); + + // The whole response has been received + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + try { + const parsedData = JSON.parse(responseData); + resolve({ success: true, data: parsedData }); + } catch (e) { + resolve({ success: true, data: responseData }); + } + } else { + reject(new Error(`API request failed with status code ${res.statusCode}: ${responseData}`)); + } + }); + }); + + // Handle request errors + req.on('error', (error) => { + reject(new Error(`Error making API request: ${error.message}`)); + }); + + // End the request (no body for GET request) + req.end(); + }); + + if (commitmentsResult.success) { + console.log('\nFetch commitments successful!'); + + // Save the commitments to a file + const commitmentsData = commitmentsResult.data; + const commitmentsPath = path.join(__dirname, `user-commitments${ACCOUNT_ID ? `-${ACCOUNT_ID}` : ''}.json`); + fs.writeFileSync(commitmentsPath, JSON.stringify(commitmentsData, null, 2)); + + console.log(`Commitments saved to: ${commitmentsPath}`); + + // Handle the expected response structure where commitments are in a 'commitments' property + const commitmentsList = commitmentsData.commitments || commitmentsData; + + console.log(`Total commitments found: ${Array.isArray(commitmentsList) ? commitmentsList.length : 'unknown'}`); + + // Display some information about the commitments + if (Array.isArray(commitmentsList) && commitmentsList.length > 0) { + console.log('\nLatest commitments:'); + const latestCommitments = commitmentsList.slice(-3); // Show last 3 commitments + latestCommitments.forEach((commitment, index) => { + console.log(`[${index}] Commitment ${commitment._id || 'unknown'} for mapping key ${commitment.mappingKey || 'unknown'}`); + }); + } + + return commitmentsData; + } else { + console.error('\nFailed to fetch commitments!'); + return null; + } + + } catch (error) { + console.error('Error fetching commitments:', error.message || error); + console.log('\nFailed to fetch commitments, but deposit may have been successful.'); + return null; + } +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/package-lock.json b/package-lock.json index 1f484a49..a96e75da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,7 @@ "chai-http": "^4.3.0", "eslint": "^8.2.0", "eslint-config-codfish": "^11.1.0", + "ethers": "^5.7.2", "mocha": "^10.8.2" } }, @@ -1868,6 +1869,737 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@ethersproject/abi": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.8.0.tgz", + "integrity": "sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/abstract-provider": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.8.0.tgz", + "integrity": "sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0" + } + }, + "node_modules/@ethersproject/abstract-signer": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.8.0.tgz", + "integrity": "sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0" + } + }, + "node_modules/@ethersproject/address": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.8.0.tgz", + "integrity": "sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/rlp": "^5.8.0" + } + }, + "node_modules/@ethersproject/base64": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.8.0.tgz", + "integrity": "sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0" + } + }, + "node_modules/@ethersproject/basex": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.8.0.tgz", + "integrity": "sha512-PIgTszMlDRmNwW9nhS6iqtVfdTAKosA7llYXNmGPw4YAI1PUyMv28988wAb41/gHF/WqGdoLv0erHaRcHRKW2Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/properties": "^5.8.0" + } + }, + "node_modules/@ethersproject/bignumber": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.8.0.tgz", + "integrity": "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "bn.js": "^5.2.1" + } + }, + "node_modules/@ethersproject/bytes": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.8.0.tgz", + "integrity": "sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/constants": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.8.0.tgz", + "integrity": "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0" + } + }, + "node_modules/@ethersproject/contracts": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.8.0.tgz", + "integrity": "sha512-0eFjGz9GtuAi6MZwhb4uvUM216F38xiuR0yYCjKJpNfSEy4HUM8hvqqBj9Jmm0IUz8l0xKEhWwLIhPgxNY0yvQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abi": "^5.8.0", + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/transactions": "^5.8.0" + } + }, + "node_modules/@ethersproject/hash": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.8.0.tgz", + "integrity": "sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/hdnode": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.8.0.tgz", + "integrity": "sha512-4bK1VF6E83/3/Im0ERnnUeWOY3P1BZml4ZD3wcH8Ys0/d1h1xaFt6Zc+Dh9zXf9TapGro0T4wvO71UTCp3/uoA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/basex": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/pbkdf2": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/wordlists": "^5.8.0" + } + }, + "node_modules/@ethersproject/json-wallets": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.8.0.tgz", + "integrity": "sha512-HxblNck8FVUtNxS3VTEYJAcwiKYsBIF77W15HufqlBF9gGfhmYOJtYZp8fSDZtn9y5EaXTE87zDwzxRoTFk11w==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hdnode": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/pbkdf2": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "aes-js": "3.0.0", + "scrypt-js": "3.0.1" + } + }, + "node_modules/@ethersproject/keccak256": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz", + "integrity": "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@ethersproject/logger": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.8.0.tgz", + "integrity": "sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT" + }, + "node_modules/@ethersproject/networks": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.8.0.tgz", + "integrity": "sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/pbkdf2": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.8.0.tgz", + "integrity": "sha512-wuHiv97BrzCmfEaPbUFpMjlVg/IDkZThp9Ri88BpjRleg4iePJaj2SW8AIyE8cXn5V1tuAaMj6lzvsGJkGWskg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/sha2": "^5.8.0" + } + }, + "node_modules/@ethersproject/properties": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.8.0.tgz", + "integrity": "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/providers": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.8.0.tgz", + "integrity": "sha512-3Il3oTzEx3o6kzcg9ZzbE+oCZYyY+3Zh83sKkn4s1DZfTUjIegHnN2Cm0kbn9YFy45FDVcuCLLONhU7ny0SsCw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/basex": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0", + "bech32": "1.1.4", + "ws": "8.18.0" + } + }, + "node_modules/@ethersproject/random": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.8.0.tgz", + "integrity": "sha512-E4I5TDl7SVqyg4/kkA/qTfuLWAQGXmSOgYyO01So8hLfwgKvYK5snIlzxJMk72IFdG/7oh8yuSqY2KX7MMwg+A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/rlp": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.8.0.tgz", + "integrity": "sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/sha2": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.8.0.tgz", + "integrity": "sha512-dDOUrXr9wF/YFltgTBYS0tKslPEKr6AekjqDW2dbn1L1xmjGR+9GiKu4ajxovnrDbwxAKdHjW8jNcwfz8PAz4A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/signing-key": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.8.0.tgz", + "integrity": "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "bn.js": "^5.2.1", + "elliptic": "6.6.1", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/solidity": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.8.0.tgz", + "integrity": "sha512-4CxFeCgmIWamOHwYN9d+QWGxye9qQLilpgTU0XhYs1OahkclF+ewO+3V1U0mvpiuQxm5EHHmv8f7ClVII8EHsA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/strings": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.8.0.tgz", + "integrity": "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/transactions": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.8.0.tgz", + "integrity": "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0" + } + }, + "node_modules/@ethersproject/units": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.8.0.tgz", + "integrity": "sha512-lxq0CAnc5kMGIiWW4Mr041VT8IhNM+Pn5T3haO74XZWFulk7wH1Gv64HqE96hT4a7iiNMdOCFEBgaxWuk8ETKQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/wallet": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.8.0.tgz", + "integrity": "sha512-G+jnzmgg6UxurVKRKvw27h0kvG75YKXZKdlLYmAHeF32TGUzHkOFd7Zn6QHOTYRFWnfjtSSFjBowKo7vfrXzPA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/hdnode": "^5.8.0", + "@ethersproject/json-wallets": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/wordlists": "^5.8.0" + } + }, + "node_modules/@ethersproject/web": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.8.0.tgz", + "integrity": "sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/wordlists": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.8.0.tgz", + "integrity": "sha512-2df9bbXicZws2Sb5S6ET493uJ0Z84Fjr3pC4tu/qlnZERibZCeUVuqdtt+7Tv9xxhUxHoIekIA7avrKUWHrezg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -2503,6 +3235,13 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", + "dev": true, + "license": "MIT" + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2899,6 +3638,13 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", + "dev": true, + "license": "MIT" + }, "node_modules/big-integer": { "version": "1.6.52", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", @@ -2921,6 +3667,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bn.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "dev": true, + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -2998,6 +3751,13 @@ "node": ">=8" } }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "dev": true, + "license": "MIT" + }, "node_modules/browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -3848,6 +4608,29 @@ "integrity": "sha512-refir3NlutEZqlKaBLK0tzlVLe5P2wDKS7UQt/3SpibizgsRAPOsqQC3ffw1nlv3ze5gjRQZYHoPymgVZkplFA==", "license": "ISC" }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "dev": true, + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -4835,6 +5618,55 @@ "node": ">= 0.6" } }, + "node_modules/ethers": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.8.0.tgz", + "integrity": "sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abi": "5.8.0", + "@ethersproject/abstract-provider": "5.8.0", + "@ethersproject/abstract-signer": "5.8.0", + "@ethersproject/address": "5.8.0", + "@ethersproject/base64": "5.8.0", + "@ethersproject/basex": "5.8.0", + "@ethersproject/bignumber": "5.8.0", + "@ethersproject/bytes": "5.8.0", + "@ethersproject/constants": "5.8.0", + "@ethersproject/contracts": "5.8.0", + "@ethersproject/hash": "5.8.0", + "@ethersproject/hdnode": "5.8.0", + "@ethersproject/json-wallets": "5.8.0", + "@ethersproject/keccak256": "5.8.0", + "@ethersproject/logger": "5.8.0", + "@ethersproject/networks": "5.8.0", + "@ethersproject/pbkdf2": "5.8.0", + "@ethersproject/properties": "5.8.0", + "@ethersproject/providers": "5.8.0", + "@ethersproject/random": "5.8.0", + "@ethersproject/rlp": "5.8.0", + "@ethersproject/sha2": "5.8.0", + "@ethersproject/signing-key": "5.8.0", + "@ethersproject/solidity": "5.8.0", + "@ethersproject/strings": "5.8.0", + "@ethersproject/transactions": "5.8.0", + "@ethersproject/units": "5.8.0", + "@ethersproject/wallet": "5.8.0", + "@ethersproject/web": "5.8.0", + "@ethersproject/wordlists": "5.8.0" + } + }, "node_modules/event-emitter": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", @@ -5614,6 +6446,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -5652,6 +6495,18 @@ "node": ">=8" } }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -6811,6 +7666,20 @@ "node": ">= 0.6" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "dev": true, + "license": "MIT" + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -10650,6 +11519,13 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/scrypt-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", + "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==", + "dev": true, + "license": "MIT" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -11946,6 +12822,28 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", diff --git a/package.json b/package.json index d82ca09a..6853c9bd 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,20 @@ "test-prelim": "mocha --inline-diffs --require @babel/register ./test/prelim-traversals/index.mjs", "test-prelim-lite": "mocha --require @babel/register ./test/prelim-traversals/index.mjs", "format": "prettier --write \"**/*.{json,css,scss,md}\"", - "lint": "eslint ." + "lint": "eslint .", + "mint": "node mintAndApprove.mjs", + "deposit": "node mintAndApprove.mjs deposit", + "mint-and-deposit": "node mintAndApprove.mjs both", + "mint:tenant": "node mintAndApprove.mjs accountId=user-alice", + "deposit:tenant": "node mintAndApprove.mjs deposit accountId=user-alice", + "mint-and-deposit:tenant": "node mintAndApprove.mjs both accountId=user-alice", + "get-commitments": "node mintAndApprove.mjs commitments", + "get-commitments:tenant": "node mintAndApprove.mjs commitments accountId=user-alice", + "transfer": "node transferNft.mjs", + "transfer:direct": "node transferNft.mjs direct", + "transfer:tenant": "node transferNft.mjs accountId=user-alice", + "transfer:example": "node transferNft.mjs 3 to=0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199", + "get-receiver-commitments": "node transferNft.mjs commitments to=0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199" }, "bin": { "zappify": "bin/index.mjs" @@ -98,6 +111,7 @@ "@types/mocha": "^9.1.0", "@types/node": "^17.0.19", "@types/prettier": "^2.4.4", + "ethers": "^5.7.2", "chai-as-promised": "^7.1.1", "chai-http": "^4.3.0", "eslint": "^8.2.0", diff --git a/src/boilerplate/common/commitment-storage.mjs b/src/boilerplate/common/commitment-storage.mjs index 6a337478..e422bab6 100644 --- a/src/boilerplate/common/commitment-storage.mjs +++ b/src/boilerplate/common/commitment-storage.mjs @@ -16,7 +16,7 @@ import { KeyManager } from './key-management/KeyManager.mjs'; const { MONGO_URL, COMMITMENTS_DB, COMMITMENTS_COLLECTION } = config; const { generalise } = gen; -export function formatCommitment (commitment) { +export function formatCommitment (commitment, context) { let data try { const nullifierHash = commitment.secretKey @@ -38,9 +38,10 @@ export function formatCommitment (commitment) { secretKey: commitment.secretKey ? commitment.secretKey.hex(32) : null, preimage, isNullified: commitment.isNullified, - nullifier: commitment.secretKey ? nullifierHash.hex(32) : null + nullifier: commitment.secretKey ? nullifierHash.hex(32) : null, + accountId: context?.accountId || null, } - logger.debug(`Storing commitment ${data._id}`) + logger.debug(`Storing commitment ${data._id}${context?.accountId ? ` for accountId: ${context.accountId}` : ''}`) } catch (error) { console.error('Error --->', error) } @@ -53,8 +54,8 @@ export async function persistCommitment (data) { return db.collection(COMMITMENTS_COLLECTION).insertOne(data) } // function to format a commitment for a mongo db and store it -export async function storeCommitment (commitment) { - const data = formatCommitment(commitment) +export async function storeCommitment (commitment, context) { + const data = formatCommitment(commitment, context) return persistCommitment(data) } @@ -155,12 +156,13 @@ export async function getBalanceByState(name, mappingKey = null) { /** * @returns all the commitments existent in this database. */ -export async function getAllCommitments() { +export async function getAllCommitments(accountId) { const connection = await mongo.connection(MONGO_URL); const db = connection.db(COMMITMENTS_DB); + const query = accountId ? { accountId } : {}; const allCommitments = await db .collection(COMMITMENTS_COLLECTION) - .find() + .find(query) .toArray(); return allCommitments; } diff --git a/src/boilerplate/common/services/saas-key-services.mjs b/src/boilerplate/common/services/saas-key-services.mjs new file mode 100644 index 00000000..199f8175 --- /dev/null +++ b/src/boilerplate/common/services/saas-key-services.mjs @@ -0,0 +1,150 @@ +/* eslint-disable prettier/prettier, camelcase, prefer-const, no-unused-vars */ +import config from "config"; +import logger from "./common/logger.mjs"; +import { + getOrCreateKeys, + extractAccountId, + getKeysFromDB, + storeKeysInDB, + generateKeyPair +} from "./common/key-management.mjs"; + +export async function service_getSharedKeys(req, res) { + try { + const accountId = extractAccountId(req); + const { targetAccountId } = req.body; + + if (!targetAccountId) { + return res.status(400).send({ error: 'targetAccountId is required' }); + } + + let sharedKeys = {}; + + if (accountId) { + // SaaS mode - get keys from database + const currentUserKeys = await getKeysFromDB(accountId); + const targetUserKeys = await getKeysFromDB(targetAccountId); + + if (!currentUserKeys || !targetUserKeys) { + return res.status(404).send({ error: 'Keys not found for one or both accounts' }); + } + + sharedKeys = { + currentUserPublicKey: currentUserKeys.publicKey, + targetUserPublicKey: targetUserKeys.publicKey, + sharedPublicKey: currentUserKeys.sharedPublicKey, + sharedSecretKey: currentUserKeys.sharedSecretKey, + }; + } else { + // File mode - return error as shared keys require multi-user context + return res.status(400).send({ error: 'Shared keys require SaaS context (multi-user mode)' }); + } + + res.send({ sharedKeys }); + } catch (err) { + logger.error('Error getting shared keys:', err); + res.status(500).send({ error: err.message }); + } +} + +/** + * Service to rotate keys for a user + */ +export async function service_rotateKeys(req, res) { + try { + const accountId = extractAccountId(req); + const { contractName, registerWithContract = false } = req.body; + + if (!accountId) { + return res.status(400).send({ error: 'SaaS context required for key rotation' }); + } + + // Generate new keys + const newKeys = generateKeyPair(); + + // Store new keys + await storeKeysInDB(accountId, newKeys); + + // Register with contract if requested + if (registerWithContract && contractName) { + const { registerKeyWithContract } = await import('./common/key-management.mjs'); + await registerKeyWithContract(newKeys.publicKey, contractName); + } + + res.send({ + message: 'Keys rotated successfully', + publicKey: newKeys.publicKey, + sharedPublicKey: newKeys.sharedPublicKey + }); + } catch (err) { + logger.error('Error rotating keys:', err); + res.status(500).send({ error: err.message }); + } +} + +/** + * Service to get current user's public keys + */ +export async function service_getUserKeys(req, res) { + try { + const accountId = extractAccountId(req); + + if (!accountId) { + return res.status(400).send({ error: 'SaaS context required' }); + } + + const keys = await getKeysFromDB(accountId); + + if (!keys) { + return res.status(404).send({ error: 'Keys not found' }); + } + + // Only return public keys for security + const publicKeys = { + publicKey: keys.publicKey, + sharedPublicKey: keys.sharedPublicKey, + accountId: accountId, + }; + + res.send({ keys: publicKeys }); + } catch (err) { + logger.error('Error getting user keys:', err); + res.status(500).send({ error: err.message }); + } +} + +/** + * Service to initialize keys for a new user + */ +export async function service_initializeUserKeys(req, res) { + try { + const accountId = extractAccountId(req); + const { contractName, registerWithContract = false } = req.body; + + if (!accountId) { + return res.status(400).send({ error: 'SaaS context required for user key initialization' }); + } + + // Check if keys already exist + const existingKeys = await getKeysFromDB(accountId); + if (existingKeys) { + return res.status(409).send({ + error: 'Keys already exist for this account', + publicKey: existingKeys.publicKey, + sharedPublicKey: existingKeys.sharedPublicKey + }); + } + + // Get or create keys (will create new ones since they don't exist) + const keys = await getOrCreateKeys(accountId, contractName, registerWithContract); + + res.send({ + message: 'Keys initialized successfully', + publicKey: keys.publicKey, + sharedPublicKey: keys.sharedPublicKey + }); + } catch (err) { + logger.error('Error initializing user keys:', err); + res.status(500).send({ error: err.message }); + } +} diff --git a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts index ebe4ca78..26ee89b8 100644 --- a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts +++ b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts @@ -660,7 +660,7 @@ sendTransaction = { }, secretKey: ${stateName}_newOwnerPublicKey.integer === ${isSharedSecret ? `sharedPublicKey.integer` : `publicKey.integer`} ? ${isSharedSecret ? `sharedSecretKey` : `secretKey`}: null, isNullified: false, - });` + errorCatch]; + }SAAS_CONTEXT_PARAM);` + errorCatch]; case 'decrement': value = structProperties ? `{ ${structProperties.map((p, i) => `${p}: ${stateName}_change.integer[${i}]`)} }` : `${stateName}_change`; return [` @@ -679,7 +679,7 @@ sendTransaction = { }, secretKey: ${stateName}_newOwnerPublicKey.integer === ${isSharedSecret ? `sharedPublicKey.integer` : `publicKey.integer`} ? ${isSharedSecret ? `sharedSecretKey` : `secretKey`}: null, isNullified: false, - });`+ errorCatch]; + }SAAS_CONTEXT_PARAM);`+ errorCatch]; case 'whole': switch (burnedOnly) { case true: @@ -703,7 +703,7 @@ sendTransaction = { }, secretKey: ${stateName}_newOwnerPublicKey.integer === ${isSharedSecret ? `sharedPublicKey.integer` : `publicKey.integer`} ? ${isSharedSecret ? `sharedSecretKey` : `secretKey`}: null, isNullified: false, - });` + errorCatch]; + }SAAS_CONTEXT_PARAM);` + errorCatch]; } default: throw new TypeError(stateType); @@ -817,7 +817,8 @@ integrationApiServicesBoilerplate = { return ` export async function service_allCommitments(req, res, next) { try { - const commitments = await getAllCommitments(); + const accountId = req.saasContext?.accountId; + const commitments = await getAllCommitments(accountId); res.send({ commitments }); await sleep(10); } catch (err) { From 14801b3e869fbe7a8400dcf0e983788f757e0030 Mon Sep 17 00:00:00 2001 From: Adarsh Ron Date: Thu, 16 Oct 2025 16:19:15 +0530 Subject: [PATCH 03/18] chore: removed the dummy logs --- mintAndApprove.mjs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/mintAndApprove.mjs b/mintAndApprove.mjs index 0d8a37f1..a7cb678f 100644 --- a/mintAndApprove.mjs +++ b/mintAndApprove.mjs @@ -300,15 +300,6 @@ async function depositToShield() { console.error('Error data:', error.data); } - // Provide troubleshooting guidance for common API errors - console.log('\n--- Troubleshooting Tips ---'); - console.log('1. Ensure the NFT Escrow Zapp API is running (on port 3000)'); - console.log('2. Check that the token is properly minted and approved'); - console.log('3. The correct API endpoint is /deposit based on the Zapp router configuration'); - console.log('4. The API expects only tokenId in the request body'); - console.log('5. For multi-tenant setups, make sure to include a valid accountId (accountId=uuid)'); - console.log('6. You can also try using the Zapp UI at http://localhost:3000'); - process.exit(1); } } From d419d52b8c504261a1187959186eaeab792b0b39 Mon Sep 17 00:00:00 2001 From: Adarsh Ron Date: Fri, 17 Oct 2025 14:30:00 +0530 Subject: [PATCH 04/18] fix: fixed issue with compilation --- src/boilerplate/common/boilerplate-docker-compose.yml | 2 ++ .../common/services/generic-api_services.mjs | 2 +- .../common/services/generic-read-only-api_services.mjs | 2 +- .../common/services/genericpublic-api_services.mjs | 2 +- .../javascript/raw/boilerplate-generator.ts | 10 +++++----- .../orchestration/files/toOrchestration.ts | 4 ++-- test/contracts/user-friendly-tests/NFT_Escrow.zol | 2 +- 7 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/boilerplate/common/boilerplate-docker-compose.yml b/src/boilerplate/common/boilerplate-docker-compose.yml index f6ccd0d2..8daecedf 100644 --- a/src/boilerplate/common/boilerplate-docker-compose.yml +++ b/src/boilerplate/common/boilerplate-docker-compose.yml @@ -114,6 +114,8 @@ services: - zapp-commitment-volume:/data/db networks: - zapp_network + ports: + - '27017:27017' ganache: image: ethereumoptimism/hardhat-node diff --git a/src/boilerplate/common/services/generic-api_services.mjs b/src/boilerplate/common/services/generic-api_services.mjs index 6d5a5b69..c118feef 100644 --- a/src/boilerplate/common/services/generic-api_services.mjs +++ b/src/boilerplate/common/services/generic-api_services.mjs @@ -39,7 +39,7 @@ export class ServiceManager{ await startEventFilter('CONTRACT_NAME'); const FUNCTION_SIG; SAAS_CONTEXT_HANDLING - const { tx , encEvent, encBackupEvent, _RESPONSE_} = await this.FUNCTION_NAME.FUNCTION_NAME(FUNCTION_SIG SAAS_CONTEXT_PARAM); + const { tx , encEvent, encBackupEvent, _RESPONSE_} = await this.FUNCTION_NAME.FUNCTION_NAME(FUNCTION_SIG, SAAS_CONTEXT_PARAM); // prints the tx console.log(tx); const txSerialized = serializeBigInt(tx); diff --git a/src/boilerplate/common/services/generic-read-only-api_services.mjs b/src/boilerplate/common/services/generic-read-only-api_services.mjs index 8f79f32f..603c5b5c 100644 --- a/src/boilerplate/common/services/generic-read-only-api_services.mjs +++ b/src/boilerplate/common/services/generic-read-only-api_services.mjs @@ -39,7 +39,7 @@ export class ServiceManager{ await startEventFilter('CONTRACT_NAME'); const FUNCTION_SIG; SAAS_CONTEXT_HANDLING - const { _RESPONSE_} = await this.FUNCTION_NAME.FUNCTION_NAME(FUNCTION_SIG SAAS_CONTEXT_PARAM); + const { _RESPONSE_} = await this.FUNCTION_NAME.FUNCTION_NAME(FUNCTION_SIG, SAAS_CONTEXT_PARAM); res.send({_RESPONSE_ }); await sleep(10); } catch (err) { diff --git a/src/boilerplate/common/services/genericpublic-api_services.mjs b/src/boilerplate/common/services/genericpublic-api_services.mjs index dea5775e..02d81515 100644 --- a/src/boilerplate/common/services/genericpublic-api_services.mjs +++ b/src/boilerplate/common/services/genericpublic-api_services.mjs @@ -12,7 +12,7 @@ let encryption = {}; async service_FUNCTION_NAME (req, res, next){ const FUNCTION_SIG; SAAS_CONTEXT_HANDLING - const { tx , _RESPONSE_} = await this.FUNCTION_NAME.FUNCTION_NAME(FUNCTION_SIG SAAS_CONTEXT_PARAM); + const { tx , _RESPONSE_} = await this.FUNCTION_NAME.FUNCTION_NAME(FUNCTION_SIG, SAAS_CONTEXT_PARAM); // prints the tx console.log(tx); const txSerialized = serializeBigInt(tx); diff --git a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts index 26ee89b8..e2630445 100644 --- a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts +++ b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts @@ -660,7 +660,7 @@ sendTransaction = { }, secretKey: ${stateName}_newOwnerPublicKey.integer === ${isSharedSecret ? `sharedPublicKey.integer` : `publicKey.integer`} ? ${isSharedSecret ? `sharedSecretKey` : `secretKey`}: null, isNullified: false, - }SAAS_CONTEXT_PARAM);` + errorCatch]; + }, SAAS_CONTEXT_PARAM);` + errorCatch]; case 'decrement': value = structProperties ? `{ ${structProperties.map((p, i) => `${p}: ${stateName}_change.integer[${i}]`)} }` : `${stateName}_change`; return [` @@ -679,7 +679,7 @@ sendTransaction = { }, secretKey: ${stateName}_newOwnerPublicKey.integer === ${isSharedSecret ? `sharedPublicKey.integer` : `publicKey.integer`} ? ${isSharedSecret ? `sharedSecretKey` : `secretKey`}: null, isNullified: false, - }SAAS_CONTEXT_PARAM);`+ errorCatch]; + }, SAAS_CONTEXT_PARAM);`+ errorCatch]; case 'whole': switch (burnedOnly) { case true: @@ -703,7 +703,7 @@ sendTransaction = { }, secretKey: ${stateName}_newOwnerPublicKey.integer === ${isSharedSecret ? `sharedPublicKey.integer` : `publicKey.integer`} ? ${isSharedSecret ? `sharedSecretKey` : `secretKey`}: null, isNullified: false, - }SAAS_CONTEXT_PARAM);` + errorCatch]; + }, SAAS_CONTEXT_PARAM);` + errorCatch]; } default: throw new TypeError(stateType); @@ -877,7 +877,7 @@ integrationApiServicesBoilerplate = { try { const { name } = req.body; SAAS_CONTEXT_HANDLING - await backupVariable(name SAAS_CONTEXT_PARAM); + await backupVariable(name, SAAS_CONTEXT_PARAM); res.send("Complete"); await sleep(10); } catch (err) { @@ -890,7 +890,7 @@ integrationApiServicesBoilerplate = { const { recipientAddress } = req.body; const recipientPubKey = req.body.recipientPubKey || 0 SAAS_CONTEXT_HANDLING - const SharedKeys = await getSharedSecretskeys(recipientAddress, recipientPubKey SAAS_CONTEXT_PARAM); + const SharedKeys = await getSharedSecretskeys(recipientAddress, recipientPubKey, SAAS_CONTEXT_PARAM); res.send({ SharedKeys }); await sleep(10); } catch (err) { diff --git a/src/codeGenerators/orchestration/files/toOrchestration.ts b/src/codeGenerators/orchestration/files/toOrchestration.ts index 05a82c23..267db8c2 100644 --- a/src/codeGenerators/orchestration/files/toOrchestration.ts +++ b/src/codeGenerators/orchestration/files/toOrchestration.ts @@ -224,7 +224,7 @@ const prepareIntegrationApiServices = (node: any) => { fnboilerplate = fnboilerplate.replace( /SAAS_CONTEXT_PARAM/g, - node.multiTenant ? `, context` : ``, + node.multiTenant ? `context` : ``, ); // replace function imports at top of file @@ -248,7 +248,7 @@ const prepareIntegrationApiServices = (node: any) => { ); commitmentsCode = commitmentsCode.replace( /SAAS_CONTEXT_PARAM/g, - node.multiTenant ? `, context` : ``, + node.multiTenant ? `context` : ``, ); commitmentsCode = commitmentsCode.replace( /SAAS_CONTEXT_DIRECT/g, diff --git a/test/contracts/user-friendly-tests/NFT_Escrow.zol b/test/contracts/user-friendly-tests/NFT_Escrow.zol index 7f80ea03..1d07cd95 100644 --- a/test/contracts/user-friendly-tests/NFT_Escrow.zol +++ b/test/contracts/user-friendly-tests/NFT_Escrow.zol @@ -23,7 +23,7 @@ contract NFT_Escrow { function transfer(secret address recipient, secret uint256 tokenId) public { require(tokenOwners[tokenId] == msg.sender); require(recipient != address(0), "NFT_Escrow: transfer to the zero address"); - tokenOwners[tokenId] = recipient; + encrypt tokenOwners[tokenId] = recipient; } function approve(secret address approvedAddress) public { From 320ad1efe289ed90311089d613503e4b9f6b1230 Mon Sep 17 00:00:00 2001 From: Adarsh Ron Date: Fri, 17 Oct 2025 15:06:54 +0530 Subject: [PATCH 05/18] chore: sample mint deposit script --- mintAndApprove.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mintAndApprove.mjs b/mintAndApprove.mjs index a7cb678f..648609df 100644 --- a/mintAndApprove.mjs +++ b/mintAndApprove.mjs @@ -12,7 +12,7 @@ const __dirname = path.dirname(__filename); // Parse arguments properly whether run directly or via npm run function parseArgs() { const args = process.argv.slice(2); - let tokenId = 3; + let tokenId = 2; let rpcUrl = 'http://localhost:8545'; let action = 'mint'; let accountId = null; // For multi-tenant setup From 083236bb3f6429c11a8e15f41a246f40527efd34 Mon Sep 17 00:00:00 2001 From: Adarsh Ron Date: Wed, 22 Oct 2025 09:52:08 +0530 Subject: [PATCH 06/18] fix: fixed issues with commitment creation --- .../orchestration/files/toOrchestration.ts | 12 +++++++++--- src/transformers/visitors/toOrchestrationVisitor.ts | 8 ++++++++ src/types/orchestration-types.ts | 3 ++- test/contracts/user-friendly-tests/NFT_Escrow.zol | 2 +- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/codeGenerators/orchestration/files/toOrchestration.ts b/src/codeGenerators/orchestration/files/toOrchestration.ts index 267db8c2..75e9a24f 100644 --- a/src/codeGenerators/orchestration/files/toOrchestration.ts +++ b/src/codeGenerators/orchestration/files/toOrchestration.ts @@ -224,7 +224,7 @@ const prepareIntegrationApiServices = (node: any) => { fnboilerplate = fnboilerplate.replace( /SAAS_CONTEXT_PARAM/g, - node.multiTenant ? `context` : ``, + node.multiTenant ? `context` : `undefined`, ); // replace function imports at top of file @@ -248,7 +248,7 @@ const prepareIntegrationApiServices = (node: any) => { ); commitmentsCode = commitmentsCode.replace( /SAAS_CONTEXT_PARAM/g, - node.multiTenant ? `context` : ``, + node.multiTenant ? `context` : `undefined`, ); commitmentsCode = commitmentsCode.replace( /SAAS_CONTEXT_DIRECT/g, @@ -1074,13 +1074,19 @@ export default function fileGenerator(node: any) { .flatMap(fileGenerator)); case 'File': + let fileContent = node.nodes.map(codeGenerator).join(''); + + fileContent = fileContent.replace( + /SAAS_CONTEXT_PARAM/g, + node.multiTenant ? `context` : `undefined`, + ); return [ { filepath: path.join( `./orchestration`, `${node.fileName}${node.fileExtension}`, ), - file: node.nodes.map(codeGenerator).join(''), + file: fileContent, }, ]; // case 'ImportStatementList': diff --git a/src/transformers/visitors/toOrchestrationVisitor.ts b/src/transformers/visitors/toOrchestrationVisitor.ts index a301e0c0..2cb48a9e 100644 --- a/src/transformers/visitors/toOrchestrationVisitor.ts +++ b/src/transformers/visitors/toOrchestrationVisitor.ts @@ -376,6 +376,7 @@ const visitor = { const newNode = buildNode('File', { fileName: 'test', fileExtension: '.mjs', + multiTenant: state.multiTenant, nodes: [ buildNode('IntegrationTestBoilerplate', { contractName, @@ -393,6 +394,7 @@ const visitor = { newNode = buildNode('File', { fileName: 'api_services', fileExtension: '.mjs', + multiTenant: state.multiTenant, nodes: [ buildNode('IntegrationApiServicesBoilerplate', { contractName, @@ -405,6 +407,7 @@ const visitor = { newNode = buildNode('File', { fileName: 'api_routes', fileExtension: '.mjs', + multiTenant: state.multiTenant, nodes: [ buildNode('IntegrationApiRoutesBoilerplate', { contractName, @@ -416,6 +419,7 @@ const visitor = { newNode = buildNode('File', { fileName: 'BackupDataRetriever', fileExtension: '.mjs', + multiTenant: state.multiTenant, nodes: [ buildNode('BackupDataRetrieverBoilerplate', { contractName, @@ -427,6 +431,7 @@ const visitor = { newNode = buildNode('File', { fileName: 'BackupVariable', fileExtension: '.mjs', + multiTenant: state.multiTenant, nodes: [ buildNode('BackupVariableBoilerplate', { contractName, @@ -439,6 +444,7 @@ const visitor = { newNode = buildNode('File', { fileName: 'encrypted-data-listener', fileExtension: '.mjs', + multiTenant: state.multiTenant, nodes: [ buildNode('IntegrationEncryptedListenerBoilerplate', { contractName, @@ -499,6 +505,7 @@ const visitor = { const newNode = buildNode('File', { fileName: fnName, // the name of this function fileExtension: '.mjs', + multiTenant: state.multiTenant, nodes: [ buildNode('Imports'), buildNode('FunctionDefinition', { name: node.name, contractName, stateMutability: node.stateMutability}), @@ -546,6 +553,7 @@ const visitor = { const newNode = buildNode('File', { fileName: fnName, fileExtension: '.mjs', + multiTenant: state.multiTenant, nodes: [ buildNode('Imports'), buildNode('FunctionDefinition', { name: node.name, contractName, stateMutability: node.stateMutability }), diff --git a/src/types/orchestration-types.ts b/src/types/orchestration-types.ts index 60e071e7..e4eef7d3 100644 --- a/src/types/orchestration-types.ts +++ b/src/types/orchestration-types.ts @@ -9,12 +9,13 @@ import { buildBoilerplateNode } from '../boilerplate/orchestration/javascript/no export default function buildNode(nodeType: string, fields: any = {}): any { switch (nodeType) { case 'File': { - const { fileName, fileExtension = '.mjs', nodes = [] } = fields; + const { fileName, fileExtension = '.mjs', nodes = [], multiTenant } = fields; return { nodeType, fileName, fileExtension, nodes, + multiTenant, }; } case 'Imports': { diff --git a/test/contracts/user-friendly-tests/NFT_Escrow.zol b/test/contracts/user-friendly-tests/NFT_Escrow.zol index 1d07cd95..7f80ea03 100644 --- a/test/contracts/user-friendly-tests/NFT_Escrow.zol +++ b/test/contracts/user-friendly-tests/NFT_Escrow.zol @@ -23,7 +23,7 @@ contract NFT_Escrow { function transfer(secret address recipient, secret uint256 tokenId) public { require(tokenOwners[tokenId] == msg.sender); require(recipient != address(0), "NFT_Escrow: transfer to the zero address"); - encrypt tokenOwners[tokenId] = recipient; + tokenOwners[tokenId] = recipient; } function approve(secret address approvedAddress) public { From 4c02d100bfb063363909cfd4bcb0f4c194868b8c Mon Sep 17 00:00:00 2001 From: Adarsh Ron Date: Tue, 4 Nov 2025 12:06:57 +0530 Subject: [PATCH 07/18] feat: added l1 key management --- .../common/boilerplate-docker-compose.yml | 2 +- src/boilerplate/common/commitment-storage.mjs | 23 +- src/boilerplate/common/config/default.js | 2 + .../common/encrypted-data-listener.mjs | 6 +- src/boilerplate/common/gas-funding.mjs | 160 ++++++++++++ .../key-management/DatabaseKeyStorage.mjs | 53 +++- .../common/key-management/FileKeyStorage.mjs | 5 + .../common/key-management/IKeyStorage.mjs | 10 +- .../common/key-management/KeyManager.mjs | 9 + src/boilerplate/common/timber.mjs | 57 ++++- .../raw/ContractBoilerplateGenerator.ts | 3 +- .../javascript/nodes/boilerplate-generator.ts | 3 +- .../javascript/raw/boilerplate-generator.ts | 231 +++++++++++++++++- .../javascript/raw/toOrchestration.ts | 18 +- .../orchestration/files/toOrchestration.ts | 5 +- src/transformers/visitors/common.ts | 1 + src/traverse/Indicator.ts | 2 + 17 files changed, 548 insertions(+), 42 deletions(-) create mode 100644 src/boilerplate/common/gas-funding.mjs diff --git a/src/boilerplate/common/boilerplate-docker-compose.yml b/src/boilerplate/common/boilerplate-docker-compose.yml index 8daecedf..29dd646a 100644 --- a/src/boilerplate/common/boilerplate-docker-compose.yml +++ b/src/boilerplate/common/boilerplate-docker-compose.yml @@ -58,7 +58,7 @@ services: timber: build: - context: https://github.com/EYBlockchain/timber.git#starlight/zscaler:merkle-tree + context: https://ghcr.io/eyblockchain/timber-multicontract:latest dockerfile: Dockerfile restart: on-failure depends_on: diff --git a/src/boilerplate/common/commitment-storage.mjs b/src/boilerplate/common/commitment-storage.mjs index e422bab6..510eb48f 100644 --- a/src/boilerplate/common/commitment-storage.mjs +++ b/src/boilerplate/common/commitment-storage.mjs @@ -71,13 +71,18 @@ export async function getCommitmentsById(id) { } // function to retrieve commitment with a specified stateVarId -export async function getCurrentWholeCommitment(id) { +export async function getCurrentWholeCommitment(id, accountId) { const connection = await mongo.connection(MONGO_URL); const db = connection.db(COMMITMENTS_DB); - const commitment = await db.collection(COMMITMENTS_COLLECTION).findOne({ + const query = { 'preimage.stateVarId': generalise(id).hex(32), isNullified: false, - }); + }; + + if (accountId) { + query.accountId = accountId; + } + const commitment = await db.collection(COMMITMENTS_COLLECTION).findOne(query); return commitment; } @@ -326,6 +331,7 @@ export async function joinCommitments( instance, contractAddr, web3, + context, ) { logger.warn( 'Existing Commitments are not appropriate and we need to call Join Commitment Circuit. It will generate proof to join commitments, this will require an on-chain verification', @@ -433,6 +439,13 @@ export async function joinCommitments( .flat(Infinity); // Send transaction to the blockchain: + // Get tenant-specific keys + const keyManager = KeyManager.getInstance(); + const keys = await keyManager.getKeys(context); + if (!keys || !keys.ethPK || !keys.ethSK) { + throw new Error('Tenant Ethereum keys not found. Please register keys first.'); + } + const txData = await instance.methods .joinCommitments( [oldCommitment_0_nullifier.integer, oldCommitment_1_nullifier.integer], @@ -443,7 +456,7 @@ export async function joinCommitments( .encodeABI(); let txParams = { - from: config.web3.options.defaultAccount, + from: keys.ethPK, to: contractAddr, gas: config.web3.options.defaultGas, gasPrice: config.web3.options.defaultGasPrice, @@ -451,7 +464,7 @@ export async function joinCommitments( chainId: await web3.eth.net.getId(), }; - const key = config.web3.key; + const key = keys.ethSK; const signed = await web3.eth.accounts.signTransaction(txParams, key); diff --git a/src/boilerplate/common/config/default.js b/src/boilerplate/common/config/default.js index eb9732d0..3f7dc090 100644 --- a/src/boilerplate/common/config/default.js +++ b/src/boilerplate/common/config/default.js @@ -6,6 +6,8 @@ module.exports = { }, merkleTree: { url: process.env.TIMBER_URL || 'http://timber:80', + defaultMaxTries: parseInt(process.env.TIMBER_MAX_TRIES || '40', 10), // 40 tries × 3s = 2 minutes + retryDelay: parseInt(process.env.TIMBER_RETRY_DELAY || '3000', 10), // 3 seconds between retries }, // merkle-tree stuff: ZERO: '0', diff --git a/src/boilerplate/common/encrypted-data-listener.mjs b/src/boilerplate/common/encrypted-data-listener.mjs index ad30e7ae..37219072 100644 --- a/src/boilerplate/common/encrypted-data-listener.mjs +++ b/src/boilerplate/common/encrypted-data-listener.mjs @@ -20,7 +20,7 @@ function decodeCommitmentData(decrypted){ export default class EncryptedDataEventListener { constructor(web3, context) { this.web3 = web3; - this.ethAddress = generalise(config.web3.options.defaultAccount); + this.ethAddress = null; this.contractMetadata = {}; this.context = context; } @@ -55,9 +55,11 @@ export default class EncryptedDataEventListener { this.secretKey = generalise(keys.secretKey); this.publicKey = generalise(keys.publicKey); + this.ethAddress = keys.ethPK ? generalise(keys.ethPK) : generalise(config.web3.options.defaultAccount); console.log('Keys loaded successfully', { - multiTenant: !!this.context?.accountId + multiTenant: !!this.context?.accountId, + ethAddress: this.ethAddress.hex(), }); } catch (error) { console.error( diff --git a/src/boilerplate/common/gas-funding.mjs b/src/boilerplate/common/gas-funding.mjs new file mode 100644 index 00000000..ea357568 --- /dev/null +++ b/src/boilerplate/common/gas-funding.mjs @@ -0,0 +1,160 @@ +import config from 'config'; +import logger from './logger.mjs'; +import Web3 from './web3.mjs'; + +export async function fundTenantAddress(tenantAddress, amountInEther) { + const web3 = Web3.connection(); + + // Validate inputs + if (!web3.utils.isAddress(tenantAddress)) { + throw new Error(`Invalid Ethereum address: ${tenantAddress}`); + } + + if (!amountInEther || parseFloat(amountInEther) <= 0) { + throw new Error(`Invalid funding amount: ${amountInEther}`); + } + + const deployerAccount = config.web3.options.defaultAccount; + const deployerKey = config.web3.key; + + if (!deployerAccount || !deployerKey) { + throw new Error('Deployer account not configured. Set DEFAULT_ACCOUNT and KEY environment variables.'); + } + + const deployerBalance = await web3.eth.getBalance(deployerAccount); + const deployerBalanceEth = web3.utils.fromWei(deployerBalance, 'ether'); + const requiredAmount = parseFloat(amountInEther); + + if (parseFloat(deployerBalanceEth) < requiredAmount) { + throw new Error( + `Insufficient deployer balance. Required: ${requiredAmount} ETH, Available: ${deployerBalanceEth} ETH` + ); + } + + logger.info(`Funding tenant address ${tenantAddress} with ${amountInEther} ETH...`); + logger.debug(`Deployer account: ${deployerAccount}, Balance: ${deployerBalanceEth} ETH`); + + try { + const amountInWei = web3.utils.toWei(amountInEther, 'ether'); + + // Get current gas price + const gasPrice = await web3.eth.getGasPrice(); + + // Estimate gas for the transaction + const gasEstimate = await web3.eth.estimateGas({ + from: deployerAccount, + to: tenantAddress, + value: amountInWei, + }); + + // Build transaction parameters + const txParams = { + from: deployerAccount, + to: tenantAddress, + value: amountInWei, + gas: gasEstimate, + gasPrice: gasPrice, + chainId: await web3.eth.net.getId(), + }; + + // Sign transaction with deployer's private key + const signedTx = await web3.eth.accounts.signTransaction(txParams, deployerKey); + + // Send signed transaction + const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction); + + logger.info( + `Successfully funded ${tenantAddress} with ${amountInEther} ETH. Tx hash: ${receipt.transactionHash}` + ); + + return receipt; + } catch (error) { + logger.error(`Failed to fund tenant address ${tenantAddress}:`, error.message); + throw new Error(`Gas funding failed: ${error.message}`); + } +} + +export async function hasSufficientGas(tenantAddress, minimumBalanceInEther) { + const web3 = Web3.connection(); + + // Validate inputs + if (!web3.utils.isAddress(tenantAddress)) { + throw new Error(`Invalid Ethereum address: ${tenantAddress}`); + } + + if (!minimumBalanceInEther || parseFloat(minimumBalanceInEther) < 0) { + throw new Error(`Invalid minimum balance: ${minimumBalanceInEther}`); + } + + try { + // Get current balance + const balanceWei = await web3.eth.getBalance(tenantAddress); + const balanceEth = web3.utils.fromWei(balanceWei, 'ether'); + const minimumBalance = parseFloat(minimumBalanceInEther); + + const hasSufficient = parseFloat(balanceEth) >= minimumBalance; + + logger.debug( + `Address ${tenantAddress} balance: ${balanceEth} ETH (minimum: ${minimumBalance} ETH) - ${ + hasSufficient ? 'Sufficient' : 'Insufficient' + }` + ); + + return hasSufficient; + } catch (error) { + logger.error(`Failed to check balance for ${tenantAddress}:`, error.message); + throw new Error(`Balance check failed: ${error.message}`); + } +} + +export async function autoFundIfNeeded( + tenantAddress, + minimumBalanceInEther = '0.01', + fundAmountInEther = '0.1' +) { + logger.debug(`Checking if tenant address ${tenantAddress} needs gas funding...`); + + try { + // Check if address already has sufficient balance + const hasSufficient = await hasSufficientGas(tenantAddress, minimumBalanceInEther); + + if (hasSufficient) { + logger.debug(`Tenant address ${tenantAddress} already has sufficient gas. No funding needed.`); + return null; + } + + logger.info( + `Tenant address ${tenantAddress} has insufficient gas (< ${minimumBalanceInEther} ETH). Auto-funding with ${fundAmountInEther} ETH...` + ); + + const receipt = await fundTenantAddress(tenantAddress, fundAmountInEther); + + logger.info(`Auto-funding complete for ${tenantAddress}. Ready to send transactions!`); + + return receipt; + } catch (error) { + logger.error(`Auto-funding failed for ${tenantAddress}:`, error.message); + throw new Error(`Auto-funding failed: ${error.message}`); + } +} + +export async function getTenantBalance(tenantAddress) { + const web3 = Web3.connection(); + + if (!web3.utils.isAddress(tenantAddress)) { + throw new Error(`Invalid Ethereum address: ${tenantAddress}`); + } + + const balanceWei = await web3.eth.getBalance(tenantAddress); + const balanceEth = web3.utils.fromWei(balanceWei, 'ether'); + + return balanceEth; +} + +export default { + fundTenantAddress, + hasSufficientGas, + autoFundIfNeeded, + getTenantBalance, +}; + diff --git a/src/boilerplate/common/key-management/DatabaseKeyStorage.mjs b/src/boilerplate/common/key-management/DatabaseKeyStorage.mjs index d297710f..0065fd65 100644 --- a/src/boilerplate/common/key-management/DatabaseKeyStorage.mjs +++ b/src/boilerplate/common/key-management/DatabaseKeyStorage.mjs @@ -105,6 +105,8 @@ export class DatabaseKeyStorage extends IKeyStorage { const keys = { secretKey: decryptIfEncrypted(doc.secretKey), publicKey: doc.publicKey, // Public key doesn't need decryption + ethSK: doc.ethSK ? decryptIfEncrypted(doc.ethSK) : null, // Ethereum private key (encrypted) + ethPK: doc.ethPK || null, }; if (doc.sharedSecretKey) { @@ -144,6 +146,14 @@ export class DatabaseKeyStorage extends IKeyStorage { updatedAt: now, }; + // Include Ethereum keys if present + if (keys.ethSK) { + doc.ethSK = encryptIfEnabled(keys.ethSK); + } + if (keys.ethPK) { + doc.ethPK = keys.ethPK; + } + // Include optional shared keys if present if (keys.sharedSecretKey) { doc.sharedSecretKey = encryptIfEnabled(keys.sharedSecretKey); @@ -208,11 +218,22 @@ export class DatabaseKeyStorage extends IKeyStorage { publicKey = compressStarlightKey(publicKeyPoint); } + const Web3 = await import('../web3.mjs'); + const web3 = Web3.default.connection(); + const ethAccount = web3.eth.accounts.create(); + const ethSK = ethAccount.privateKey; + const ethPK = ethAccount.address; + + logger.info(`Generated Ethereum address for tenant ${context.accountId}: ${ethPK}`); + + // AUTO-FUND tenant address with gas + const { autoFundIfNeeded } = await import('../gas-funding.mjs'); + await autoFundIfNeeded(ethPK, '0.1', '0.5'); + logger.info(`Auto-funded tenant address ${ethPK} with gas. Ready to send transactions!`); + // Register on-chain if requested if (registerWithContract) { const { getContractInstance, getContractAddress } = await import('../contract.mjs'); - const Web3 = await import('../web3.mjs'); - const web3 = Web3.default.connection(); const instance = await getContractInstance(contractName); const contractAddr = await getContractAddress(contractName); @@ -221,7 +242,7 @@ export class DatabaseKeyStorage extends IKeyStorage { .encodeABI(); const txParams = { - from: config.web3.options.defaultAccount, + from: ethPK, to: contractAddr, gas: config.web3.options.defaultGas, gasPrice: config.web3.options.defaultGasPrice, @@ -229,8 +250,7 @@ export class DatabaseKeyStorage extends IKeyStorage { chainId: await web3.eth.net.getId(), }; - const key = config.web3.key; - const signed = await web3.eth.accounts.signTransaction(txParams, key); + const signed = await web3.eth.accounts.signTransaction(txParams, ethSK); await web3.eth.sendSignedTransaction(signed.rawTransaction); logger.info(`Key registered on-chain for accountId: ${context.accountId}`); } @@ -239,6 +259,8 @@ export class DatabaseKeyStorage extends IKeyStorage { await this.saveKeys({ secretKey: secretKey.integer, publicKey: publicKey.integer, + ethSK, + ethPK, }, context); // Update metadata @@ -363,6 +385,27 @@ export class DatabaseKeyStorage extends IKeyStorage { throw new Error(`Failed to delete keys: ${error.message}`); } } + + async getAccountIdByEthAddress(ethAddress) { + try { + const collection = await this.getCollection(); + const doc = await collection.findOne( + { ethPK: ethAddress }, + { projection: { accountId: 1 } } + ); + + if (!doc) { + logger.debug(`No accountId found for Ethereum address: ${ethAddress}`); + return null; + } + + logger.debug(`Found accountId ${doc.accountId} for Ethereum address ${ethAddress}`); + return doc.accountId; + } catch (error) { + logger.error(`Error looking up accountId for Ethereum address ${ethAddress}:`, error); + throw new Error(`Failed to lookup accountId: ${error.message}`); + } + } } export default DatabaseKeyStorage; diff --git a/src/boilerplate/common/key-management/FileKeyStorage.mjs b/src/boilerplate/common/key-management/FileKeyStorage.mjs index 7be63bdc..eb66de51 100644 --- a/src/boilerplate/common/key-management/FileKeyStorage.mjs +++ b/src/boilerplate/common/key-management/FileKeyStorage.mjs @@ -253,6 +253,11 @@ export class FileKeyStorage extends IKeyStorage { throw new Error(`Failed to delete key file: ${error.message}`); } } + + async getAccountIdByEthAddress(ethAddress) { + logger.debug(`getAccountIdByEthAddress not supported in single-tenant mode (address: ${ethAddress})`); + return null; + } } export default FileKeyStorage; diff --git a/src/boilerplate/common/key-management/IKeyStorage.mjs b/src/boilerplate/common/key-management/IKeyStorage.mjs index c84510b0..f650e0a1 100644 --- a/src/boilerplate/common/key-management/IKeyStorage.mjs +++ b/src/boilerplate/common/key-management/IKeyStorage.mjs @@ -11,8 +11,10 @@ /** * @typedef {Object} UserKeys - * @property {string} secretKey - User's secret key (as integer string) - * @property {string} publicKey - User's public key (as integer string) + * @property {string} secretKey - User's ZKP secret key (as integer string) + * @property {string} publicKey - User's ZKP public key (as integer string) + * @property {string} ethSK - User's Ethereum private key + * @property {string} ethPK - User's Ethereum address * @property {string} [sharedSecretKey] - Optional shared secret key for encrypted communication * @property {string} [sharedPublicKey] - Optional shared public key */ @@ -104,6 +106,10 @@ export class IKeyStorage { async deleteKeys(context) { throw new Error('deleteKeys() must be implemented by subclass'); } + + async getAccountIdByEthAddress(ethAddress) { + throw new Error('getAccountIdByEthAddress() must be implemented by subclass'); + } } export default IKeyStorage; diff --git a/src/boilerplate/common/key-management/KeyManager.mjs b/src/boilerplate/common/key-management/KeyManager.mjs index f93dece3..0ec5e2d2 100644 --- a/src/boilerplate/common/key-management/KeyManager.mjs +++ b/src/boilerplate/common/key-management/KeyManager.mjs @@ -197,6 +197,15 @@ export class KeyManager { accountId: context?.accountId || null, }; } + + async getAccountIdByEthAddress(ethAddress) { + try { + return await this.dbStorage.getAccountIdByEthAddress(ethAddress); + } catch (error) { + logger.error('KeyManager.getAccountIdByEthAddress failed:', error); + throw error; + } + } } /** diff --git a/src/boilerplate/common/timber.mjs b/src/boilerplate/common/timber.mjs index ada451db..3fab5a11 100644 --- a/src/boilerplate/common/timber.mjs +++ b/src/boilerplate/common/timber.mjs @@ -50,7 +50,16 @@ export const getLeafIndex = async ( let leafIndex; let errorCount = 0; const limit = - typeof maxTries === 'number' && !isNaN(maxTries) ? maxTries : 20; + typeof maxTries === 'number' && !isNaN(maxTries) + ? maxTries + : (config.merkleTree.defaultMaxTries || 40); + + // Track timing for performance monitoring + const startTime = Date.now(); + + let consecutiveNulls = 0; + const resyncThreshold = config.merkleTree?.resyncThreshold || 5; + while (errorCount < limit) { try { // eslint-disable-next-line no-await-in-loop @@ -71,20 +80,46 @@ export const getLeafIndex = async ( logger.http('Timber Response:', response.data.data); if (response.data.data !== null) { leafIndex = response.data.data.leafIndex; - if (leafIndex) break; + if (leafIndex) { + const elapsedMs = Date.now() - startTime; + const elapsedSec = (elapsedMs / 1000).toFixed(2); + if (errorCount === 0) { + logger.info(`Timber: Leaf already indexed for ${contractName} (leafIndex: ${leafIndex}, commitment: ${value.substring(0, 20)}...)`); + } else { + logger.info(`Timber: Leaf successfully indexed for ${contractName} after ${errorCount + 1} attempts in ${elapsedSec}s (leafIndex: ${leafIndex}, commitment: ${value.substring(0, 20)}...)`); + } + break; + } break; } else { + consecutiveNulls++; + + if (consecutiveNulls === resyncThreshold) { + try { + await getRoot(contractName, contractAddress); + logger.info("Timber resynced successfully"); + } catch (err) { + logger.warn(`Failed to trigger resync: ${err.message}`) + } + } throw new Error('leaf not found'); } } catch (err) { errorCount++; - logger.warn('Unable to get leaf - will try again in 3 seconds'); + const retryDelay = config.merkleTree.retryDelay || 3000; + logger.warn(`Unable to get leaf - will try again in ${retryDelay / 1000} seconds (attempt ${errorCount}/${limit})`); // eslint-disable-next-line no-await-in-loop await new Promise(resolve => { - setTimeout(() => resolve(), 3000); + setTimeout(() => resolve(), retryDelay); }); } } + + if (leafIndex === undefined) { + const elapsedMs = Date.now() - startTime; + const elapsedSec = (elapsedMs / 1000).toFixed(2); + logger.error(`Timber: Leaf NOT found for ${contractName} after ${errorCount} attempts in ${elapsedSec}s (commitment: ${value.substring(0, 20)}...)`); + } return leafIndex; }; export const getRoot = async (contractName, address) => { @@ -155,15 +190,25 @@ export const getSiblingPath = async (contractName, leafIndex, leafValue) => { } return siblingPath; }; -export const getMembershipWitness = async (contractName, leafValue) => { +export const getMembershipWitness = async (contractName, leafValue, maxTries) => { logger.http(`\nCalling getMembershipWitness for ${contractName} tree`); try { - const leafIndex = await getLeafIndex(contractName, leafValue); + const tries = typeof maxTries === 'number' && !isNaN(maxTries) + ? maxTries + : config.merkleTree?.defaultMaxTries; + const leafIndex = await getLeafIndex(contractName, leafValue, undefined, tries); + + if (undefined === leafIndex) { + const totalWaitTime = (tries * (config.merkleTree.retryDelay || 3000)) / 1000; + throw new Error(`Commitment not found in Timber after ${tries} attempts (${totalWaitTime}s total wait time)`) + } + let path = await getSiblingPath(contractName, leafIndex); const root = path[0].value; path = path.map(node => node.value); path.splice(0, 1); const witness = { index: leafIndex, path, root }; + logger.info("Membership witness generated successfully"); return witness; } catch (error) { throw new Error(error); diff --git a/src/boilerplate/contract/solidity/raw/ContractBoilerplateGenerator.ts b/src/boilerplate/contract/solidity/raw/ContractBoilerplateGenerator.ts index 911c7dbf..752d6f71 100644 --- a/src/boilerplate/contract/solidity/raw/ContractBoilerplateGenerator.ts +++ b/src/boilerplate/contract/solidity/raw/ContractBoilerplateGenerator.ts @@ -94,7 +94,8 @@ class ContractBoilerplateGenerator { ]; }, - + // NOTE: zkpPublicKeys[msg.sender] gets oevrwritten when each user regisyters + // That means only the last user will have their key mapped properly registerZKPPublicKey(): string[] { return [ ` diff --git a/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts b/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts index fe4703a7..031f6a56 100644 --- a/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts +++ b/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts @@ -246,11 +246,12 @@ export function buildPrivateStateNode(nodeType: string, fields: any = {}): any { export function buildBoilerplateNode(nodeType: string, fields: any = {}): any { switch (nodeType) { case 'InitialiseKeys': { - const { onChainKeyRegistry, contractName } = fields; + const { onChainKeyRegistry, contractName, msgSenderParam } = fields; return { nodeType, contractName, onChainKeyRegistry, + msgSenderParam, }; } case 'InitialisePreimage': { diff --git a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts index e2630445..b0b76549 100644 --- a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts +++ b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts @@ -42,7 +42,7 @@ class BoilerplateGenerator { \n // Initialise commitment preimage of whole accessed state: ${stateVarIds.join('\n')} \nlet ${stateName}_commitmentExists = true; - \nconst ${stateName}_commitment = await getCurrentWholeCommitment(${stateName}_stateVarId); + \nconst ${stateName}_commitment = await getCurrentWholeCommitment(${stateName}_stateVarId, SAAS_CONTEXT_PARAM?.accountId); \nconst ${stateName}_preimage = ${stateName}_commitment.preimage; \nconst ${stateName} = generalise(${stateName}_preimage.value);`]; default: @@ -51,7 +51,7 @@ class BoilerplateGenerator { ${stateVarIds.join('\n')} \nlet ${stateName}_commitmentExists = true; let ${stateName}_witnessRequired = true; - \nconst ${stateName}_commitment = await getCurrentWholeCommitment(${stateName}_stateVarId); + \nconst ${stateName}_commitment = await getCurrentWholeCommitment(${stateName}_stateVarId, SAAS_CONTEXT_PARAM?.accountId); \nlet ${stateName}_preimage = { \tvalue: ${structProperties ? `{` + structProperties.map(p => `${p}: 0`) + `}` : `0`}, \tsalt: 0, @@ -70,7 +70,8 @@ class BoilerplateGenerator { initialiseKeys = { - postStatements(contractName, onChainKeyRegistry): string[] { + postStatements(contractName, onChainKeyRegistry, msgSenderParam): string[] { + const msgSenderLine = msgSenderParam ? `\nconst msgSender = generalise(keys.ethPK);` : ''; return [ ` \n\n// Read keys using KeyManager @@ -84,7 +85,7 @@ class BoilerplateGenerator { \nconst secretKey = generalise(keys.secretKey); \nconst publicKey = generalise(keys.publicKey); \nconst sharedPublicKey = keys.sharedPublicKey ? generalise(keys.sharedPublicKey) : null; - \nconst sharedSecretKey = keys.sharedSecretKey ? generalise(keys.sharedSecretKey) : null; + \nconst sharedSecretKey = keys.sharedSecretKey ? generalise(keys.sharedSecretKey) : null;${msgSenderLine} ` ]; }, @@ -233,7 +234,7 @@ class BoilerplateGenerator { if(${stateName}_1_oldCommitment === null && ${stateName}_commitmentFlag){ \n${stateName}_witness_0 = await getMembershipWitness('${contractName}', generalise(${stateName}_0_oldCommitment._id).integer); - \n const tx = await splitCommitments('${contractName}', '${mappingName}', ${stateName}_newCommitmentValue, secretKey, publicKey, [${stateVarId.join(' , ')}], ${stateName}_0_oldCommitment, ${stateName}_witness_0, instance, contractAddr, web3); + \n const tx = await splitCommitments('${contractName}', '${mappingName}', ${stateName}_newCommitmentValue, secretKey, publicKey, [${stateVarId.join(' , ')}], ${stateName}_0_oldCommitment, ${stateName}_witness_0, instance, contractAddr, web3, context); ${stateName}_preimage = await getCommitmentsById(${stateName}_stateVarId); [${stateName}_commitmentFlag, ${stateName}_0_oldCommitment, ${stateName}_1_oldCommitment] = getInputCommitments( @@ -247,7 +248,7 @@ class BoilerplateGenerator { \n${stateName}_witness_0 = await getMembershipWitness('${contractName}', generalise(${stateName}_0_oldCommitment._id).integer); \n${stateName}_witness_1 = await getMembershipWitness('${contractName}', generalise(${stateName}_1_oldCommitment._id).integer); - \n const tx = await joinCommitments('${contractName}', '${mappingName}', ${isSharedSecret? `sharedSecretKey, sharedPublicKey`: `secretKey, publicKey`}, [${stateVarId.join(' , ')}], [${stateName}_0_oldCommitment, ${stateName}_1_oldCommitment], [${stateName}_witness_0, ${stateName}_witness_1], instance, contractAddr, web3); + \n const tx = await joinCommitments('${contractName}', '${mappingName}', ${isSharedSecret? `sharedSecretKey, sharedPublicKey`: `secretKey, publicKey`}, [${stateVarId.join(' , ')}], [${stateName}_0_oldCommitment, ${stateName}_1_oldCommitment], [${stateName}_witness_0, ${stateName}_witness_1], instance, contractAddr, web3, context); ${stateName}_preimage = await getCommitmentsById(${stateName}_stateVarId); @@ -425,7 +426,9 @@ class BoilerplateGenerator { `\nimport { generateProof } from './common/zokrates.mjs';`, `\nimport { getMembershipWitness, getRoot } from './common/timber.mjs';`, `\nimport { decompressStarlightKey, compressStarlightKey, encrypt, decrypt, poseidonHash, scalarMult } from './common/number-theory.mjs';`, - `\nimport { KeyManager } from './common/key-management/KeyManager.mjs'; + `\nimport { KeyManager } from './common/key-management/KeyManager.mjs';`, + `\nimport { autoFundIfNeeded } from './common/gas-funding.mjs';`, + `\nimport logger from './common/logger.mjs'; \n`, `\nconst { generalise } = GN;`, `\nconst db = '/app/orchestration/common/db/preimage.json';`, @@ -690,6 +693,21 @@ sendTransaction = { return [` \n${reinitialisedOnly ? ' ': `if (${stateName}_commitmentExists) await markNullified(${stateName}_currentCommitment, secretKey.hex(32)); `} + \n// Look up recipient's accountId for proper multi-tenant isolation + \nlet ${stateName}_recipientContext = SAAS_CONTEXT_PARAM; + \nif (SAAS_CONTEXT_PARAM && ${stateName}_newOwnerPublicKey.integer !== ${isSharedSecret ? `sharedPublicKey.integer` : `publicKey.integer`}) { + \n// Commitment is being transferred to a different user + \nconst ${stateName}_recipientAddress = recipient.hex ? recipient.hex(20) : generalise(recipient).hex(20); + \nconst keyManager = KeyManager.getInstance(); + \nconst ${stateName}_recipientAccountId = await keyManager.getAccountIdByEthAddress(${stateName}_recipientAddress); + \nif (${stateName}_recipientAccountId) { + \n${stateName}_recipientContext = { accountId: ${stateName}_recipientAccountId }; + \nlogger.debug(\`Storing commitment for recipient accountId: \${${stateName}_recipientAccountId}\`); + \n} else { + \nlogger.debug(\`Recipient \${${stateName}_recipientAddress} not registered, storing without accountId\`); + \n${stateName}_recipientContext = undefined; + \n} + \n} \n try { \nawait storeCommitment({ hash: ${stateName}_newCommitment, @@ -703,7 +721,7 @@ sendTransaction = { }, secretKey: ${stateName}_newOwnerPublicKey.integer === ${isSharedSecret ? `sharedPublicKey.integer` : `publicKey.integer`} ? ${isSharedSecret ? `sharedSecretKey` : `secretKey`}: null, isNullified: false, - }, SAAS_CONTEXT_PARAM);` + errorCatch]; + }, ${stateName}_recipientContext);` + errorCatch]; } default: throw new TypeError(stateType); @@ -783,7 +801,7 @@ integrationApiServicesBoilerplate = { ` }, preStatements(): string{ - return ` import { startEventFilter, getSiblingPath } from './common/timber.mjs';\nimport fs from "fs";\nimport logger from './common/logger.mjs';\nimport { decrypt } from "./common/number-theory.mjs";\nimport { getAllCommitments, getCommitmentsByState, getBalance, getSharedSecretskeys , getBalanceByState } from "./common/commitment-storage.mjs";\nimport { backupDataRetriever } from "./BackupDataRetriever.mjs";\nimport { backupVariable } from "./BackupVariable.mjs";\nimport web3 from './common/web3.mjs';\n\n + return ` import { startEventFilter, getSiblingPath } from './common/timber.mjs';\nimport fs from "fs";\nimport logger from './common/logger.mjs';\nimport { decrypt } from "./common/number-theory.mjs";\nimport { getAllCommitments, getCommitmentsByState, getBalance, getSharedSecretskeys , getBalanceByState } from "./common/commitment-storage.mjs";\nimport { backupDataRetriever } from "./BackupDataRetriever.mjs";\nimport { backupVariable } from "./BackupVariable.mjs";\nimport web3 from './common/web3.mjs';\nimport { KeyManager } from './common/key-management/KeyManager.mjs';\n\n /** NOTE: this is the api service file, if you need to call any function use the correct url and if Your input contract has two functions, add() and minus(). minus() cannot be called before an initial add(). */ @@ -897,6 +915,194 @@ integrationApiServicesBoilerplate = { logger.error(err); res.send({ errors: [err.message] }); } + } + export async function service_registerKeys(req, res, next) { + try { + SAAS_CONTEXT_HANDLING + + const keyManager = KeyManager.getInstance(); + let keys = await keyManager.getKeys(SAAS_CONTEXT_PARAM); + + if (keys) { + return res.send({ + success: true, + message: 'Keys already registered', + address: keys.ethPK, + publicKey: keys.publicKey + }); + } + + logger.info('Registering new keys', { accountId: SAAS_CONTEXT_PARAM?.accountId }); + + const utils = await import('zkp-utils'); + const { registerKey } = await import('./common/contract.mjs'); + + const publicKey = await registerKey( + utils.default.randomHex(31), + 'CONTRACT_NAME', + true, + SAAS_CONTEXT_PARAM + ); + + keys = await keyManager.getKeys(SAAS_CONTEXT_PARAM); + + res.send({ + success: true, + message: 'Keys registered successfully', + address: keys.ethPK, + publicKey: keys.publicKey, + zkpPublicKey: publicKey.integer + }); + } catch (err) { + logger.error('Failed to register keys:', err); + res.send({ errors: [err.message] }); + } + } + + export async function service_getAddress(req, res, next) { + try { + SAAS_CONTEXT_HANDLING + + const keyManager = KeyManager.getInstance(); + let keys = await keyManager.getKeys(SAAS_CONTEXT_PARAM); + + if (!keys) { + return res.send({ + success: false, + message: 'No keys found. Please call /registerKeys first.' + }); + } + + res.send({ + address: keys.ethPK, + publicKey: keys.publicKey + }); + } catch (err) { + logger.error(err); + res.send({ errors: [err.message] }); + } + } + + export async function service_mintNFT(req, res, next) { + try { + const { tokenId } = req.body; + SAAS_CONTEXT_HANDLING + + const keyManager = KeyManager.getInstance(); + let keys = await keyManager.getKeys(SAAS_CONTEXT_PARAM); + + if (!keys) { + return res.send({ + success: false, + message: 'No keys found. Please call /registerKeys first.' + }); + } + + const { getContractAddress, getContractInterface } = await import('./common/contract.mjs'); + const erc721Address = await getContractAddress('ERC721'); + const erc721Interface = await getContractInterface('ERC721'); + const Web3 = await import('./common/web3.mjs'); + const web3 = Web3.default.connection(); + const erc721 = new web3.eth.Contract(erc721Interface.abi, erc721Address); + + try { + const owner = await erc721.methods.ownerOf(tokenId).call(); + if (owner && owner !== '0x0000000000000000000000000000000000000000') { + return res.send({ + success: false, + message: \`Token \${tokenId} already exists (owner: \${owner})\` + }); + } + } catch (error) { + // Token doesn't exist - expected + } + + const accounts = await web3.eth.getAccounts(); + const defaultAccount = accounts[0]; + + logger.info(\`Minting token \${tokenId} to \${keys.ethPK}\`); + + const mintTx = await erc721.methods + .mint(keys.ethPK, tokenId) + .send({ from: defaultAccount, gas: 500000 }); + + res.send({ + success: true, + tokenId: tokenId, + owner: keys.ethPK, + txHash: mintTx.transactionHash + }); + } catch (err) { + logger.error('Failed to mint NFT:', err); + res.send({ errors: [err.message] }); + } + } + + export async function service_approveNFT(req, res, next) { + try { + const { tokenId } = req.body; + SAAS_CONTEXT_HANDLING + + const keyManager = KeyManager.getInstance(); + let keys = await keyManager.getKeys(SAAS_CONTEXT_PARAM); + + if (!keys) { + return res.send({ + success: false, + message: 'No keys found. Please call /registerKeys first.' + }); + } + + const { getContractAddress, getContractInterface } = await import('./common/contract.mjs'); + const erc721Address = await getContractAddress('ERC721'); + const erc721Interface = await getContractInterface('ERC721'); + const Web3 = await import('./common/web3.mjs'); + const web3 = Web3.default.connection(); + const erc721 = new web3.eth.Contract(erc721Interface.abi, erc721Address); + + const escrowAddress = await getContractAddress('CONTRACT_NAME'); + + const currentApproval = await erc721.methods.getApproved(tokenId).call(); + + if (currentApproval.toLowerCase() === escrowAddress.toLowerCase()) { + return res.send({ + success: true, + message: 'Token already approved', + tokenId: tokenId, + spender: escrowAddress + }); + } + + logger.info(\`Approving token \${tokenId} for escrow\`); + + const txData = await erc721.methods + .approve(escrowAddress, tokenId) + .encodeABI(); + + const config = await import('config'); + + let txParams = { + from: keys.ethPK, + to: erc721Address, + gas: 500000, + gasPrice: config.default.web3.options.defaultGasPrice, + data: txData, + chainId: await web3.eth.net.getId(), + }; + + const signed = await web3.eth.accounts.signTransaction(txParams, keys.ethSK); + const sendTxn = await web3.eth.sendSignedTransaction(signed.rawTransaction); + + res.send({ + success: true, + tokenId: tokenId, + spender: escrowAddress, + txHash: sendTxn.transactionHash + }); + } catch (err) { + logger.error('Failed to approve NFT:', err); + res.send({ errors: [err.message] }); + } }` @@ -920,7 +1126,7 @@ integrationApiRoutesBoilerplate = { return `router.post('/FUNCTION_NAME', this.serviceMgr.service_FUNCTION_NAME.bind(this.serviceMgr),);` }, commitmentImports(): string { - return `import { service_allCommitments, service_getCommitmentsByState, service_getSharedKeys, service_getBalance, service_getBalanceByState, service_backupData, service_backupVariable,} from "./api_services.mjs";\n`; + return `import { service_allCommitments, service_getCommitmentsByState, service_getSharedKeys, service_getBalance, service_getBalanceByState, service_backupData, service_backupVariable, service_registerKeys, service_getAddress, service_mintNFT, service_approveNFT, } from "./api_services.mjs";\n`; }, commitmentRoutes(): string { return `// commitment getter routes @@ -932,6 +1138,11 @@ integrationApiRoutesBoilerplate = { // backup route router.post("/backupDataRetriever", service_backupData); router.post("/backupVariable", service_backupVariable); + // key management routes + router.post("/registerKeys", service_registerKeys); + router.get("/getAddress", service_getAddress); + router.post("/mintNFT", service_mintNFT); + router.post("/approveNFT", service_approveNFT); `; } }; diff --git a/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts b/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts index f79e31a9..19559288 100644 --- a/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts +++ b/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts @@ -38,7 +38,7 @@ const stateVariableIds = (node: any) => { privateStateName.includes('msg') ) { stateVarIds.push( - `\nconst ${privateStateName}_stateVarId_key = generalise(config.web3.options.defaultAccount); // emulates msg.sender`, + `\nconst ${privateStateName}_stateVarId_key = generalise(keys.ethPK); // emulates msg.sender`, ); } stateVarIds.push( @@ -501,9 +501,6 @@ export const OrchestrationCodeBoilerPlate: any = (node: any) => { if (node.stateMutability !== 'view') { lines.push(`let BackupData = [];`); } - if (node.msgSenderParam) - lines.push(` - \nconst msgSender = generalise(config.web3.options.defaultAccount);`); if (node.msgValueParam) lines.push(` \nconst msgValue = 1;`); @@ -647,6 +644,7 @@ export const OrchestrationCodeBoilerPlate: any = (node: any) => { `${Orchestrationbp.initialiseKeys.postStatements( node.contractName, states[0], + node.msgSenderParam, ) }`, ], }; @@ -1019,18 +1017,20 @@ export const OrchestrationCodeBoilerPlate: any = (node: any) => { return { statements: [ `${returnsCall} + \n\n// Ensure user has enough funds + \nawait autoFundIfNeeded(keys.ethPK, '0.5', '1'); \n\n// Send transaction to the blockchain: \nconst txData = await instance.methods .${node.functionName}(${lines.length > 0 ? `${lines},`: ``} {customInputs: [${returnInputs}], newNullifiers: ${params[0][0]} commitmentRoot:${params[0][1]} checkNullifiers: ${params[0][3]} newCommitments: ${params[0][2]} cipherText:${params[0][4]} encKeys: ${params[0][5]}}, proof, BackupData).encodeABI(); \n let txParams = { - from: config.web3.options.defaultAccount, + from: keys.ethPK, to: contractAddr, gas: config.web3.options.defaultGas, gasPrice: config.web3.options.defaultGasPrice, data: txData, chainId: await web3.eth.net.getId(), }; - \n const key = config.web3.key; + \n const key = keys.ethSK; \n const signed = await web3.eth.accounts.signTransaction(txParams, key); \n const sendTxn = await web3.eth.sendSignedTransaction(signed.rawTransaction); \n let tx = await instance.getPastEvents("NewLeaves", {fromBlock: sendTxn?.blockNumber || 0, toBlock: sendTxn?.blockNumber || 'latest'}); @@ -1121,17 +1121,19 @@ export const OrchestrationCodeBoilerPlate: any = (node: any) => { return { statements: [ `${returnsCallPublic} + \n\n// Ensure user has enough funds + \nawait autoFundIfNeeded(keys.ethPK, '0.05', '0.5'); \n\n// Send transaction to the blockchain: \nconst txData = await instance.methods.${node.functionName}(${lines}).encodeABI(); \nlet txParams = { - from: config.web3.options.defaultAccount, + from: keys.ethPK, to: contractAddr, gas: config.web3.options.defaultGas, gasPrice: config.web3.options.defaultGasPrice, data: txData, chainId: await web3.eth.net.getId(), }; - \nconst key = config.web3.key; + \nconst key = keys.ethSK; \nconst signed = await web3.eth.accounts.signTransaction(txParams, key); \nconst tx = await web3.eth.sendSignedTransaction(signed.rawTransaction); \nconst encEvent = {}; diff --git a/src/codeGenerators/orchestration/files/toOrchestration.ts b/src/codeGenerators/orchestration/files/toOrchestration.ts index 75e9a24f..58a2c716 100644 --- a/src/codeGenerators/orchestration/files/toOrchestration.ts +++ b/src/codeGenerators/orchestration/files/toOrchestration.ts @@ -254,7 +254,10 @@ const prepareIntegrationApiServices = (node: any) => { /SAAS_CONTEXT_DIRECT/g, node.multiTenant ? `context` : `undefined`, ); - + commitmentsCode = commitmentsCode.replace( + /CONTRACT_NAME/g, + node.contractName, + ); outputApiServiceFile = `${preprefix}\n${outputApiServiceFile}}\n ${commitmentsCode}\n`; return outputApiServiceFile; }; diff --git a/src/transformers/visitors/common.ts b/src/transformers/visitors/common.ts index 523cfb34..b480c3b8 100644 --- a/src/transformers/visitors/common.ts +++ b/src/transformers/visitors/common.ts @@ -22,6 +22,7 @@ export const initialiseOrchestrationBoilerplateNodes = (fnIndicator: FunctionDef newNodes.InitialiseKeysNode = buildNode('InitialiseKeys', { contractName, onChainKeyRegistry: fnIndicator.onChainKeyRegistry, + msgSenderParam: fnIndicator.msgSenderParam, }); if (fnIndicator.oldCommitmentAccessRequired || fnIndicator.internalFunctionoldCommitmentAccessRequired) newNodes.initialisePreimageNode = buildNode('InitialisePreimage'); diff --git a/src/traverse/Indicator.ts b/src/traverse/Indicator.ts index db20579d..75d70856 100644 --- a/src/traverse/Indicator.ts +++ b/src/traverse/Indicator.ts @@ -80,6 +80,8 @@ export class FunctionDefinitionIndicator extends ContractDefinitionIndicator { internalFunctionModifiesSecretState?: boolean; internalFunctionoldCommitmentAccessRequired?: boolean; onChainKeyRegistry?: boolean; + msgSenderParam?: boolean; + msgValueParam?: boolean; constructor(scope: Scope) { super(); From f5684b3a5484661b272bbc5430f88dd3820ed408 Mon Sep 17 00:00:00 2001 From: Adarsh Ron Date: Tue, 4 Nov 2025 15:16:21 +0530 Subject: [PATCH 08/18] chore: removed unused scripts --- package.json | 15 +-------------- src/boilerplate/common/key-management/index.mjs | 6 ------ 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/package.json b/package.json index 6853c9bd..c63aa713 100644 --- a/package.json +++ b/package.json @@ -9,20 +9,7 @@ "test-prelim": "mocha --inline-diffs --require @babel/register ./test/prelim-traversals/index.mjs", "test-prelim-lite": "mocha --require @babel/register ./test/prelim-traversals/index.mjs", "format": "prettier --write \"**/*.{json,css,scss,md}\"", - "lint": "eslint .", - "mint": "node mintAndApprove.mjs", - "deposit": "node mintAndApprove.mjs deposit", - "mint-and-deposit": "node mintAndApprove.mjs both", - "mint:tenant": "node mintAndApprove.mjs accountId=user-alice", - "deposit:tenant": "node mintAndApprove.mjs deposit accountId=user-alice", - "mint-and-deposit:tenant": "node mintAndApprove.mjs both accountId=user-alice", - "get-commitments": "node mintAndApprove.mjs commitments", - "get-commitments:tenant": "node mintAndApprove.mjs commitments accountId=user-alice", - "transfer": "node transferNft.mjs", - "transfer:direct": "node transferNft.mjs direct", - "transfer:tenant": "node transferNft.mjs accountId=user-alice", - "transfer:example": "node transferNft.mjs 3 to=0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199", - "get-receiver-commitments": "node transferNft.mjs commitments to=0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199" + "lint": "eslint ." }, "bin": { "zappify": "bin/index.mjs" diff --git a/src/boilerplate/common/key-management/index.mjs b/src/boilerplate/common/key-management/index.mjs index 9761cd7d..f7eb0403 100644 --- a/src/boilerplate/common/key-management/index.mjs +++ b/src/boilerplate/common/key-management/index.mjs @@ -30,9 +30,6 @@ export { isMultiTenant } from '../middleware/saas-context.mjs'; -// Migration utilities -export { migrate as setupUserKeysCollection } from './migrations/setup-user-keys.mjs'; - /** * Convenience function to get a configured KeyManager instance. * This is the recommended way to access key management functionality. @@ -72,8 +69,5 @@ export default { forbidSaasContext, getSaasContext, isMultiTenant, - - // Migration - setupUserKeysCollection }; From 023977fb567dc50a828b7febaf5f24ee400ea9ee Mon Sep 17 00:00:00 2001 From: SosuAlex-EY <“sosu.alex@gds.ey.com”> Date: Fri, 7 Nov 2025 13:30:26 +0530 Subject: [PATCH 09/18] feat: update for getBalance --- src/boilerplate/common/commitment-storage.mjs | 11 +++++++---- .../javascript/raw/boilerplate-generator.ts | 10 ++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/boilerplate/common/commitment-storage.mjs b/src/boilerplate/common/commitment-storage.mjs index 510eb48f..623d59ab 100644 --- a/src/boilerplate/common/commitment-storage.mjs +++ b/src/boilerplate/common/commitment-storage.mjs @@ -87,10 +87,11 @@ export async function getCurrentWholeCommitment(id, accountId) { } // function to retrieve commitment with a specified stateName -export async function getCommitmentsByState(name, mappingKey = null) { +export async function getCommitmentsByState(name, mappingKey = null, accountId = null) { const connection = await mongo.connection(MONGO_URL); const db = connection.db(COMMITMENTS_DB); const query = { name: name }; + if (accountId) query['accountId'] = accountId; if (mappingKey) query['mappingKey'] = generalise(mappingKey).integer; const commitments = await db .collection(COMMITMENTS_COLLECTION) @@ -125,12 +126,13 @@ export async function getNullifiedCommitments() { /** * @returns {Promise} The sum of the values ​​of all non-nullified commitments */ -export async function getBalance() { +export async function getBalance(accountId) { const connection = await mongo.connection(MONGO_URL); const db = connection.db(COMMITMENTS_DB); + const query = accountId ? { accountId } : {}; const commitments = await db .collection(COMMITMENTS_COLLECTION) - .find({ isNullified: false }) // no nullified + .find({ ...query, isNullified: false }) // no nullified .toArray(); let sumOfValues = 0; @@ -140,10 +142,11 @@ export async function getBalance() { return sumOfValues; } -export async function getBalanceByState(name, mappingKey = null) { +export async function getBalanceByState(name, mappingKey = null, accountId=null) { const connection = await mongo.connection(MONGO_URL); const db = connection.db(COMMITMENTS_DB); const query = { name: name }; + if (accountId) query['accountId'] = accountId; if (mappingKey) query['mappingKey'] = generalise(mappingKey).integer; const commitments = await db .collection(COMMITMENTS_COLLECTION) diff --git a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts index b0b76549..d5f424a8 100644 --- a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts +++ b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts @@ -846,8 +846,8 @@ integrationApiServicesBoilerplate = { } export async function service_getBalance(req, res, next) { try { - - const sum = await getBalance(); + const accountId = req.saasContext?.accountId; + const sum = await getBalance(accountId); res.send( {"totalBalance": sum} ); } catch (error) { console.error("Error in calculation :", error); @@ -858,7 +858,8 @@ integrationApiServicesBoilerplate = { export async function service_getBalanceByState(req, res, next) { try { const { name, mappingKey } = req.body; - const balance = await getBalanceByState(name, mappingKey); + const accountId = req.saasContext?.accountId; + const balance = await getBalanceByState(name, mappingKey, accountId); res.send( {"totalBalance": balance} ); } catch (error) { console.error("Error in calculation :", error); @@ -870,7 +871,8 @@ integrationApiServicesBoilerplate = { export async function service_getCommitmentsByState(req, res, next) { try { const { name, mappingKey } = req.body; - const commitments = await getCommitmentsByState(name, mappingKey); + const accountId = req.saasContext?.accountId; + const commitments = await getCommitmentsByState(name, mappingKey, accountId); res.send({ commitments }); await sleep(10); } catch (err) { From ec982b349716ed216d525abc1bf98afbcb684908 Mon Sep 17 00:00:00 2001 From: Adarsh Ron Date: Tue, 18 Nov 2025 20:14:50 +0530 Subject: [PATCH 10/18] feat: added multi nft support but with issues --- .../zokrates/nodes/BoilerplateGenerator.ts | 13 +- .../zokrates/raw/BoilerplateGenerator.ts | 71 ++++++- .../javascript/nodes/boilerplate-generator.ts | 21 +++ .../javascript/raw/boilerplate-generator.ts | 47 ++++- .../javascript/raw/toOrchestration.ts | 68 ++++++- src/parse/redecorate.ts | 83 +++++++++ src/parse/removeDecorators.ts | 174 +++++++++++++++++- src/transformers/checks.ts | 9 + .../checks/domainConsistencyVisitor.ts | 136 ++++++++++++++ .../checks/functionParameterVisitor.ts | 137 ++++++++++++++ .../visitors/checks/mappingAccessVisitor.ts | 133 +++++++++++++ .../visitors/redecorateVisitor.ts | 48 ++++- src/traverse/Binding.ts | 82 ++++++++- src/traverse/MappingKey.ts | 6 + src/traverse/NodePath.ts | 1 + .../NFT_Escrow_DomainParams.zol | 70 +++++++ 16 files changed, 1064 insertions(+), 35 deletions(-) create mode 100644 src/transformers/visitors/checks/domainConsistencyVisitor.ts create mode 100644 src/transformers/visitors/checks/functionParameterVisitor.ts create mode 100644 src/transformers/visitors/checks/mappingAccessVisitor.ts create mode 100644 test/contracts/user-friendly-tests/NFT_Escrow_DomainParams.zol diff --git a/src/boilerplate/circuit/zokrates/nodes/BoilerplateGenerator.ts b/src/boilerplate/circuit/zokrates/nodes/BoilerplateGenerator.ts index a0896749..ad6588e2 100644 --- a/src/boilerplate/circuit/zokrates/nodes/BoilerplateGenerator.ts +++ b/src/boilerplate/circuit/zokrates/nodes/BoilerplateGenerator.ts @@ -377,10 +377,15 @@ class BoilerplateGenerator { encryption = () => ({}); - mapping = (bpSection) => ({ - mappingName: this.mappingName, - mappingKeyName: bpSection === 'postStatements' ? this.mappingKeyName : bpSection === 'parameters' ? this.mappingKeyName.split('.')[0] : this.mappingKeyName.replace('.', 'dot'), - }); + mapping = (bpSection) => { + // Get perParameters from the binding's node + const perParameters = this.indicators?.binding?.node?.perParameters || this.indicators?.node?.perParameters || []; + return { + mappingName: this.mappingName, + mappingKeyName: bpSection === 'postStatements' ? this.mappingKeyName : bpSection === 'parameters' ? this.mappingKeyName.split('.')[0] : this.mappingKeyName.replace('.', 'dot'), + ...(perParameters.length > 0 && { perParameters }), + }; + }; /** Partitioned states need boilerplate for an incrementation/decrementation, because it's so weird and different from `a = a - b`. Whole states inherit directly from the AST, so don't need boilerplate here. */ incrementation = (extraParams) => { diff --git a/src/boilerplate/circuit/zokrates/raw/BoilerplateGenerator.ts b/src/boilerplate/circuit/zokrates/raw/BoilerplateGenerator.ts index f0227350..65e352a3 100644 --- a/src/boilerplate/circuit/zokrates/raw/BoilerplateGenerator.ts +++ b/src/boilerplate/circuit/zokrates/raw/BoilerplateGenerator.ts @@ -495,26 +495,77 @@ class BoilerplateGenerator { ]; }, - parameters({ mappingKeyName: k, mappingKeyTypeName: t }): string[] { - if (t === 'local') return []; - return [ - `private ${t ? t : 'field'} ${k}`, // must be a field, in case we need to do arithmetic on it. - ]; + /** + * Generate circuit parameters for mapping + * Includes domain parameters if present + */ + parameters({ mappingKeyName: k, mappingKeyTypeName: t, perParameters = [] }): string[] { + const params: string[] = []; + + // Add domain parameters as private inputs + for (const domainParam of perParameters) { + params.push(`private field ${domainParam.name}`); + } + + // Add mapping key parameter + if (t !== 'local') { + params.push(`private ${t ? t : 'field'} ${k}`); + } + + return params; }, - preStatements({ id: mappingId, mappingName: m }): string[] { - return [ + /** + * Generate pre-statements for mapping + * Includes domain parameter hashing if present + */ + preStatements({ id: mappingId, mappingName: m, perParameters = [] }): string[] { + const statements: string[] = [ ` // We need to hard-code the mappingId's of mappings into the circuit: field ${m}_mappingId = ${mappingId};`, ]; + + // Generate chained MiMC hashing for domain parameters + if (perParameters.length > 0) { + let currentHash = `${m}_mappingId`; + + for (let i = 0; i < perParameters.length; i++) { + const domainParam = perParameters[i]; + const nextHashVar = `${m}_perHash_${i}`; + + statements.push( + ` + // Chain domain parameter: ${domainParam.name} + field ${nextHashVar} = mimc2([${currentHash}, ${domainParam.name}]);`, + ); + + currentHash = nextHashVar; + } + + statements.push( + ` + // Final domain-chained hash + field ${m}_domainChainedId = ${currentHash};`, + ); + } + + return statements; }, - postStatements({ name: x, mappingName: m, mappingKeyName: k }): string[] { - // const x = `${m}_${k}`; + /** + * Generate post-statements for mapping + * Calculates final stateVarId with domain parameter support + */ + postStatements({ name: x, mappingName: m, mappingKeyName: k, perParameters = [] }): string[] { + // Use chained hash if domain parameters exist, otherwise use mappingId + const baseId = perParameters.length > 0 + ? `${m}_domainChainedId` + : `${m}_mappingId`; + return [ ` - field ${x}_stateVarId_field = mimc2([${m}_mappingId, ${k}]);`, + field ${x}_stateVarId_field = mimc2([${baseId}, ${k}]);`, ]; }, }; diff --git a/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts b/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts index 031f6a56..e8fa3273 100644 --- a/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts +++ b/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts @@ -11,12 +11,15 @@ export function buildPrivateStateNode(nodeType: string, fields: any = {}): any { switch (nodeType) { case 'InitialisePreimage': { const { privateStateName, id, accessedOnly = false, indicator = {} } = fields; + // For MappingKey indicators, access binding through container + const binding = indicator.binding || indicator.container?.binding; return { privateStateName, stateVarId: id, accessedOnly, mappingKey: indicator.isMapping ? indicator.referencedKeyName || indicator.keyPath.node.name : null, mappingName: indicator.isMapping ? indicator.node?.name : null, + perParameters: indicator.isMapping && binding?.node?.perParameters ? binding.node.perParameters : undefined, structProperties: indicator.isStruct ? Object.keys(indicator.structProperties) : null, }; } @@ -29,6 +32,8 @@ export function buildPrivateStateNode(nodeType: string, fields: any = {}): any { accessedOnly, indicator = {}, } = fields; + // For MappingKey indicators, access binding through container + const binding = indicator.binding || indicator.container?.binding; return { increment, stateVarId: id, @@ -38,6 +43,7 @@ export function buildPrivateStateNode(nodeType: string, fields: any = {}): any { structProperties: indicator.isStruct ? Object.keys(indicator.structProperties) : null, mappingKey: indicator.isMapping ? indicator.referencedKeyName || indicator.keyPath.node.name : null, mappingName: indicator.isMapping ? indicator.node?.name : null, + perParameters: indicator.isMapping && binding?.node?.perParameters ? binding.node.perParameters : undefined, nullifierRequired: indicator.isNullified, reinitialisedOnly, accessedOnly, @@ -56,6 +62,8 @@ export function buildPrivateStateNode(nodeType: string, fields: any = {}): any { } case 'WritePreimage': { const { id, increment, burnedOnly, reinitialisedOnly, indicator = {} } = fields + // For MappingKey indicators, access binding through container + const binding = indicator.binding || indicator.container?.binding; return { increment, stateVarId: id, @@ -65,6 +73,7 @@ export function buildPrivateStateNode(nodeType: string, fields: any = {}): any { structProperties: indicator.isStruct ? indicator.referencingPaths[0]?.getStructDeclaration()?.members.map(m => m.name) : null, mappingKey: indicator.isMapping ? indicator.referencedKeyName || indicator.keyPath.node.name : null, mappingName: indicator.isMapping ? indicator.node?.name : null, + perParameters: indicator.isMapping && binding?.node?.perParameters ? binding.node.perParameters : undefined, nullifierRequired: indicator.isNullified, burnedOnly, reinitialisedOnly, @@ -110,6 +119,8 @@ export function buildPrivateStateNode(nodeType: string, fields: any = {}): any { } case 'CalculateCommitment': { const { id, increment, privateStateName, indicator = {} } = fields; + // For MappingKey indicators, access binding through container + const binding = indicator.binding || indicator.container?.binding; return { privateStateName, stateVarId: id, @@ -119,6 +130,7 @@ export function buildPrivateStateNode(nodeType: string, fields: any = {}): any { isPartitioned: indicator.isPartitioned, nullifierRequired: indicator.isNullified, structProperties: indicator.isStruct ? indicator.referencingPaths[0]?.getStructDeclaration()?.members.map(m => m.name) : null, + perParameters: indicator.isMapping && binding?.node?.perParameters ? binding.node.perParameters : undefined, isOwned: indicator.isOwned, mappingOwnershipType: indicator.mappingOwnershipType, owner: indicator.isOwned @@ -141,6 +153,8 @@ export function buildPrivateStateNode(nodeType: string, fields: any = {}): any { localMappingKey, } = fields; const structProperties = !indicator.isStruct ? null : indicator.isAccessed ? indicator.referencingPaths[0]?.getStructDeclaration()?.members.map(m => m.name) : Object.keys(indicator.structProperties); + // For MappingKey indicators, access binding through container + const binding = indicator.binding || indicator.container?.binding; return { privateStateName, stateVarId: id, @@ -152,6 +166,7 @@ export function buildPrivateStateNode(nodeType: string, fields: any = {}): any { increment, structProperties, isMapping: indicator.isMapping, + perParameters: indicator.isMapping && binding?.node?.perParameters ? binding.node.perParameters : undefined, isWhole: indicator.isWhole, isPartitioned: indicator.isPartitioned, isOwned: indicator.isOwned, @@ -170,12 +185,15 @@ export function buildPrivateStateNode(nodeType: string, fields: any = {}): any { case 'EncryptBackupPreimage': { const { id, increment, privateStateName, indicator = {} } = fields; + // For MappingKey indicators, access binding through container + const binding = indicator.binding || indicator.container?.binding; return { privateStateName, stateVarId: id, increment, mappingKey: indicator.isMapping ? indicator.referencedKeyName || indicator.keyPath.node.name : null, mappingName: indicator.isMapping ? indicator.node?.name : null, + perParameters: indicator.isMapping && binding?.node?.perParameters ? binding.node.perParameters : undefined, isWhole: indicator.isWhole, isPartitioned: indicator.isPartitioned, nullifierRequired: indicator.isNullified, @@ -213,12 +231,15 @@ export function buildPrivateStateNode(nodeType: string, fields: any = {}): any { case 'buildBoilerplateReciever': { const { id, increment, privateStateName, indicator = {} } = fields; + // For MappingKey indicators, access binding through container + const binding = indicator.binding || indicator.container?.binding; return { privateStateName, stateVarId: id, increment, mappingKey: indicator.isMapping ? indicator.referencedKeyName || indicator.keyPath.node.name : null, mappingName: indicator.isMapping ? indicator.node?.name : null, + perParameters: indicator.isMapping && binding?.node?.perParameters ? binding.node.perParameters : undefined, isWhole: indicator.isWhole, isPartitioned: indicator.isPartitioned, structProperties: indicator.isStruct ? indicator.referencingPaths[0]?.getStructDeclaration()?.members.map(m => m.name) : null, diff --git a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts index d5f424a8..f6bb4ab5 100644 --- a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts +++ b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts @@ -1105,6 +1105,50 @@ integrationApiServicesBoilerplate = { logger.error('Failed to approve NFT:', err); res.send({ errors: [err.message] }); } + } + + export async function service_deployNFT(req, res, next) { + try { + const { name, symbol } = req.body; + SAAS_CONTEXT_HANDLING + + const { getContractInterface } = await import('./common/contract.mjs'); + const erc721Interface = await getContractInterface('ERC721'); + const Web3 = await import('./common/web3.mjs'); + const web3 = Web3.default.connection(); + + const accounts = await web3.eth.getAccounts(); + const defaultAccount = accounts[0]; + + logger.info(\`Deploying new ERC721 contract: \${name} (\${symbol})\`); + + const ERC721Contract = new web3.eth.Contract(erc721Interface.abi); + const deployTx = ERC721Contract.deploy({ + data: erc721Interface.bytecode, + arguments: [name || 'TestNFT', symbol || 'TNFT'] + }); + + const gas = await deployTx.estimateGas({ from: defaultAccount }); + const deployedContract = await deployTx.send({ + from: defaultAccount, + gas: Math.floor(gas * 1.2) // Add 20% buffer + }); + + const contractAddress = deployedContract.options.address; + + logger.info(\`ERC721 deployed at: \${contractAddress}\`); + + res.send({ + success: true, + contractAddress: contractAddress, + name: name || 'TestNFT', + symbol: symbol || 'TNFT', + txHash: deployedContract._requestManager.provider.lastJsonRpcResponse?.result + }); + } catch (err) { + logger.error('Failed to deploy NFT contract:', err); + res.send({ errors: [err.message] }); + } }` @@ -1128,7 +1172,7 @@ integrationApiRoutesBoilerplate = { return `router.post('/FUNCTION_NAME', this.serviceMgr.service_FUNCTION_NAME.bind(this.serviceMgr),);` }, commitmentImports(): string { - return `import { service_allCommitments, service_getCommitmentsByState, service_getSharedKeys, service_getBalance, service_getBalanceByState, service_backupData, service_backupVariable, service_registerKeys, service_getAddress, service_mintNFT, service_approveNFT, } from "./api_services.mjs";\n`; + return `import { service_allCommitments, service_getCommitmentsByState, service_getSharedKeys, service_getBalance, service_getBalanceByState, service_backupData, service_backupVariable, service_registerKeys, service_getAddress, service_mintNFT, service_approveNFT, service_deployNFT, } from "./api_services.mjs";\n`; }, commitmentRoutes(): string { return `// commitment getter routes @@ -1145,6 +1189,7 @@ integrationApiRoutesBoilerplate = { router.get("/getAddress", service_getAddress); router.post("/mintNFT", service_mintNFT); router.post("/approveNFT", service_approveNFT); + router.post("/deployNFT", service_deployNFT); `; } }; diff --git a/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts b/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts index 19559288..87cbf8d4 100644 --- a/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts +++ b/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts @@ -3,9 +3,20 @@ import OrchestrationBP from './boilerplate-generator.js'; +/** + * Generates stateVarId calculation code with support for domain parameters + * + * For mappings without domain parameters: + * stateVarId = mimc2([mappingId, key]) + * + * For mappings with domain parameters (chained MiMC hashing): + * perHash = mimc2([mimc2([...mimc2([mappingId, domain1]), domain2...]), domainN]) + * stateVarId = mimc2([perHash, key]) + */ const stateVariableIds = (node: any) => { const {privateStateName, stateNode} = node; const stateVarIds: string[] = []; + // state variable ids // if not a mapping, use singular unique id (if mapping, stateVarId is an array) if (!stateNode.stateVarId[1]) { @@ -14,35 +25,76 @@ const stateVariableIds = (node: any) => { ); } else { // if is a mapping... + const mappingId = stateNode.stateVarId[0]; + const mappingKey = stateNode.stateVarId[1]; + const domainParameters = stateNode.perParameters || []; + stateVarIds.push( - `\nlet ${privateStateName}_stateVarIdInit = ${stateNode.stateVarId[0]};`, + `\nlet ${privateStateName}_stateVarIdInit = ${mappingId};`, ); + + // Handle domain parameters with chained MiMC hashing + if (domainParameters.length > 0) { + // Extract and hash each domain parameter in sequence + let currentHash = `generalise(${privateStateName}_stateVarIdInit).bigInt`; + + for (let i = 0; i < domainParameters.length; i++) { + const domainParam = domainParameters[i]; + const domainVarName = `${privateStateName}_domain_${domainParam.name}`; + + // Generate code to extract domain parameter + stateVarIds.push( + `\nconst ${domainVarName} = ${domainParam.name};`, + ); + + // Chain the MiMC hash: mimc2([currentHash, domainParam]) + const nextHashVar = `${privateStateName}_perHash_${i}`; + stateVarIds.push( + `\nconst ${nextHashVar} = generalise(utils.mimcHash([${currentHash}, ${domainVarName}.bigInt], 'ALT_BN_254')).bigInt;`, + ); + + currentHash = nextHashVar; + } + + // Store the final chained hash for use in final stateVarId calculation + stateVarIds.push( + `\nlet ${privateStateName}_stateVarIdInit_chained = ${currentHash};`, + ); + } + // ... and the mapping key is not msg.sender, but is a parameter if ( - privateStateName.includes(stateNode.stateVarId[1].replaceAll('.', 'dot')) && - stateNode.stateVarId[1] !== 'msg' + privateStateName.includes(mappingKey.replaceAll('.', 'dot')) && + mappingKey !== 'msg' ) { - if (+stateNode.stateVarId[1] || stateNode.stateVarId[1] === '0') { + if (+mappingKey || mappingKey === '0') { stateVarIds.push( - `\nconst ${privateStateName}_stateVarId_key = generalise(${stateNode.stateVarId[1]});`, + `\nconst ${privateStateName}_stateVarId_key = generalise(${mappingKey});`, ); } else { stateVarIds.push( - `\nconst ${privateStateName}_stateVarId_key = ${stateNode.stateVarId[1]};`, + `\nconst ${privateStateName}_stateVarId_key = ${mappingKey};`, ); } } // ... and the mapping key is msg, and the caller of the fn has the msg key if ( - stateNode.stateVarId[1] === 'msg' && + mappingKey === 'msg' && privateStateName.includes('msg') ) { stateVarIds.push( `\nconst ${privateStateName}_stateVarId_key = generalise(keys.ethPK); // emulates msg.sender`, ); } + + // Calculate final stateVarId + // If domain parameters exist, use the chained hash; otherwise use mappingId + const baseId = domainParameters.length > 0 + ? `${privateStateName}_stateVarIdInit_chained` + : `generalise(${privateStateName}_stateVarIdInit).bigInt`; + stateVarIds.push( - `\nlet ${privateStateName}_stateVarId = generalise(utils.mimcHash([generalise(${privateStateName}_stateVarIdInit).bigInt, ${privateStateName}_stateVarId_key.bigInt], 'ALT_BN_254')).hex(32);`, + `\nlet ${privateStateName}_stateVarId = generalise(utils.mimcHash([${baseId}, ${privateStateName}_stateVarId_key.bigInt], 'ALT_BN_254')).hex(32);`, ); } return stateVarIds; diff --git a/src/parse/redecorate.ts b/src/parse/redecorate.ts index 1dee0fd2..a9748575 100644 --- a/src/parse/redecorate.ts +++ b/src/parse/redecorate.ts @@ -14,9 +14,15 @@ export class ToRedecorate { decorator: string; charStart: number; added?: boolean; + perParameters?: Array<{type: string, name: string}>; + perFunctionParam?: boolean; + paramType?: string; + paramName?: string; } const errorCheckVisitor = (thisPath: any, decoratorObj: any) => { + // skip if node doesn't have src property + if (!thisPath.node || !thisPath.node.src) return; // extract the char number const srcStart = thisPath.node.src.split(':')[0]; // if it matches the one we removed, throw error @@ -34,6 +40,83 @@ function transformation1(oldAST: any, toRedecorate: ToRedecorate[]) { // HACK: ordinarily the 2nd parameter `state` is an object. toRedecorate is an array (special kind of object). Not ideal, but it works. traverseNodesFastVisitor(oldAST, explode(redecorateVisitor), toRedecorate); + // Handle per(...) mapping decorators that weren't matched by the visitor + // This can happen when the charStart doesn't match the src property + for (const decorator of toRedecorate) { + if (decorator.added || decorator.decorator !== 'per' || decorator.perFunctionParam) continue; + + // Handle per(...) mapping decorators + // Try to find a VariableDeclaration node that matches + const findPerNode = (node: any): any => { + if (node && node.nodeType === 'VariableDeclaration' && node.src && node.stateVariable) { + const srcStart = node.src.split(':')[0]; + if (decorator.charStart === Number(srcStart)) { + return node; + } + } + if (node && typeof node === 'object') { + for (const key in node) { + if (Array.isArray(node[key])) { + for (const item of node[key]) { + const result = findPerNode(item); + if (result) return result; + } + } else if (typeof node[key] === 'object') { + const result = findPerNode(node[key]); + if (result) return result; + } + } + } + return null; + }; + + const perNode = findPerNode(oldAST); + if (perNode) { + perNode.perParameters = decorator.perParameters || []; + decorator.added = true; + } + } + + // Handle per function parameters by finding the parameter nodes and setting isPer flag + // We need to track which parameters have been marked to avoid marking the same parameter twice + const markedParams = new Set(); + + for (const decorator of toRedecorate) { + if (!decorator.perFunctionParam || !decorator.paramName) continue; + + // Find all function definitions and their parameters + const findAndMarkPerParams = (node: any): void => { + if (node && node.nodeType === 'FunctionDefinition' && node.parameters && node.parameters.parameters) { + // Look for the parameter with the matching name in this function + for (const param of node.parameters.parameters) { + if (param.nodeType === 'VariableDeclaration' && param.name === decorator.paramName && !param.stateVariable) { + // Create a unique key for this parameter + const paramKey = `${node.id}_${param.id}`; + if (!markedParams.has(paramKey)) { + param.isPer = true; + markedParams.add(paramKey); + return; // Found and marked, move to next decorator + } + } + } + } + + if (node && typeof node === 'object') { + for (const key in node) { + if (Array.isArray(node[key])) { + for (const item of node[key]) { + findAndMarkPerParams(item); + } + } else if (typeof node[key] === 'object') { + findAndMarkPerParams(node[key]); + } + } + } + }; + + findAndMarkPerParams(oldAST); + } + // we check for decorators we couldn't re-add for (const decorator of toRedecorate) { if (decorator.added) continue; diff --git a/src/parse/removeDecorators.ts b/src/parse/removeDecorators.ts index af212929..b25dd993 100644 --- a/src/parse/removeDecorators.ts +++ b/src/parse/removeDecorators.ts @@ -14,7 +14,11 @@ import { boolean } from 'yargs'; // regex: matches decorators when standalone words // eg: for {unknown knownknown known1 123lknown known secretvalue} finds only 1 match for decorator 'known' //const decorators = [/(? { + // Extract content between parentheses + const match = perDeclaration.match(/per\s*\(\s*([^)]+)\s*\)/); + if (!match || !match[1]) return []; + + const paramString = match[1]; + // Split by comma and parse each parameter + const params = paramString.split(',').map(param => { + const trimmed = param.trim(); + // Split by whitespace to separate type and name + const parts = trimmed.split(/\s+/); + if (parts.length >= 2) { + return { + type: parts[0], + name: parts[1] + }; + } + return null; + }).filter(p => p !== null); + + return params; +} + /** * Takes an input '.zol' file and rearranges any complete struct overwrites. * returns deDecoratedFile // a '.sol' file, where the struct overwrites @@ -280,6 +312,20 @@ function removeDecorators(options: any): { matches.push(...deDecoratedFile.matchAll(decorator)); } + // Process per(...) domain parameter declarations (in mapping declarations) + // These are different from standalone 'per' keywords in function parameters + const perMatches = [...deDecoratedFile.matchAll(perParameterPattern)]; + for (const perMatch of perMatches) { + const perParameters = parsePerParameters(perMatch[0]); + matches.push({ + index: perMatch.index, + 0: perMatch[0], + length: perMatch[0].length, + isPer: true, + perParameters: perParameters + } as any); + } + // number of chars to offset let offset = 0; @@ -301,23 +347,135 @@ function removeDecorators(options: any): { process.exit(1); } } + // Track per(...) removals to adjust charStart values later + const perRemovals: Array<{index: number, length: number, declStart?: number, matchIndex?: number, currentOffset?: number}> = []; + for (const match of matches) { // skip removal and offsetting if we're in a comment if (inComment(decoratedFile, match.index)) continue; // add this keyword length to offset, since we'll remove it (add one for the space we remove) const offsetSrcStart = match.index - offset; - // save the keyword and where the next word starts - toRedecorate.push({ decorator: match[0], charStart: offsetSrcStart }); - // replace the dedecorated file with one w/o the keyword (and remove one space) - deDecoratedFile = - deDecoratedFile.substring(0, offsetSrcStart) + - deDecoratedFile.substring(offsetSrcStart + match[0].length + 1); - offset += match[0].length + 1; + + // Handle per(...) domain parameters in mapping declarations + if ((match as any).isPer) { + // Find the start of the mapping declaration in the *de-decorated* file + // by looking backwards for 'mapping' from the current (offset-adjusted) index. + const beforePer = deDecoratedFile.substring(0, offsetSrcStart); + let mappingIndexInOriginal = beforePer.lastIndexOf('mapping'); + + // Also check for 'secret mapping' or other decorators before 'mapping'. + // We need to find the actual start of the variable declaration. + // Look backwards from 'mapping' to find the start (could be leading whitespace). + let declStart = mappingIndexInOriginal; + const beforeMapping = deDecoratedFile.substring(0, mappingIndexInOriginal); + const lastNewline = beforeMapping.lastIndexOf('\n'); + const lineStart = lastNewline === -1 ? 0 : lastNewline + 1; + + // Find the first non-whitespace character on this line in the de-decorated file + let i = lineStart; + while (i < mappingIndexInOriginal && /\s/.test(deDecoratedFile[i])) { + i++; + } + declStart = i; + + toRedecorate.push({ + decorator: 'per', + charStart: declStart, // Position of the start of the declaration in the de-decorated file + perParameters: (match as any).perParameters + }); + + // Track this removal so we can adjust charStart values later + const removalLength = match[0].length + 1; // +1 for the space after per(...) + perRemovals.push({ + index: offsetSrcStart, + length: removalLength, + declStart: declStart, // For debugging + matchIndex: match.index, // Original position in decorated file + currentOffset: offset // Offset at time of processing + }); + + // Remove the entire per(...) declaration and the space after it + // The per(...) pattern always has a space after it in valid Solidity + deDecoratedFile = + deDecoratedFile.substring(0, offsetSrcStart) + + deDecoratedFile.substring(offsetSrcStart + match[0].length + 1); + offset += match[0].length + 1; + } else if (match[0] === 'per') { + // Handle standalone 'per' keyword in function parameters + // Find the parameter that follows this 'per' keyword + const afterPer = decoratedFile.substring(match.index + 3); // 3 = length of 'per' + // Match: optional whitespace, type, whitespace, name, and optional comma/closing paren + const paramMatch = afterPer.match(/^\s+(\w+)\s+(\w+)\s*[,)]/); + + if (paramMatch) { + // Store the per parameter info for later use + const paramType = paramMatch[1]; + const paramName = paramMatch[2]; + + toRedecorate.push({ + decorator: 'per', + charStart: offsetSrcStart, + perFunctionParam: true, + paramType: paramType, + paramName: paramName, + added: true // Mark as added since we'll handle it in redecorate + }); + } else { + // Fallback: mark as added if we can't parse the parameter + toRedecorate.push({ decorator: 'per', charStart: offsetSrcStart, added: true }); + } + + // replace the dedecorated file with one w/o the keyword (and remove one space) + deDecoratedFile = + deDecoratedFile.substring(0, offsetSrcStart) + + deDecoratedFile.substring(offsetSrcStart + match[0].length + 1); + offset += match[0].length + 1; + } else { + // Handle regular decorators (secret, known, etc.) + toRedecorate.push({ decorator: match[0], charStart: offsetSrcStart }); + // replace the dedecorated file with one w/o the keyword (and remove one space) + deDecoratedFile = + deDecoratedFile.substring(0, offsetSrcStart) + + deDecoratedFile.substring(offsetSrcStart + match[0].length + 1); + offset += match[0].length + 1; + } } + // NO ADJUSTMENT NEEDED! + // Each per(...) decorator's charStart (declStart) was calculated in the de-decorated file + // at the time of processing. Since we always look backwards from the per(...) position + // to find the mapping start, and the per(...) removal happens AFTER the mapping start, + // the declStart values are already correct for the final de-decorated file. + // + // Example: + // - First per(...): declStart=356 in file with some decorators removed + // - We then remove per(...) at position 384 (AFTER 356) + // - So the mapping still starts at 356 in the final file + // + // - Second per(...): declStart=466 in file with first per(...) already removed + // - We then remove per(...) at position 494 (AFTER 466) + // - So the mapping still starts at 466 in the final file + // + // The key insight: each declStart is calculated BEFORE its corresponding per(...) is removed, + // and the removal happens AFTER the mapping start, so no adjustment is needed. + // const deDecoratedFile = deDecledLines.join('\r\n'); backtrace.setSolContract(deDecoratedFile); // store for later backtracing 'src' locators to lines of original code. + + // Debug: persist redecorate metadata for inspection + try { + const redecorateDebugPath = `${options.parseDirPath}/${options.inputFileName}_toRedecorate.json`; + const debugInfo = { + toRedecorate, + perRemovals, + perDecoratorsCount: toRedecorate.filter(d => d.decorator === 'per' && !d.perFunctionParam).length + }; + fs.writeFileSync(redecorateDebugPath, JSON.stringify(debugInfo, null, 2)); + } catch (e) { + // Non-fatal; best-effort debug output + } + const deDecoratedFilePath = `${options.parseDirPath}/${options.inputFileName}_dedecorated.sol`; fs.writeFileSync(deDecoratedFilePath, deDecoratedFile); // TODO: consider adding a 'safe' cli option to prevent overwrites. diff --git a/src/transformers/checks.ts b/src/transformers/checks.ts index c7841426..415c7088 100644 --- a/src/transformers/checks.ts +++ b/src/transformers/checks.ts @@ -15,6 +15,9 @@ import localDeclarationsVisitor from './visitors/checks/localDeclarationsVisitor import msgSenderParam from './visitors/checks/msgSenderParam.js'; import msgValueParam from './visitors/checks/msgValueParam.js'; import interactsWithSecretVisitor from './visitors/checks/interactsWithSecretVisitor.js'; +import domainConsistencyVisitor from './visitors/checks/domainConsistencyVisitor.js'; +import functionParameterVisitor from './visitors/checks/functionParameterVisitor.js'; +import mappingAccessVisitor from './visitors/checks/mappingAccessVisitor.js'; /** * Inspired by the Transformer @@ -59,6 +62,12 @@ function transformation1(oldAST: any) { logger.verbose('Pass the Correct internal calls Parameters'); path.traverse(explode(decoratorVisitor), state); logger.verbose('No conflicting known/unknown decorators'); + path.traverse(explode(domainConsistencyVisitor), state); + logger.verbose('Domain parameters are consistent'); + path.traverse(explode(functionParameterVisitor), state); + logger.verbose('Function parameters with per keyword are valid'); + path.traverse(explode(mappingAccessVisitor), state); + logger.verbose('Mapping access with domain parameters is valid'); path.traverse(explode(interactsWithSecretVisitor), state); logger.verbose('Secret interacts marked'); path.traverse(explode(incrementedVisitor), state); diff --git a/src/transformers/visitors/checks/domainConsistencyVisitor.ts b/src/transformers/visitors/checks/domainConsistencyVisitor.ts new file mode 100644 index 00000000..2e8257f4 --- /dev/null +++ b/src/transformers/visitors/checks/domainConsistencyVisitor.ts @@ -0,0 +1,136 @@ +/* eslint-disable no-param-reassign, no-unused-vars */ + +import logger from '../../../utils/logger.js'; +import backtrace from '../../../error/backtrace.js'; +import { SyntaxUsageError } from '../../../error/errors.js'; +import NodePath from '../../../traverse/NodePath.js'; +import { VariableBinding } from '../../../traverse/Binding.js'; + +/** + * Visitor validates that domain parameters are used consistently throughout the code. + * + * Checks: + * 1. All accesses to a per-mapped variable use the same domain parameters + * 2. Domain parameter types match declaration + * 3. Domain parameter names are consistent across the contract + */ + +// Track domain parameter declarations globally within a contract +const domainParameterRegistry: Map = new Map(); + +export default { + ContractDefinition: { + enter(path: NodePath) { + // Clear registry for each contract + domainParameterRegistry.clear(); + }, + + exit(path: NodePath) { + // Clear registry after contract processing + domainParameterRegistry.clear(); + }, + }, + + VariableDeclaration: { + enter(path: NodePath) { + const { node, scope } = path; + + // Only check secret mappings with per parameters + if (!node.isSecret || !node.perParameters || node.perParameters.length === 0) { + return; + } + + if (!path.isMappingDeclaration()) { + return; + } + + const mappingName = node.name; + const registryKey = `mapping:${mappingName}`; + + // Check if this mapping has been declared before with different per parameters + if (domainParameterRegistry.has(registryKey)) { + const previousDeclaration = domainParameterRegistry.get(registryKey); + + // Validate per parameter count matches + if (previousDeclaration.perParameters.length !== node.perParameters.length) { + throw new SyntaxUsageError( + `Mapping '${mappingName}' declared with ${node.perParameters.length} domain parameter(s), ` + + `but previously declared with ${previousDeclaration.perParameters.length} domain parameter(s). ` + + `Domain parameters must be consistent across all declarations.`, + node, + ); + } + + // Validate each per parameter matches + for (let i = 0; i < node.perParameters.length; i++) { + const current = node.perParameters[i]; + const previous = previousDeclaration.perParameters[i]; + + if (current.type !== previous.type) { + throw new SyntaxUsageError( + `Domain parameter '${current.name}' at position ${i + 1} has type '${current.type}', ` + + `but previously declared as '${previous.type}'. ` + + `Domain parameter types must match across all declarations.`, + node, + ); + } + + if (current.name !== previous.name) { + throw new SyntaxUsageError( + `Domain parameter at position ${i + 1} is named '${current.name}', ` + + `but previously named '${previous.name}'. ` + + `Domain parameter names must be consistent across all declarations.`, + node, + ); + } + } + } else { + // Register this mapping's domain parameters + domainParameterRegistry.set(registryKey, { + mappingName, + perParameters: node.perParameters, + node, + }); + } + + // Store per parameters in binding for later validation + const binding = scope.getReferencedBinding(node); + if (binding instanceof VariableBinding) { + binding.perParameters = node.perParameters; + } + }, + }, + + Identifier: { + exit(path: NodePath) { + const { node, scope } = path; + + // Skip special identifiers + if (path.isMsg() || path.isThis() || path.isExportedSymbol()) { + return; + } + + // Get the binding for this identifier + const binding = scope.getReferencedBinding(node); + if (!binding || !binding.stateVariable) { + return; + } + + // Check if this is a mapping with domain parameters + if (!binding.isMapping || !binding.perParameters || binding.perParameters.length === 0) { + return; + } + + // Verify the identifier is being used in a context where domain parameters are available + // This will be validated more thoroughly in the mapping access visitor + const indexAccessAncestor = path.getAncestorOfType('IndexAccess'); + if (!indexAccessAncestor) { + return; + } + + // Mark that this mapping is being accessed + binding.isReferenced = true; + }, + }, +}; + diff --git a/src/transformers/visitors/checks/functionParameterVisitor.ts b/src/transformers/visitors/checks/functionParameterVisitor.ts new file mode 100644 index 00000000..22766d6b --- /dev/null +++ b/src/transformers/visitors/checks/functionParameterVisitor.ts @@ -0,0 +1,137 @@ +/* eslint-disable no-param-reassign, no-unused-vars */ + +import logger from '../../../utils/logger.js'; +import backtrace from '../../../error/backtrace.js'; +import { SyntaxUsageError } from '../../../error/errors.js'; +import NodePath from '../../../traverse/NodePath.js'; + +/** + * @desc: + * Visitor validates function parameters with `per` keyword. + * + * Checks: + * 1. Function parameters with `per` keyword are properly declared + * 2. Per parameters appear before regular parameters + * 3. Per parameters are not marked as secret + * 4. Per parameter types are valid + */ + +export default { + FunctionDefinition: { + enter(path: NodePath) { + const { node } = path; + + // Skip if no parameters + if (!node.parameters || !node.parameters.parameters) { + return; + } + + const params = node.parameters.parameters; + let lastPerParamIndex = -1; + const perParameters: any[] = []; + const regularParameters: any[] = []; + + // First pass: identify per parameters and validate ordering + for (let i = 0; i < params.length; i++) { + const param = params[i]; + + // Check if parameter has per keyword (stored during parsing) + if (param.isPer) { + lastPerParamIndex = i; + perParameters.push(param); + + // Validate per parameter is not secret + if (param.isSecret) { + throw new SyntaxUsageError( + `Domain parameter '${param.name}' cannot be marked as 'secret'. ` + + `Domain parameters are part of the public API and must be public.`, + param, + ); + } + } else { + regularParameters.push(param); + + // Validate that regular parameters don't come before per parameters + if (lastPerParamIndex !== -1 && i > lastPerParamIndex) { + // This is fine - regular params can come after per params + } + } + } + + // Validate per parameters come first + for (let i = 0; i < params.length; i++) { + const param = params[i]; + if (!param.isPer && i < lastPerParamIndex) { + throw new SyntaxUsageError( + `Regular parameter '${param.name}' appears before domain parameter. ` + + `All domain parameters (with 'per' keyword) must appear before regular parameters.`, + param, + ); + } + } + + // Store per parameters in function scope for later validation + if (perParameters.length > 0) { + node.perParameters = perParameters; + } + }, + }, + + ParameterList: { + enter(path: NodePath) { + const { node, parent } = path; + + // Only process function parameters + if (parent.nodeType !== 'FunctionDefinition') { + return; + } + + if (!node.parameters) { + return; + } + + // Validate each parameter + for (const param of node.parameters) { + // Check for per keyword in parameter name or metadata + if (param.isPer) { + // Validate parameter type is valid + const typeName = param.typeName?.name || param.typeDescriptions?.typeString; + if (!typeName) { + throw new SyntaxUsageError( + `Domain parameter '${param.name}' has no type. ` + + `Domain parameters must have a valid Solidity type.`, + param, + ); + } + + // Validate parameter is not an array or mapping + if (param.typeName?.nodeType === 'ArrayTypeName') { + throw new SyntaxUsageError( + `Domain parameter '${param.name}' cannot be an array type. ` + + `Domain parameters must be scalar types (uint256, address, bytes32, etc.).`, + param, + ); + } + + if (param.typeName?.nodeType === 'Mapping') { + throw new SyntaxUsageError( + `Domain parameter '${param.name}' cannot be a mapping type. ` + + `Domain parameters must be scalar types.`, + param, + ); + } + } + } + }, + }, + + FunctionCall: { + enter(path: NodePath) { + const { node, scope } = path; + + // This will be used in Phase 3 to validate function calls with per parameters + // For now, we just mark that we've seen a function call + }, + }, +}; + diff --git a/src/transformers/visitors/checks/mappingAccessVisitor.ts b/src/transformers/visitors/checks/mappingAccessVisitor.ts new file mode 100644 index 00000000..6f34146d --- /dev/null +++ b/src/transformers/visitors/checks/mappingAccessVisitor.ts @@ -0,0 +1,133 @@ +/* eslint-disable no-param-reassign, no-unused-vars */ + +import logger from '../../../utils/logger.js'; +import backtrace from '../../../error/backtrace.js'; +import { SyntaxUsageError } from '../../../error/errors.js'; +import NodePath from '../../../traverse/NodePath.js'; +import { VariableBinding } from '../../../traverse/Binding.js'; + +/** + * @desc: + * Visitor validates that mappings are accessed with correct domain parameters. + * + * Checks: + * 1. Mapping access includes all required domain parameters + * 2. Domain parameters are available in the current scope + * 3. Domain parameter values match expected types + */ + +export default { + IndexAccess: { + enter(path: NodePath) { + const { node, scope } = path; + + // Get the base expression (the mapping being accessed) + const baseExpression = node.baseExpression; + if (!baseExpression || baseExpression.nodeType !== 'Identifier') { + return; + } + + // Get the binding for the mapping + const binding = scope.getReferencedBinding(baseExpression); + if (!binding || !binding.stateVariable || !binding.isMapping) { + return; + } + + // Check if this mapping has domain parameters + if (!binding.perParameters || binding.perParameters.length === 0) { + return; + } + + // Get the function definition context + const functionDef = path.getFunctionDefinition(); + if (!functionDef) { + throw new SyntaxUsageError( + `Mapping '${binding.name}' with domain parameters can only be accessed within a function. ` + + `Cannot access domain-parameterized mapping at contract level.`, + node, + ); + } + + // Get function parameters + const functionParams = functionDef.node.parameters?.parameters || []; + const perParamsInFunction = functionParams.filter((p: any) => p.isPer); + + // Validate that function has required per parameters + if (perParamsInFunction.length < binding.perParameters.length) { + const missingCount = binding.perParameters.length - perParamsInFunction.length; + const missingParams = binding.perParameters + .slice(perParamsInFunction.length) + .map((p: any) => `${p.type} ${p.name}`) + .join(', '); + + throw new SyntaxUsageError( + `Mapping '${binding.name}' requires ${binding.perParameters.length} domain parameter(s), ` + + `but function '${functionDef.node.name}' only has ${perParamsInFunction.length}. ` + + `Missing: ${missingParams}. ` + + `Add these to the function signature: function ${functionDef.node.name}(per ${missingParams}, ...) ...`, + node, + ); + } + + // Validate per parameter types match + for (let i = 0; i < binding.perParameters.length; i++) { + const mappingPerParam = binding.perParameters[i]; + const functionPerParam = perParamsInFunction[i]; + + if (mappingPerParam.type !== functionPerParam.typeName?.name && + mappingPerParam.type !== functionPerParam.typeDescriptions?.typeString) { + throw new SyntaxUsageError( + `Domain parameter '${mappingPerParam.name}' at position ${i + 1} ` + + `has type '${mappingPerParam.type}' in mapping declaration, ` + + `but function parameter has type '${functionPerParam.typeName?.name || functionPerParam.typeDescriptions?.typeString}'. ` + + `Domain parameter types must match exactly.`, + node, + ); + } + + if (mappingPerParam.name !== functionPerParam.name) { + logger.warn( + `Domain parameter at position ${i + 1} is named '${mappingPerParam.name}' in mapping ` + + `but '${functionPerParam.name}' in function. ` + + `Consider using consistent names for clarity.`, + ); + } + } + + // Mark that this mapping is being accessed with domain parameters + binding.isReferenced = true; + }, + }, + + Identifier: { + exit(path: NodePath) { + const { node, scope } = path; + + // Skip special identifiers + if (path.isMsg() || path.isThis() || path.isExportedSymbol()) { + return; + } + + // Check if this identifier is a domain parameter being used + const binding = scope.getReferencedBinding(node); + if (!binding || binding.stateVariable) { + return; + } + + // Check if this is a per parameter in a function + const functionDef = path.getFunctionDefinition(); + if (!functionDef) { + return; + } + + const functionParams = functionDef.node.parameters?.parameters || []; + const isPerParam = functionParams.some((p: any) => p.name === node.name && p.isPer); + + if (isPerParam) { + // This is a valid per parameter usage + binding.isReferenced = true; + } + }, + }, +}; + diff --git a/src/transformers/visitors/redecorateVisitor.ts b/src/transformers/visitors/redecorateVisitor.ts index 93b8f335..12fbf551 100644 --- a/src/transformers/visitors/redecorateVisitor.ts +++ b/src/transformers/visitors/redecorateVisitor.ts @@ -40,10 +40,24 @@ export default { enter(node: any, state: any) { // for each decorator we have to re-add... for (const toRedecorate of state) { - // skip if the decorator is not secret or sharedSecret (can't be a variable dec) or if its already been added - if (toRedecorate.added || (toRedecorate.decorator !== 'secret' && toRedecorate.decorator !== 'sharedSecret')) continue; + // skip if already been added + if (toRedecorate.added) continue; + // extract the char number const srcStart = node.src.split(':')[0]; + + // Handle per(...) domain parameters + if (toRedecorate.decorator === 'per') { + if (toRedecorate.charStart === Number(srcStart)) { + toRedecorate.added = true; + node.perParameters = toRedecorate.perParameters || []; + return; + } + } + + // Handle secret/sharedSecret decorators + if (toRedecorate.decorator !== 'secret' && toRedecorate.decorator !== 'sharedSecret') continue; + // if it matches the one we removed, add it back to the AST if (toRedecorate.charStart === Number(srcStart)) { toRedecorate.added = true; @@ -91,6 +105,12 @@ export default { case 'reinitialisable': node.reinitialisable = true; return; + case 'per': + // Handle per function parameters + if (toRedecorate.perFunctionParam) { + node.isPer = true; + } + return; default: return; } @@ -98,4 +118,28 @@ export default { } }, }, + + // Catch-all for any node type that might have per(...) domain parameters + '*': { + enter(node: any, state: any) { + // Only process nodes with src property + if (!node || !node.src) return; + + // for each decorator we have to re-add... + for (const toRedecorate of state) { + // skip if already been added or not a per decorator + if (toRedecorate.added || toRedecorate.decorator !== 'per') continue; + + // extract the char number + const srcStart = node.src.split(':')[0]; + + // Handle per(...) domain parameters + if (toRedecorate.charStart === Number(srcStart)) { + toRedecorate.added = true; + node.perParameters = toRedecorate.perParameters || []; + return; + } + } + }, + }, }; diff --git a/src/traverse/Binding.ts b/src/traverse/Binding.ts index 536c3c56..c325706e 100644 --- a/src/traverse/Binding.ts +++ b/src/traverse/Binding.ts @@ -174,8 +174,8 @@ export class VariableBinding extends Binding { blacklist?: any[]; - - + // Domain parameters (per keyword) + perParameters?: Array<{type: string, name: string}>; isOwned?: boolean; owner: any = null; // object of objects, indexed by node id. @@ -196,6 +196,8 @@ export class VariableBinding extends Binding { this.isSecret = node.isSecret ?? false; this.isSharedSecret = node.isSharedSecret ?? false; + // Initialize domain parameters (per keyword) + this.perParameters = node.perParameters ?? []; if (path.isMappingDeclaration() || path.isArrayDeclaration()) { this.isMapping = true; @@ -278,6 +280,19 @@ export class VariableBinding extends Binding { updateOwnership(ownerNode: any, msgIsMappingKeyorMappingValue?: string | null) { if (this.isOwned && this.owner.mappingOwnershipType === 'key') return; + + // For mapping states, ignore caller-restriction-based owners that clearly + // refer to a different mapping. This avoids spurious "two distinct owners" + // errors when a function both (a) restricts msg.sender via one mapping and + // (b) nullifies a different mapping. + if (this.isMapping && !msgIsMappingKeyorMappingValue && ownerNode.baseExpression) { + const referencedDeclaration = ownerNode.baseExpression.referencedDeclaration; + if (referencedDeclaration && referencedDeclaration !== this.id) { + // This restriction is not about this mapping; skip it for this binding. + return; + } + } + if ( ownerNode.expression?.name === 'msg' && msgIsMappingKeyorMappingValue === 'value' @@ -654,4 +669,67 @@ export class VariableBinding extends Binding { // TODO more useful indicators here } } + + /** + * Validates that domain parameters are consistent with a given set of parameters + * @param {Array} otherPerParameters - The domain parameters to compare against + * @param {string} context - Context for error messages (e.g., "function parameter") + * @returns {boolean} - True if parameters match, throws error otherwise + */ + validateDomainParameterConsistency(otherPerParameters: Array<{type: string, name: string}>, context: string = 'function'): boolean { + if (!this.perParameters || this.perParameters.length === 0) { + return true; + } + + if (!otherPerParameters || otherPerParameters.length === 0) { + throw new SyntaxUsageError( + `Mapping '${this.name}' requires ${this.perParameters.length} domain parameter(s), ` + + `but the ${context} has none. ` + + `Add domain parameters: ${this.perParameters.map(p => `per ${p.type} ${p.name}`).join(', ')}`, + this.node, + ); + } + + if (this.perParameters.length !== otherPerParameters.length) { + throw new SyntaxUsageError( + `Mapping '${this.name}' requires ${this.perParameters.length} domain parameter(s), ` + + `but the ${context} has ${otherPerParameters.length}. ` + + `Expected: ${this.perParameters.map(p => `${p.type} ${p.name}`).join(', ')}`, + this.node, + ); + } + + for (let i = 0; i < this.perParameters.length; i++) { + const mappingParam = this.perParameters[i]; + const otherParam = otherPerParameters[i]; + + if (mappingParam.type !== otherParam.type) { + throw new SyntaxUsageError( + `Domain parameter '${mappingParam.name}' at position ${i + 1} ` + + `has type '${mappingParam.type}' in mapping '${this.name}', ` + + `but type '${otherParam.type}' in the ${context}. ` + + `Domain parameter types must match exactly.`, + this.node, + ); + } + } + + return true; + } + + /** + * Gets the domain parameters required for this binding + * @returns {Array} - Array of domain parameter objects + */ + getDomainParameters(): Array<{type: string, name: string}> { + return this.perParameters || []; + } + + /** + * Checks if this binding has domain parameters + * @returns {boolean} - True if binding has domain parameters + */ + hasDomainParameters(): boolean { + return this.perParameters && this.perParameters.length > 0; + } } diff --git a/src/traverse/MappingKey.ts b/src/traverse/MappingKey.ts index 7559dcaf..91e69d6a 100644 --- a/src/traverse/MappingKey.ts +++ b/src/traverse/MappingKey.ts @@ -76,6 +76,9 @@ export default class MappingKey { owner: any = null; // object of objects, indexed by node id. encryptionRequired?: boolean; + // Domain parameters (per keyword) + perParameters?: Array<{type: string, name: string}>; + returnKeyName(keyNode: any) { if (this.keyPath.isMsgSender(keyNode)) return 'msgSender'; if (this.keyPath.isMsgValue(keyNode)) return 'msgValue'; @@ -113,6 +116,9 @@ export default class MappingKey { this.isSecret = container.isSecret; this.isSharedSecret = container.isSharedSecret; + // Initialize domain parameters from container + this.perParameters = container.perParameters ?? []; + this.isMapping = container.isMapping; this.isStruct = container.isStruct; // keyPath.isStruct(); diff --git a/src/traverse/NodePath.ts b/src/traverse/NodePath.ts index 7471e665..23a36e80 100644 --- a/src/traverse/NodePath.ts +++ b/src/traverse/NodePath.ts @@ -635,6 +635,7 @@ export default class NodePath { } isExternalContractInstanceDeclaration(node: any = this.node): boolean { + if (!node) return false; if ( !['VariableDeclaration', 'VariableDeclarationStatement'].includes( node.nodeType, diff --git a/test/contracts/user-friendly-tests/NFT_Escrow_DomainParams.zol b/test/contracts/user-friendly-tests/NFT_Escrow_DomainParams.zol new file mode 100644 index 00000000..b4c77e9b --- /dev/null +++ b/test/contracts/user-friendly-tests/NFT_Escrow_DomainParams.zol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: CC0 +// NFT Escrow with Domain Parameters (Phase 3 Test) + +pragma solidity ^0.8.0; + +import "./Escrow-imports/IERC721.sol"; + +contract NFT_Escrow_DomainParams { + + // Domain parameter: nftContract + // This allows tracking tokens from multiple NFT contracts + // stateVarId = mimc2([mimc2([mappingId, nftContract]), tokenId]) + secret mapping(uint256 => address) per(address nftContract) public tokenOwners; + + // Domain-scoped approvals: one namespace per nftContract + secret mapping(address => address) per(address nftContract) public approvals; + IERC721 public erc721; + + constructor(address _erc721) { + erc721 = IERC721(_erc721); + } + + // Function with domain parameter + // The per parameter must match the mapping's domain parameter + // Note: We use the stored erc721 instance for the actual transfer, + // but nftContract is used as the domain parameter for cryptographic separation + function deposit(per address nftContract, uint256 tokenId) public { + require(nftContract != address(0), "NFT_Escrow: invalid nftContract"); + bool success = erc721.transferFrom(msg.sender, address(this), tokenId); + require(success, "NFT_Escrow: ERC721 transfer failed"); + // stateVarId calculation includes nftContract domain parameter + reinitialisable tokenOwners[tokenId] = msg.sender; + } + + function transfer(per address nftContract, secret address recipient, secret uint256 tokenId) public { + require(nftContract != address(0), "NFT_Escrow: invalid nftContract"); + require(tokenOwners[tokenId] == msg.sender); + require(recipient != address(0), "NFT_Escrow: transfer to the zero address"); + tokenOwners[tokenId] = recipient; + } + + function approve(per address nftContract, secret address approvedAddress) public { + require(nftContract != address(0), "NFT_Escrow: invalid nftContract"); + require(approvedAddress != address(0), "Escrow: approve to the zero address"); + approvals[msg.sender] = approvedAddress; + } + + function transferFrom(per address nftContract, secret address sender, secret address recipient, secret uint256 tokenId) public { + require(nftContract != address(0), "NFT_Escrow: invalid nftContract"); + require(recipient != address(0), "NFT_Escrow: transfer to the zero address"); + require(sender != address(0), "NFT_Escrow: transfer from the zero address"); + + // Approval: sender (owner) has approved msg.sender in this nftContract domain + require(approvals[sender] == msg.sender, "NFT_Escrow: not approved"); + + // Ownership: sender actually owns tokenId in this nftContract's domain + require(tokenOwners[tokenId] == sender, "NFT_Escrow: sender does not own token"); + + tokenOwners[tokenId] = recipient; + } + + function withdraw(per address nftContract, uint256 tokenId) public { + require(nftContract != address(0), "NFT_Escrow: invalid nftContract"); + require(tokenOwners[tokenId] == msg.sender); + bool success = erc721.transferFrom(address(this), msg.sender, tokenId); + require(success, "ERC721 transfer failed"); + tokenOwners[tokenId] = address(0); + } +} + From 623910135c6d9a14fd4812b88bdab6e27581fbe1 Mon Sep 17 00:00:00 2001 From: Lydia Garms Date: Wed, 19 Nov 2025 22:59:17 -0300 Subject: [PATCH 11/18] fix: burningfor reinitialisable variables --- src/traverse/Binding.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/traverse/Binding.ts b/src/traverse/Binding.ts index 536c3c56..d71ddb61 100644 --- a/src/traverse/Binding.ts +++ b/src/traverse/Binding.ts @@ -599,17 +599,7 @@ export class VariableBinding extends Binding { } } } - // mapping[key] = msg.sender is owned by msg.sender => look for mapping[key] = 0 - // OR owner is some value (admin = address) => look for admin = 0 - if ( - ownerNode.name === 'msg' && - ownerNode.mappingOwnershipType === 'value' - ) { - // the owner is represented by the mapping value - we look through the modifyingPaths for 0 - this.searchModifyingPathsForZero(); - } else if (ownerBinding && ownerBinding instanceof VariableBinding) { - ownerBinding.searchModifyingPathsForZero(); - } + this.searchModifyingPathsForZero(); if (this.reinitialisable && !this.isBurned) throw new SyntaxUsageError( `The state ${this.name} has been marked as reinitialisable but we can't find anywhere to burn a commitment ready for reinitialisation.`, From 441dc4fa9ef8d16fd4bf087cb9747ea6f7d080ff Mon Sep 17 00:00:00 2001 From: Adarsh Ron Date: Mon, 24 Nov 2025 12:56:21 +0530 Subject: [PATCH 12/18] feat: multi nft working --- scripts/nft-escrow-e2e.sh | 377 ++++++++++++++++++ src/boilerplate/common/commitment-storage.mjs | 51 ++- src/boilerplate/common/contract.mjs | 8 +- src/boilerplate/common/migrations/metadata.js | 8 +- .../javascript/nodes/boilerplate-generator.ts | 3 +- .../javascript/raw/boilerplate-generator.ts | 123 ++++-- .../javascript/raw/toOrchestration.ts | 61 ++- .../orchestration/files/toOrchestration.ts | 90 +++-- src/transformers/visitors/common.ts | 3 + src/transformers/visitors/toCircuitVisitor.ts | 24 +- .../visitors/toOrchestrationVisitor.ts | 80 +++- .../NFT_Escrow_DomainParams.zol | 26 +- 12 files changed, 723 insertions(+), 131 deletions(-) create mode 100755 scripts/nft-escrow-e2e.sh diff --git a/scripts/nft-escrow-e2e.sh b/scripts/nft-escrow-e2e.sh new file mode 100755 index 00000000..f8cf2877 --- /dev/null +++ b/scripts/nft-escrow-e2e.sh @@ -0,0 +1,377 @@ +#!/bin/bash + +set -e # Exit on error + +# Configuration +BASE_URL="http://localhost:3000" +ACCOUNT_ID="user-alice" +ZAPP_DIR="zapps/NFT_Escrow_DomainParams" + +# Use random token IDs to avoid conflicts with previous tests +TOKEN_ID_1=$((100 + RANDOM % 900)) +TOKEN_ID_2=$((1000 + RANDOM % 9000)) + +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo "==========================================" +echo "NFT_Escrow_DomainParams - Multi-NFT Test" +echo "==========================================" +echo "" +echo "Using accountId: $ACCOUNT_ID" +echo "Testing namespace isolation..." +echo "" + +# Helper function to print success +print_success() { + echo -e "${GREEN}✓${NC} $1" +} + +# Helper function to print error +print_error() { + echo -e "${RED}✗${NC} $1" +} + +# Helper function to print info +print_info() { + echo -e "${YELLOW}ℹ${NC} $1" +} + +# Step 1: Verify bytecode is present +echo "Step 1: Verifying bytecode in ERC721.json..." +if [ -f "$ZAPP_DIR/build/contracts/ERC721.json" ]; then + if cat "$ZAPP_DIR/build/contracts/ERC721.json" | jq -e '.bytecode' > /dev/null 2>&1; then + BYTECODE_LENGTH=$(cat "$ZAPP_DIR/build/contracts/ERC721.json" | jq -r '.bytecode' | wc -c) + print_success "Bytecode found! Length: $BYTECODE_LENGTH characters" + else + print_error "Bytecode not found in ERC721.json" + exit 1 + fi +else + print_error "ERC721.json not found at $ZAPP_DIR/build/contracts/ERC721.json" + exit 1 +fi +echo "" + +# Step 2: Register keys for user +echo "Step 2: Registering keys for user..." +REGISTER_RESPONSE=$(curl -s -X POST "$BASE_URL/registerKeys" \ + -H "Content-Type: application/json" \ + -H "x-saas-context: {\"accountId\": \"$ACCOUNT_ID\"}" \ + -d '{}') +print_info "Response: $REGISTER_RESPONSE" + +# Extract user address +USER_ADDRESS=$(echo "$REGISTER_RESPONSE" | jq -r '.address // empty') +if [ -z "$USER_ADDRESS" ] || [ "$USER_ADDRESS" == "null" ]; then + print_error "Failed to register keys" + exit 1 +fi +print_success "Keys registered. User address: $USER_ADDRESS" +echo "" + +# Step 3: Deploy first ERC721 contract (CryptoKitties) +echo "Step 3: Deploying first NFT contract (CryptoKitties)..." +DEPLOY1_RESPONSE=$(curl -s -X POST "$BASE_URL/deployNFT" \ + -H "Content-Type: application/json" \ + -H "x-saas-context: {\"accountId\": \"$ACCOUNT_ID\"}" \ + -d '{"name": "CryptoKitties", "symbol": "CK"}') + +CONTRACT1_ADDRESS=$(echo "$DEPLOY1_RESPONSE" | jq -r '.contractAddress // empty') +if [ -z "$CONTRACT1_ADDRESS" ] || [ "$CONTRACT1_ADDRESS" == "null" ]; then + print_error "Failed to deploy CryptoKitties contract" + echo "Response: $DEPLOY1_RESPONSE" + exit 1 +fi +print_success "CryptoKitties deployed at: $CONTRACT1_ADDRESS" +echo "" + +# Step 4: Deploy second ERC721 contract (CryptoPunks) +echo "Step 4: Deploying second NFT contract (CryptoPunks)..." +DEPLOY2_RESPONSE=$(curl -s -X POST "$BASE_URL/deployNFT" \ + -H "Content-Type: application/json" \ + -H "x-saas-context: {\"accountId\": \"$ACCOUNT_ID\"}" \ + -d '{"name": "CryptoPunks", "symbol": "CP"}') + +CONTRACT2_ADDRESS=$(echo "$DEPLOY2_RESPONSE" | jq -r '.contractAddress // empty') +if [ -z "$CONTRACT2_ADDRESS" ] || [ "$CONTRACT2_ADDRESS" == "null" ]; then + print_error "Failed to deploy CryptoPunks contract" + echo "Response: $DEPLOY2_RESPONSE" + exit 1 +fi +print_success "CryptoPunks deployed at: $CONTRACT2_ADDRESS" +echo "" + +# Step 5: Verify contracts are different +echo "Step 5: Verifying contracts are different..." +if [ "$CONTRACT1_ADDRESS" == "$CONTRACT2_ADDRESS" ]; then + print_error "Both contracts have the same address!" + exit 1 +fi +print_success "Contracts have different addresses" +echo "" + +# Step 6: Mint NFT #1 from CryptoKitties contract +echo "Step 6: Minting NFT #$TOKEN_ID_1 from CryptoKitties contract ($CONTRACT1_ADDRESS)..." +MINT1_RESPONSE=$(curl -s -X POST "$BASE_URL/mintNFT" \ + -H "Content-Type: application/json" \ + -H "x-saas-context: {\"accountId\": \"$ACCOUNT_ID\"}" \ + -d "{\"tokenId\": \"$TOKEN_ID_1\", \"nftContract\": \"$CONTRACT1_ADDRESS\"}") + +MINT1_SUCCESS=$(echo "$MINT1_RESPONSE" | jq -r '.success // false') +if [ "$MINT1_SUCCESS" != "true" ]; then + print_error "Failed to mint NFT #$TOKEN_ID_1 from CryptoKitties" + echo "Response: $MINT1_RESPONSE" + exit 1 +fi +print_success "Minted NFT #$TOKEN_ID_1 from CryptoKitties contract" +echo "" + +# Step 7: Mint NFT #2 from CryptoPunks contract +echo "Step 7: Minting NFT #$TOKEN_ID_2 from CryptoPunks contract ($CONTRACT2_ADDRESS)..." +MINT2_RESPONSE=$(curl -s -X POST "$BASE_URL/mintNFT" \ + -H "Content-Type: application/json" \ + -H "x-saas-context: {\"accountId\": \"$ACCOUNT_ID\"}" \ + -d "{\"tokenId\": \"$TOKEN_ID_2\", \"nftContract\": \"$CONTRACT2_ADDRESS\"}") + +MINT2_SUCCESS=$(echo "$MINT2_RESPONSE" | jq -r '.success // false') +if [ "$MINT2_SUCCESS" != "true" ]; then + print_error "Failed to mint NFT #$TOKEN_ID_2 from CryptoPunks" + echo "Response: $MINT2_RESPONSE" + exit 1 +fi +print_success "Minted NFT #$TOKEN_ID_2 from CryptoPunks contract" +echo "" + +# Step 8: Approve NFT #1 from CryptoKitties for escrow +echo "Step 8: Approving NFT #$TOKEN_ID_1 from CryptoKitties for escrow..." +APPROVE1_RESPONSE=$(curl -s -X POST "$BASE_URL/approveNFT" \ + -H "Content-Type: application/json" \ + -H "x-saas-context: {\"accountId\": \"$ACCOUNT_ID\"}" \ + -d "{\"tokenId\": \"$TOKEN_ID_1\", \"nftContract\": \"$CONTRACT1_ADDRESS\"}") + +APPROVE1_SUCCESS=$(echo "$APPROVE1_RESPONSE" | jq -r '.success // false') +if [ "$APPROVE1_SUCCESS" != "true" ]; then + print_error "Failed to approve NFT #$TOKEN_ID_1 from CryptoKitties" + echo "Response: $APPROVE1_RESPONSE" + exit 1 +fi +print_success "Approved NFT #$TOKEN_ID_1 from CryptoKitties for escrow" +echo "" + +# Step 9: Approve NFT #2 from CryptoPunks for escrow +echo "Step 9: Approving NFT #$TOKEN_ID_2 from CryptoPunks for escrow..." +APPROVE2_RESPONSE=$(curl -s -X POST "$BASE_URL/approveNFT" \ + -H "Content-Type: application/json" \ + -H "x-saas-context: {\"accountId\": \"$ACCOUNT_ID\"}" \ + -d "{\"tokenId\": \"$TOKEN_ID_2\", \"nftContract\": \"$CONTRACT2_ADDRESS\"}") + +APPROVE2_SUCCESS=$(echo "$APPROVE2_RESPONSE" | jq -r '.success // false') +if [ "$APPROVE2_SUCCESS" != "true" ]; then + print_error "Failed to approve NFT #$TOKEN_ID_2 from CryptoPunks" + echo "Response: $APPROVE2_RESPONSE" + exit 1 +fi +print_success "Approved NFT #$TOKEN_ID_2 from CryptoPunks for escrow" +echo "" + +# Step 10: Deposit NFT #1 with CryptoKitties as domain parameter +echo "Step 10: Depositing NFT #$TOKEN_ID_1 with nftContract=$CONTRACT1_ADDRESS (CryptoKitties)..." +DEPOSIT1_RESPONSE=$(curl -s -X POST "$BASE_URL/deposit" \ + -H "Content-Type: application/json" \ + -H "x-saas-context: {\"accountId\": \"$ACCOUNT_ID\"}" \ + -d "{\"nftContract\": \"$CONTRACT1_ADDRESS\", \"tokenId\": \"$TOKEN_ID_1\"}") + +print_info "Deposit response: $DEPOSIT1_RESPONSE" + +# Check if deposit was successful (look for proof or success indicator) +if echo "$DEPOSIT1_RESPONSE" | jq -e '.proof' > /dev/null 2>&1; then + print_success "NFT #$TOKEN_ID_1 deposited successfully with CryptoKitties domain parameter" +elif echo "$DEPOSIT1_RESPONSE" | jq -e '.errors' > /dev/null 2>&1; then + ERRORS=$(echo "$DEPOSIT1_RESPONSE" | jq -r '.errors[]') + print_error "Failed to deposit NFT #$TOKEN_ID_1: $ERRORS" + exit 1 +else + print_success "NFT #$TOKEN_ID_1 deposit transaction submitted" +fi +echo "" + +# Step 11: Deposit NFT #2 with CryptoPunks as domain parameter +echo "Step 11: Depositing NFT #$TOKEN_ID_2 with nftContract=$CONTRACT2_ADDRESS (CryptoPunks)..." +DEPOSIT2_RESPONSE=$(curl -s -X POST "$BASE_URL/deposit" \ + -H "Content-Type: application/json" \ + -H "x-saas-context: {\"accountId\": \"$ACCOUNT_ID\"}" \ + -d "{\"nftContract\": \"$CONTRACT2_ADDRESS\", \"tokenId\": \"$TOKEN_ID_2\"}") + +print_info "Deposit response: $DEPOSIT2_RESPONSE" + +# Check if deposit was successful +if echo "$DEPOSIT2_RESPONSE" | jq -e '.proof' > /dev/null 2>&1; then + print_success "NFT #$TOKEN_ID_2 deposited successfully with CryptoPunks domain parameter" +elif echo "$DEPOSIT2_RESPONSE" | jq -e '.errors' > /dev/null 2>&1; then + ERRORS=$(echo "$DEPOSIT2_RESPONSE" | jq -r '.errors[]') + print_error "Failed to deposit NFT #$TOKEN_ID_2: $ERRORS" + exit 1 +else + print_success "NFT #$TOKEN_ID_2 deposit transaction submitted" +fi +echo "" + +# Step 12: Verify commitments for CryptoKitties domain +echo "Step 12: Fetching commitments for CryptoKitties domain..." +sleep 2 # Wait for commitments to be indexed +COMMITMENTS1_RESPONSE=$(curl -s -X POST "$BASE_URL/getCommitmentsByVariableName" \ + -H "Content-Type: application/json" \ + -H "x-saas-context: {\"accountId\": \"$ACCOUNT_ID\"}" \ + -d "{\"name\": \"tokenOwners\", \"domainParameters\": {\"nftContract\": \"$CONTRACT1_ADDRESS\"}}") + +COMMITMENTS1_COUNT=$(echo "$COMMITMENTS1_RESPONSE" | jq '.commitments | length') +print_info "CryptoKitties domain commitments: $COMMITMENTS1_COUNT" +if [ "$COMMITMENTS1_COUNT" -gt 0 ]; then + print_success "Found commitments for CryptoKitties domain" +else + print_error "No commitments found for CryptoKitties domain" +fi +echo "" + +# Step 13: Verify commitments for CryptoPunks domain +echo "Step 13: Fetching commitments for CryptoPunks domain..." +COMMITMENTS2_RESPONSE=$(curl -s -X POST "$BASE_URL/getCommitmentsByVariableName" \ + -H "Content-Type: application/json" \ + -H "x-saas-context: {\"accountId\": \"$ACCOUNT_ID\"}" \ + -d "{\"name\": \"tokenOwners\", \"domainParameters\": {\"nftContract\": \"$CONTRACT2_ADDRESS\"}}") + +COMMITMENTS2_COUNT=$(echo "$COMMITMENTS2_RESPONSE" | jq '.commitments | length') +print_info "CryptoPunks domain commitments: $COMMITMENTS2_COUNT" +if [ "$COMMITMENTS2_COUNT" -gt 0 ]; then + print_success "Found commitments for CryptoPunks domain" +else + print_error "No commitments found for CryptoPunks domain" +fi +echo "" + +# Step 14: Verify domain isolation after deposits +echo "Step 14: Verifying domain parameter isolation after deposits..." +print_info "CryptoKitties domain ($CONTRACT1_ADDRESS): $COMMITMENTS1_COUNT commitments" +print_info "CryptoPunks domain ($CONTRACT2_ADDRESS): $COMMITMENTS2_COUNT commitments" + +if [ "$COMMITMENTS1_COUNT" -gt 0 ] && [ "$COMMITMENTS2_COUNT" -gt 0 ]; then + print_success "Domain parameter isolation confirmed! Each nftContract has separate state" +else + print_error "Domain isolation verification incomplete" +fi +echo "" + +# Step 15: Register second user (Bob) for transfers +echo "Step 15: Registering second user (Bob) for transfers..." +REGISTER_BOB_RESPONSE=$(curl -s -X POST "$BASE_URL/registerKeys" \ + -H "Content-Type: application/json" \ + -H "x-saas-context: {\"accountId\": \"user-bob\"}" \ + -d '{}') + +BOB_ADDRESS=$(echo "$REGISTER_BOB_RESPONSE" | jq -r '.address') +print_info "Response: $REGISTER_BOB_RESPONSE" +if [ "$BOB_ADDRESS" != "null" ] && [ -n "$BOB_ADDRESS" ]; then + print_success "Bob's keys registered. Address: $BOB_ADDRESS" +else + print_error "Failed to register Bob's keys" + exit 1 +fi +echo "" + +# Step 16: Transfer CryptoKitties NFT from Alice to Bob +echo "Step 16: Transferring NFT #$TOKEN_ID_1 from Alice to Bob (CryptoKitties domain)..." +TRANSFER1_RESPONSE=$(curl -s -X POST "$BASE_URL/transfer" \ + -H "Content-Type: application/json" \ + -H "x-saas-context: {\"accountId\": \"$ACCOUNT_ID\"}" \ + -d "{\"tokenId\": $TOKEN_ID_1, \"recipient\": \"$BOB_ADDRESS\", \"nftContract\": \"$CONTRACT1_ADDRESS\"}") + +print_info "Transfer response: $TRANSFER1_RESPONSE" +if echo "$TRANSFER1_RESPONSE" | jq -e '.tx' > /dev/null 2>&1; then + print_success "NFT #$TOKEN_ID_1 transfer transaction submitted (CryptoKitties)" +else + print_error "Failed to transfer NFT #$TOKEN_ID_1 (CryptoKitties)" +fi +echo "" + +# Step 17: Transfer CryptoPunks NFT from Alice to Bob +echo "Step 17: Transferring NFT #$TOKEN_ID_2 from Alice to Bob (CryptoPunks domain)..." +TRANSFER2_RESPONSE=$(curl -s -X POST "$BASE_URL/transfer" \ + -H "Content-Type: application/json" \ + -H "x-saas-context: {\"accountId\": \"$ACCOUNT_ID\"}" \ + -d "{\"tokenId\": $TOKEN_ID_2, \"recipient\": \"$BOB_ADDRESS\", \"nftContract\": \"$CONTRACT2_ADDRESS\"}") + +print_info "Transfer response: $TRANSFER2_RESPONSE" +if echo "$TRANSFER2_RESPONSE" | jq -e '.tx' > /dev/null 2>&1; then + print_success "NFT #$TOKEN_ID_2 transfer transaction submitted (CryptoPunks)" +else + print_error "Failed to transfer NFT #$TOKEN_ID_2 (CryptoPunks)" +fi +echo "" + +# Step 18: Verify Alice's commitments after transfers (should be nullified) +echo "Step 18: Verifying Alice's commitments after transfers..." +sleep 2 # Wait for commitments to be updated +ALICE_COMMITMENTS1_RESPONSE=$(curl -s -X POST "$BASE_URL/getCommitmentsByVariableName" \ + -H "Content-Type: application/json" \ + -H "x-saas-context: {\"accountId\": \"$ACCOUNT_ID\"}" \ + -d "{\"name\": \"tokenOwners\", \"domainParameters\": {\"nftContract\": \"$CONTRACT1_ADDRESS\"}}") + +ALICE_COMMITMENTS1_COUNT=$(echo "$ALICE_COMMITMENTS1_RESPONSE" | jq '[.commitments[] | select(.isNullified == false)] | length') +print_info "Alice's active CryptoKitties commitments: $ALICE_COMMITMENTS1_COUNT" + +ALICE_COMMITMENTS2_RESPONSE=$(curl -s -X POST "$BASE_URL/getCommitmentsByVariableName" \ + -H "Content-Type: application/json" \ + -H "x-saas-context: {\"accountId\": \"$ACCOUNT_ID\"}" \ + -d "{\"name\": \"tokenOwners\", \"domainParameters\": {\"nftContract\": \"$CONTRACT2_ADDRESS\"}}") + +ALICE_COMMITMENTS2_COUNT=$(echo "$ALICE_COMMITMENTS2_RESPONSE" | jq '[.commitments[] | select(.isNullified == false)] | length') +print_info "Alice's active CryptoPunks commitments: $ALICE_COMMITMENTS2_COUNT" + +if [ "$ALICE_COMMITMENTS1_COUNT" -eq 0 ] && [ "$ALICE_COMMITMENTS2_COUNT" -eq 0 ]; then + print_success "Alice's commitments correctly nullified after transfers" +else + print_error "Alice still has active commitments after transfers" +fi +echo "" + +# Step 19: Verify Bob's commitments after transfers +echo "Step 19: Verifying Bob's commitments after transfers..." +BOB_COMMITMENTS1_RESPONSE=$(curl -s -X POST "$BASE_URL/getCommitmentsByVariableName" \ + -H "Content-Type: application/json" \ + -H "x-saas-context: {\"accountId\": \"user-bob\"}" \ + -d "{\"name\": \"tokenOwners\", \"domainParameters\": {\"nftContract\": \"$CONTRACT1_ADDRESS\"}}") + +BOB_COMMITMENTS1_COUNT=$(echo "$BOB_COMMITMENTS1_RESPONSE" | jq '[.commitments[] | select(.isNullified == false)] | length') +print_info "Bob's active CryptoKitties commitments: $BOB_COMMITMENTS1_COUNT" + +BOB_COMMITMENTS2_RESPONSE=$(curl -s -X POST "$BASE_URL/getCommitmentsByVariableName" \ + -H "Content-Type: application/json" \ + -H "x-saas-context: {\"accountId\": \"user-bob\"}" \ + -d "{\"name\": \"tokenOwners\", \"domainParameters\": {\"nftContract\": \"$CONTRACT2_ADDRESS\"}}") + +BOB_COMMITMENTS2_COUNT=$(echo "$BOB_COMMITMENTS2_RESPONSE" | jq '[.commitments[] | select(.isNullified == false)] | length') +print_info "Bob's active CryptoPunks commitments: $BOB_COMMITMENTS2_COUNT" + +if [ "$BOB_COMMITMENTS1_COUNT" -eq 1 ] && [ "$BOB_COMMITMENTS2_COUNT" -eq 1 ]; then + print_success "Bob received commitments for both NFTs correctly" +else + print_error "Bob's commitments not correct after transfers" +fi +echo "" + +# Step 20: Final domain isolation verification +echo "Step 20: Final multi nft verification..." +print_info "CryptoKitties domain ($CONTRACT1_ADDRESS): Bob has $BOB_COMMITMENTS1_COUNT commitments" +print_info "CryptoPunks domain ($CONTRACT2_ADDRESS): Bob has $BOB_COMMITMENTS2_COUNT commitments" + +if [ "$BOB_COMMITMENTS1_COUNT" -eq 1 ] && [ "$BOB_COMMITMENTS2_COUNT" -eq 1 ]; then + print_success "Namespace isolation maintained through transfers!" +else + print_error "Namespace isolation verification incomplete after transfers" +fi +echo "" + + diff --git a/src/boilerplate/common/commitment-storage.mjs b/src/boilerplate/common/commitment-storage.mjs index 623d59ab..8359fe3b 100644 --- a/src/boilerplate/common/commitment-storage.mjs +++ b/src/boilerplate/common/commitment-storage.mjs @@ -30,20 +30,34 @@ export function formatCommitment (commitment, context) { preimage.value = generalise(commitment.preimage.value).all ? generalise(commitment.preimage.value).all.integer : generalise(commitment.preimage.value).integer + + // Format domain parameters if they exist + const domainParameters = commitment.domainParameters + ? Object.fromEntries( + Object.entries(commitment.domainParameters).map(([key, value]) => [ + key, + generalise(value).integer + ]) + ) + : null; + data = { _id: commitment.hash.hex(32), name: commitment.name, source: commitment.source, mappingKey: commitment.mappingKey ? commitment.mappingKey : null, + domainParameters, secretKey: commitment.secretKey ? commitment.secretKey.hex(32) : null, preimage, isNullified: commitment.isNullified, nullifier: commitment.secretKey ? nullifierHash.hex(32) : null, accountId: context?.accountId || null, } - logger.debug(`Storing commitment ${data._id}${context?.accountId ? ` for accountId: ${context.accountId}` : ''}`) + logger.debug(`Storing commitment ${data._id}${context?.accountId ? ` for accountId: ${context.accountId}` : ''}${domainParameters ? ` with domain parameters: ${JSON.stringify(domainParameters)}` : ''}`) } catch (error) { - console.error('Error --->', error) + console.error('Error formatting commitment --->', error) + console.error('Commitment object:', JSON.stringify(commitment, null, 2)) + throw error } return data } @@ -56,7 +70,18 @@ export async function persistCommitment (data) { // function to format a commitment for a mongo db and store it export async function storeCommitment (commitment, context) { const data = formatCommitment(commitment, context) - return persistCommitment(data) + if (!data) { + console.error('formatCommitment returned undefined/null data') + throw new Error('Failed to format commitment') + } + try { + const result = await persistCommitment(data) + logger.debug(`Successfully persisted commitment ${data._id}`) + return result + } catch (error) { + console.error('Error persisting commitment:', error) + throw error + } } // function to retrieve commitment with a specified stateVarId @@ -87,12 +112,20 @@ export async function getCurrentWholeCommitment(id, accountId) { } // function to retrieve commitment with a specified stateName -export async function getCommitmentsByState(name, mappingKey = null, accountId = null) { +export async function getCommitmentsByState(name, mappingKey = null, accountId = null, domainParameters = null) { const connection = await mongo.connection(MONGO_URL); const db = connection.db(COMMITMENTS_DB); const query = { name: name }; if (accountId) query['accountId'] = accountId; if (mappingKey) query['mappingKey'] = generalise(mappingKey).integer; + + // Add domain parameter filters if provided + if (domainParameters) { + for (const [key, value] of Object.entries(domainParameters)) { + query[`domainParameters.${key}`] = generalise(value).integer; + } + } + const commitments = await db .collection(COMMITMENTS_COLLECTION) .find(query) @@ -142,12 +175,20 @@ export async function getBalance(accountId) { return sumOfValues; } -export async function getBalanceByState(name, mappingKey = null, accountId=null) { +export async function getBalanceByState(name, mappingKey = null, accountId=null, domainParameters = null) { const connection = await mongo.connection(MONGO_URL); const db = connection.db(COMMITMENTS_DB); const query = { name: name }; if (accountId) query['accountId'] = accountId; if (mappingKey) query['mappingKey'] = generalise(mappingKey).integer; + + // Add domain parameter filters if provided + if (domainParameters) { + for (const [key, value] of Object.entries(domainParameters)) { + query[`domainParameters.${key}`] = generalise(value).integer; + } + } + const commitments = await db .collection(COMMITMENTS_COLLECTION) .find(query) diff --git a/src/boilerplate/common/contract.mjs b/src/boilerplate/common/contract.mjs index 749375c1..2cc60455 100644 --- a/src/boilerplate/common/contract.mjs +++ b/src/boilerplate/common/contract.mjs @@ -73,7 +73,13 @@ export async function getContractInstance(contractName, deployedAddress) { export async function getContractBytecode(contractName) { const contractInterface = await getContractInterface(contractName); - return contractInterface.evm.bytecode.object; + // Support both Hardhat format (bytecode) and Truffle/Solc format (evm.bytecode.object) + if (contractInterface.bytecode) { + return contractInterface.bytecode; + } else if (contractInterface.evm?.bytecode?.object) { + return contractInterface.evm.bytecode.object; + } + throw new Error(`Bytecode not found for contract ${contractName}`); } export async function deploy( diff --git a/src/boilerplate/common/migrations/metadata.js b/src/boilerplate/common/migrations/metadata.js index 6d80496b..b07c31ec 100644 --- a/src/boilerplate/common/migrations/metadata.js +++ b/src/boilerplate/common/migrations/metadata.js @@ -48,11 +48,15 @@ function saveMetadata ( // console.log("hardhatArtifactPath: ", hardhatArtifactPath); const compilationData = fs.readFileSync(hardhatArtifactPath, 'utf-8') - const abi = JSON.parse(compilationData).abi - const contractNameFromHardhat = JSON.parse(compilationData).contractName + const compiledContract = JSON.parse(compilationData) + const abi = compiledContract.abi + const contractNameFromHardhat = compiledContract.contractName + const bytecode = compiledContract.bytecode deployedMetadata.abi = abi deployedMetadata.contractName = contractNameFromHardhat + // Save bytecode for runtime deployment (needed for deployNFT endpoint) + deployedMetadata.bytecode = bytecode deployedPositionMetadata.address = contractDeployedAddress deployedPositionMetadata.blockNumber = blockNumber deployedPositionMetadata.transactionHash = transactionHash diff --git a/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts b/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts index e8fa3273..adf55051 100644 --- a/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts +++ b/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts @@ -283,11 +283,12 @@ export function buildBoilerplateNode(nodeType: string, fields: any = {}): any { }; } case 'ReadPreimage': { - const { contractName, privateStates = {} } = fields; + const { contractName, privateStates = {}, inputParameters = [] } = fields; return { nodeType, privateStates, contractName, + inputParameters, }; } case 'WritePreimage': { diff --git a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts index f6bb4ab5..d5e2481d 100644 --- a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts +++ b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts @@ -634,7 +634,8 @@ sendTransaction = { burnedOnly, reinitialisedOnly, structProperties, - isConstructor + isConstructor, + perParameters, }): string[] { let value; const errorCatch = `\n console.log("Added commitment", newCommitment.hex(32)); @@ -647,6 +648,12 @@ sendTransaction = { ); } }`; + + // Generate domain parameters object if perParameters exist + const domainParamsCode = perParameters && perParameters.length > 0 + ? `domainParameters: { ${perParameters.map(p => `${p.name}: ${p.name}_init`).join(', ')} },\n ` + : ''; + switch (stateType) { case 'increment': value = structProperties ? `{ ${structProperties.map((p, i) => `${p}: ${stateName}_newCommitmentValue.integer[${i}]`)} }` : `${stateName}_newCommitmentValue`; @@ -655,7 +662,7 @@ sendTransaction = { hash: ${stateName}_newCommitment, name: '${mappingName}', mappingKey: ${mappingKey === `` ? `null` : `${mappingKey}`}, - preimage: { + ${domainParamsCode}preimage: { \tstateVarId: generalise(${stateName}_stateVarId), \tvalue: ${value}, \tsalt: ${stateName}_newSalt, @@ -674,7 +681,7 @@ sendTransaction = { hash: ${stateName}_2_newCommitment, name: '${mappingName}', mappingKey: ${mappingKey === `` ? `null` : `${mappingKey}`}, - preimage: { + ${domainParamsCode}preimage: { \tstateVarId: generalise(${stateName}_stateVarId), \tvalue: ${value}, \tsalt: ${stateName}_2_newSalt, @@ -713,7 +720,7 @@ sendTransaction = { hash: ${stateName}_newCommitment, name: '${mappingName}', mappingKey: ${mappingKey === `` ? `null` : `${mappingKey}`}, - preimage: { + ${domainParamsCode}preimage: { \tstateVarId: generalise(${stateName}_stateVarId), \tvalue: ${value}, \tsalt: ${stateName}_newSalt, @@ -857,22 +864,22 @@ integrationApiServicesBoilerplate = { export async function service_getBalanceByState(req, res, next) { try { - const { name, mappingKey } = req.body; + const { name, mappingKey, domainParameters } = req.body; const accountId = req.saasContext?.accountId; - const balance = await getBalanceByState(name, mappingKey, accountId); + const balance = await getBalanceByState(name, mappingKey, accountId, domainParameters); res.send( {"totalBalance": balance} ); } catch (error) { console.error("Error in calculation :", error); res.status(500).send({ error: err.message }); } } - - + + export async function service_getCommitmentsByState(req, res, next) { try { - const { name, mappingKey } = req.body; + const { name, mappingKey, domainParameters } = req.body; const accountId = req.saasContext?.accountId; - const commitments = await getCommitmentsByState(name, mappingKey, accountId); + const commitments = await getCommitmentsByState(name, mappingKey, accountId, domainParameters); res.send({ commitments }); await sleep(10); } catch (err) { @@ -987,7 +994,7 @@ integrationApiServicesBoilerplate = { export async function service_mintNFT(req, res, next) { try { - const { tokenId } = req.body; + const { tokenId, nftContract } = req.body; SAAS_CONTEXT_HANDLING const keyManager = KeyManager.getInstance(); @@ -1001,7 +1008,8 @@ integrationApiServicesBoilerplate = { } const { getContractAddress, getContractInterface } = await import('./common/contract.mjs'); - const erc721Address = await getContractAddress('ERC721'); + // Use provided nftContract address or fall back to default ERC721 + const erc721Address = nftContract || await getContractAddress('ERC721'); const erc721Interface = await getContractInterface('ERC721'); const Web3 = await import('./common/web3.mjs'); const web3 = Web3.default.connection(); @@ -1042,7 +1050,7 @@ integrationApiServicesBoilerplate = { export async function service_approveNFT(req, res, next) { try { - const { tokenId } = req.body; + const { tokenId, nftContract } = req.body; SAAS_CONTEXT_HANDLING const keyManager = KeyManager.getInstance(); @@ -1056,7 +1064,8 @@ integrationApiServicesBoilerplate = { } const { getContractAddress, getContractInterface } = await import('./common/contract.mjs'); - const erc721Address = await getContractAddress('ERC721'); + // Use provided nftContract address or fall back to default ERC721 + const erc721Address = nftContract || await getContractAddress('ERC721'); const erc721Interface = await getContractInterface('ERC721'); const Web3 = await import('./common/web3.mjs'); const web3 = Web3.default.connection(); @@ -1107,52 +1116,84 @@ integrationApiServicesBoilerplate = { } } - export async function service_deployNFT(req, res, next) { + export async function service_deployNFT(req, res, next) { try { const { name, symbol } = req.body; SAAS_CONTEXT_HANDLING - const { getContractInterface } = await import('./common/contract.mjs'); - const erc721Interface = await getContractInterface('ERC721'); + const keyManager = KeyManager.getInstance(); + let keys = await keyManager.getKeys(SAAS_CONTEXT_PARAM); + + if (!keys) { + return res.send({ + success: false, + message: 'No keys found. Please call /registerKeys first.' + }); + } + + if (!name || !symbol) { + return res.send({ + errors: ['name and symbol are required'] + }); + } + + logger.info(\`Deploying new ERC721 contract: \${name} (\${symbol})\`); + + const { getContractInterface, getContractBytecode } = await import('./common/contract.mjs'); const Web3 = await import('./common/web3.mjs'); const web3 = Web3.default.connection(); - const accounts = await web3.eth.getAccounts(); - const defaultAccount = accounts[0]; + // Get the ERC721 contract interface and bytecode + const erc721Interface = await getContractInterface('ERC721'); + const erc721Bytecode = await getContractBytecode('ERC721'); - logger.info(\`Deploying new ERC721 contract: \${name} (\${symbol})\`); + // Create a new contract instance + const erc721Contract = new web3.eth.Contract(erc721Interface.abi); - const ERC721Contract = new web3.eth.Contract(erc721Interface.abi); - const deployTx = ERC721Contract.deploy({ - data: erc721Interface.bytecode, - arguments: [name || 'TestNFT', symbol || 'TNFT'] + // Deploy the contract + const deployTx = erc721Contract.deploy({ + data: erc721Bytecode, + arguments: [name, symbol] }); - const gas = await deployTx.estimateGas({ from: defaultAccount }); - const deployedContract = await deployTx.send({ - from: defaultAccount, - gas: Math.floor(gas * 1.2) // Add 20% buffer - }); + const config = await import('config'); + + // Auto-fund tenant address if needed (same as other endpoints) + const { autoFundIfNeeded } = await import('./common/gas-funding.mjs'); + await autoFundIfNeeded(keys.ethPK, '0.1', '0.5'); + + // Estimate gas for deployment + const gas = await deployTx.estimateGas({ from: keys.ethPK }); - const contractAddress = deployedContract.options.address; + // Send the deployment transaction using tenant's keys + const txParams = { + from: keys.ethPK, + data: deployTx.encodeABI(), + gas: Math.floor(Number(gas) * 1.2), // Add 20% buffer + gasPrice: config.default.web3.options.defaultGasPrice, + chainId: await web3.eth.net.getId() + }; + + const signed = await web3.eth.accounts.signTransaction(txParams, keys.ethSK); + const receipt = await web3.eth.sendSignedTransaction(signed.rawTransaction); - logger.info(\`ERC721 deployed at: \${contractAddress}\`); + const deployedAddress = receipt.contractAddress; + + logger.info(\`ERC721 contract deployed at: \${deployedAddress}\`); res.send({ success: true, - contractAddress: contractAddress, - name: name || 'TestNFT', - symbol: symbol || 'TNFT', - txHash: deployedContract._requestManager.provider.lastJsonRpcResponse?.result + contractAddress: deployedAddress, + name: name, + symbol: symbol, + txHash: receipt.transactionHash }); } catch (err) { logger.error('Failed to deploy NFT contract:', err); res.send({ errors: [err.message] }); } - }` - - - ; + } + `; } @@ -1177,9 +1218,9 @@ integrationApiRoutesBoilerplate = { commitmentRoutes(): string { return `// commitment getter routes router.get("/getAllCommitments", service_allCommitments); - router.get("/getCommitmentsByVariableName", service_getCommitmentsByState); + router.post("/getCommitmentsByVariableName", service_getCommitmentsByState); router.get("/getBalance", service_getBalance); - router.get("/getBalanceByState", service_getBalanceByState); + router.post("/getBalanceByState", service_getBalanceByState); router.post("/getSharedKeys", service_getSharedKeys); // backup route router.post("/backupDataRetriever", service_backupData); diff --git a/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts b/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts index 87cbf8d4..0eecb9ba 100644 --- a/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts +++ b/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts @@ -218,8 +218,9 @@ export const generateProofBoilerplate = (node: any) => { const stateVarIdLines = !stateNode.localMappingKey && stateNode.isMapping && !(node.parameters.includes(stateNode.stateVarId[1])) && !(node.parameters.includes(stateNode.stateVarId[2])) && !msgSenderParamAndMappingKey && !msgValueParamAndMappingKey && !constantMappingKey ? [`\n\t\t\t\t\t\t\t\t${stateName}_stateVarId_key.integer,`] - : []; + : []; // we add any extra params the circuit needs + // Preserve the order from node.parameters (which matches the circuit parameter order) node.parameters .filter((para: string) => { if (privateStateNames.includes(para)) return false; @@ -228,15 +229,9 @@ export const generateProofBoilerplate = (node: any) => { }) .forEach((para: string) => { const transformed = transformToIntegerAccess(para); - if (para === 'msgValue') { - parameters.unshift(`\t${transformed},`); - } else if (para === 'msgSender') { - parameters.unshift(`\t${transformed},`); - } else { - parameters.push(`\t${transformed},`); - } + parameters.push(`\t${transformed},`); }); - + // then we build boilerplate code per state switch (stateNode.isWhole) { case true: @@ -385,14 +380,28 @@ export const preimageBoilerPlate = (node: any) => { preimageParams.push(`\t${privateStateName}: 0,`); // ownership (PK in commitment) + // For reinitialisable states (transfers), we need to use the assignment RHS as the new owner + // not the require statement owner (which is the old owner) const newOwner = stateNode.isOwned ? stateNode.owner : null; let newOwnerStatment: string; switch (newOwner) { case null: - if(stateNode.isSharedSecret) - newOwnerStatment = `_${privateStateName}_newOwnerPublicKey === 0 ? sharedPublicKey : ${privateStateName}_newOwnerPublicKey;`; - else - newOwnerStatment = `_${privateStateName}_newOwnerPublicKey === 0 ? publicKey : ${privateStateName}_newOwnerPublicKey;`; + // Check if there's a 'recipient' parameter in the function (including secret parameters) + const hasRecipientParam = node.inputParameters && node.inputParameters.includes('recipient'); + + if (hasRecipientParam && stateNode.reinitialisable) { + // For transfer functions with a recipient parameter, look up the recipient's public key + newOwnerStatment = `_${privateStateName}_newOwnerPublicKey === 0 ? generalise(await instance.methods.zkpPublicKeys(recipient.hex ? recipient.hex(20) : generalise(recipient).hex(20)).call()) : ${privateStateName}_newOwnerPublicKey; + \nif (_${privateStateName}_newOwnerPublicKey === 0 && ${privateStateName}_newOwnerPublicKey.integer === 0) { + \nconsole.log('WARNING: Public key for recipient address not found - using your public key'); + \n${privateStateName}_newOwnerPublicKey = ${stateNode.isSharedSecret ? 'sharedPublicKey' : 'publicKey'}; + \n} + \n${privateStateName}_newOwnerPublicKey = generalise(${privateStateName}_newOwnerPublicKey);`; + } else if(stateNode.isSharedSecret) { + newOwnerStatment = `_${privateStateName}_newOwnerPublicKey === 0 ? sharedPublicKey : ${privateStateName}_newOwnerPublicKey;`; + } else { + newOwnerStatment = `_${privateStateName}_newOwnerPublicKey === 0 ? publicKey : ${privateStateName}_newOwnerPublicKey;`; + } break; case 'msg': if (privateStateName.includes('msg')) { @@ -402,7 +411,19 @@ export const preimageBoilerPlate = (node: any) => { newOwnerStatment = `generalise(await instance.methods.zkpPublicKeys(${stateNode.stateVarId[1]}.hex(20)).call()); // address should be registered`; } else if (stateNode.mappingOwnershipType === 'value') { if (stateNode.reinitialisable){ - newOwnerStatment = `_${privateStateName}_newOwnerPublicKey === 0 ? publicKey : ${privateStateName}_newOwnerPublicKey;`; + // For reinitialisable states (transfers), look up the new owner's public key from the contract + // Check if there's a parameter that could be the new owner (like 'recipient') + const hasRecipientParam = node.inputParameters && node.inputParameters.includes('recipient'); + if (hasRecipientParam) { + newOwnerStatment = `_${privateStateName}_newOwnerPublicKey === 0 ? generalise(await instance.methods.zkpPublicKeys(recipient.hex ? recipient.hex(20) : generalise(recipient).hex(20)).call()) : ${privateStateName}_newOwnerPublicKey; + \nif (_${privateStateName}_newOwnerPublicKey === 0 && ${privateStateName}_newOwnerPublicKey.integer === 0) { + \nconsole.log('WARNING: Public key for recipient address not found - using your public key'); + \n${privateStateName}_newOwnerPublicKey = ${stateNode.isSharedSecret ? 'sharedPublicKey' : 'publicKey'}; + \n} + \n${privateStateName}_newOwnerPublicKey = generalise(${privateStateName}_newOwnerPublicKey);`; + } else { + newOwnerStatment = `_${privateStateName}_newOwnerPublicKey === 0 ? ${stateNode.isSharedSecret ? 'sharedPublicKey' : 'publicKey'} : ${privateStateName}_newOwnerPublicKey;`; + } } else { // TODO test below // if the private state is an address (as here) its still in eth form - we need to convert @@ -426,7 +447,14 @@ export const preimageBoilerPlate = (node: any) => { if (!stateNode.ownerIsSecret && !stateNode.ownerIsParam) { newOwnerStatment = `_${privateStateName}_newOwnerPublicKey === 0 ? generalise(await instance.methods.zkpPublicKeys(await instance.methods.${newOwner}().call()).call()) : ${privateStateName}_newOwnerPublicKey;`; } else if (stateNode.ownerIsParam && newOwner) { - newOwnerStatment = `_${privateStateName}_newOwnerPublicKey === 0 ? ${newOwner} : ${privateStateName}_newOwnerPublicKey;`; + // Owner is a parameter - need to look up the public key from the contract + // The parameter could be an address (like 'recipient') or already a public key + newOwnerStatment = `_${privateStateName}_newOwnerPublicKey === 0 ? generalise(await instance.methods.zkpPublicKeys(${newOwner}.hex ? ${newOwner}.hex(20) : generalise(${newOwner}).hex(20)).call()) : ${privateStateName}_newOwnerPublicKey; + \nif (_${privateStateName}_newOwnerPublicKey === 0 && ${privateStateName}_newOwnerPublicKey.integer === 0) { + \nconsole.log('WARNING: Public key for ${newOwner} address not found - using your public key'); + \n${privateStateName}_newOwnerPublicKey = ${stateNode.isSharedSecret ? 'sharedPublicKey' : 'publicKey'}; + \n} + \n${privateStateName}_newOwnerPublicKey = generalise(${privateStateName}_newOwnerPublicKey);`; } else { // is secret - we just use the users to avoid revealing the secret owner if(stateNode.isSharedSecret) @@ -727,6 +755,7 @@ export const OrchestrationCodeBoilerPlate: any = (node: any) => { structProperties: stateNode.structProperties, isConstructor: node.isConstructor, reinitialisedOnly: false, + perParameters: stateNode.perParameters, })); break; @@ -745,6 +774,7 @@ export const OrchestrationCodeBoilerPlate: any = (node: any) => { structProperties: stateNode.structProperties, isConstructor: node.isConstructor, reinitialisedOnly: stateNode.reinitialisedOnly, + perParameters: stateNode.perParameters, })); break; @@ -765,6 +795,7 @@ export const OrchestrationCodeBoilerPlate: any = (node: any) => { reinitialisedOnly: stateNode.reinitialisedOnly, structProperties: stateNode.structProperties, isConstructor: node.isConstructor, + perParameters: stateNode.perParameters, })); } } diff --git a/src/codeGenerators/orchestration/files/toOrchestration.ts b/src/codeGenerators/orchestration/files/toOrchestration.ts index 58a2c716..cf7c0ac1 100644 --- a/src/codeGenerators/orchestration/files/toOrchestration.ts +++ b/src/codeGenerators/orchestration/files/toOrchestration.ts @@ -456,18 +456,18 @@ const prepareMigrationsFile = (file: localFile, node: any) => { } }); // we collect any imported contracts which must be migrated - if (node.contractImports && constructorParamsIncludesAddr) { + if (node.contractImports) { node.contractImports.forEach((importObj: any) => { // read each imported contract if(!fs.existsSync(`./contracts/${importObj.absolutePath}`)){ logger.warn(`Please Make Sure you Deploy all the imports before testing the zApp.`); return; - } + } const importedContract = fs.readFileSync( `./contracts/${importObj.absolutePath}`, 'utf8', ); - + let importedContractName = path.basename( importObj.absolutePath, path.extname(importObj.absolutePath), @@ -526,56 +526,60 @@ const prepareMigrationsFile = (file: localFile, node: any) => { const erc1155 = await ERC1155.deploy() \n await erc1155.waitForDeployment() \n erc1155Address = await erc1155.getAddress() \n - console.log('ERC1155 deployed to:', erc1155Address) \n + console.log('ERC1155 deployed to:', erc1155Address) \n blockNumber = await hre.ethers.provider.getBlockNumber(); \n deployTx = await erc1155.deploymentTransaction().wait() \n saveMetadata(erc1155Address, 'ERC1155Token', "/Escrow-imports", chainId, blockNumber, deployTx.hash) \n \n`; - break; + break; } - } + } } - if ( - importedContractName === 'ERC20' || - importedContractName === 'ERC721' || importedContractName === 'ERC1155' - ) { - // for each address in the shield contract constructor... - constructorAddrParams.forEach(name => { - if ( - name - .toLowerCase() - .includes(importedContractName.substring(1).toLowerCase()) || - importedContractName - .substring(1) - .toLowerCase() - .includes(name.toLowerCase()) - ) { - // if that address is of the current importedContractName, we add it to the migration arguments - const index = constructorParamNames.indexOf(name); - constructorParamNames[index] = `${importedContractName.toLowerCase()}Address`; - } - }); - } else { + // Only update constructor params if we have address params in the constructor + if (constructorParamsIncludesAddr) { + if ( + importedContractName === 'ERC20' || + importedContractName === 'ERC721' || importedContractName === 'ERC1155' + ) { // for each address in the shield contract constructor... - constructorAddrParams.forEach(name => { - if ( - name - .toLowerCase() - .includes(importedContractName.substring(1).toLowerCase()) || - importedContractName - .substring(1) - .toLowerCase() - .includes(name.toLowerCase()) - ) { - // if that address is of the current importedContractName, we add it to the migration arguments - const index = constructorParamNames.indexOf(name); - constructorParamNames[index] = `${importedContractName}.address`; - } - }); + constructorAddrParams.forEach(name => { + if ( + name + .toLowerCase() + .includes(importedContractName.substring(1).toLowerCase()) || + importedContractName + .substring(1) + .toLowerCase() + .includes(name.toLowerCase()) + ) { + // if that address is of the current importedContractName, we add it to the migration arguments + const index = constructorParamNames.indexOf(name); + constructorParamNames[index] = `${importedContractName.toLowerCase()}Address`; + } + }); + } else { + // for each address in the shield contract constructor... + constructorAddrParams.forEach(name => { + if ( + name + .toLowerCase() + .includes(importedContractName.substring(1).toLowerCase()) || + importedContractName + .substring(1) + .toLowerCase() + .includes(name.toLowerCase()) + ) { + // if that address is of the current importedContractName, we add it to the migration arguments + const index = constructorParamNames.indexOf(name); + constructorParamNames[index] = `${importedContractName}.address`; + } + }); + } } } }); - } else if(constructorParamsIncludesAddr) { + } + if(constructorParamsIncludesAddr) { // for each address in the shield contract constructor... constructorAddrParams.forEach(name => { // we have an address input which is likely not a another contract diff --git a/src/transformers/visitors/common.ts b/src/transformers/visitors/common.ts index b480c3b8..9415f4de 100644 --- a/src/transformers/visitors/common.ts +++ b/src/transformers/visitors/common.ts @@ -26,8 +26,11 @@ export const initialiseOrchestrationBoilerplateNodes = (fnIndicator: FunctionDef }); if (fnIndicator.oldCommitmentAccessRequired || fnIndicator.internalFunctionoldCommitmentAccessRequired) newNodes.initialisePreimageNode = buildNode('InitialisePreimage'); + // Extract function parameter names for use in boilerplate generation + const inputParameters = node.parameters?.parameters?.map((p: any) => p.name) || []; newNodes.readPreimageNode = buildNode('ReadPreimage', { contractName, + inputParameters, }); if (fnIndicator.nullifiersRequired || fnIndicator.containsAccessedOnlyState || fnIndicator.internalFunctionInteractsWithSecret) { newNodes.membershipWitnessNode = buildNode('MembershipWitness', { diff --git a/src/transformers/visitors/toCircuitVisitor.ts b/src/transformers/visitors/toCircuitVisitor.ts index b1cbfe47..3227c3e6 100644 --- a/src/transformers/visitors/toCircuitVisitor.ts +++ b/src/transformers/visitors/toCircuitVisitor.ts @@ -506,12 +506,11 @@ const visitor = { if (node.kind === 'constructor' && state.constructorStatements && state.constructorStatements[0]) newFunctionDefinitionNode.body.statements.unshift(...state.constructorStatements); // We populate the boilerplate for the function - newFunctionDefinitionNode.parameters.parameters.push( - ...buildNode('Boilerplate', { - bpSection: 'parameters', - indicators, - }), - ); + const boilerplateParams = buildNode('Boilerplate', { + bpSection: 'parameters', + indicators, + }); + newFunctionDefinitionNode.parameters.parameters.push(...boilerplateParams); newFunctionDefinitionNode.body.preStatements.push( ...buildNode('Boilerplate', { @@ -1134,6 +1133,19 @@ const visitor = { declarationType, }); + // Set isPrivate for function parameters + // Domain parameters (per parameters) should be private + // Secret parameters should be private + // Regular parameters should be public (even if they interact with secrets) + if (declarationType === 'parameter') { + if (node.isPer) { + newNode.isPrivate = true; + } else { + // Only mark as private if explicitly declared as secret + newNode.isPrivate = node.isSecret; + } + } + if (path.isStruct(node)) { state.structNode = addStructDefinition(path); newNode.typeName.name = state.structNode.name; diff --git a/src/transformers/visitors/toOrchestrationVisitor.ts b/src/transformers/visitors/toOrchestrationVisitor.ts index 2cb48a9e..1fc5d3d5 100644 --- a/src/transformers/visitors/toOrchestrationVisitor.ts +++ b/src/transformers/visitors/toOrchestrationVisitor.ts @@ -993,19 +993,83 @@ const visitor = { } // this adds other values we need in the circuit - for (const param of node._newASTPointer.parameters.parameters) { + // Get the circuit AST function node to determine the correct parameter order + // The circuit AST has files named after the Solidity function (e.g., 'deposit'), + // and each file has a 'main' function + let circuitFunctionNode: any = null; + if (state.circuitAST) { + for (const file of state.circuitAST.files) { + if (file.fileName === node.name) { + // Find the main function in this file + for (const circuitNode of file.nodes) { + if (circuitNode.nodeType === 'FunctionDefinition' && circuitNode.name === 'main') { + circuitFunctionNode = circuitNode; + break; + } + } + break; + } + } + } + + // Use circuit AST parameters if available, otherwise fall back to Solidity AST + const circuitParams = circuitFunctionNode?.parameters?.parameters || node._newASTPointer.parameters.parameters; + + // Track which parameters we've already added to avoid duplicates + const addedParams = new Set(); + + for (const param of circuitParams) { + // Expand Boilerplate nodes to get domain parameters and mapping keys + if (param.nodeType === 'Boilerplate') { + // Handle different boilerplate types + if (param.bpType === 'mapping') { + // Add domain parameters first (from perParameters) + if (param.perParameters && Array.isArray(param.perParameters)) { + for (const domainParam of param.perParameters) { + if (!addedParams.has(domainParam.name)) { + newNodes.generateProofNode.parameters.push(domainParam.name); + addedParams.add(domainParam.name); + } + } + } + + // Add mapping key parameter (if not 'local') + if (param.mappingKeyTypeName && param.mappingKeyTypeName !== 'local' && param.mappingKeyName) { + if (!addedParams.has(param.mappingKeyName)) { + newNodes.generateProofNode.parameters.push(param.mappingKeyName); + addedParams.add(param.mappingKeyName); + } + } + } + // Skip newCommitment and oldCommitmentPreimage boilerplate + // The commitment parameters are added by the orchestration boilerplate generator + + continue; + } + + // Skip if already added + if (addedParams.has(param.name)) { + continue; + } + let oldParam : any ; - for(const para of node.parameters.parameters) { - if ( para?.name === param?.name ) - oldParam = para ; - break; + for(const para of node.parameters.parameters) { + if ( para?.name === param?.name ) { + oldParam = para ; + break; + } } - if (param.isPrivate || param.isSecret || param.interactsWithSecret || scope.getReferencedIndicator(oldParam)?.interactsWithSecret) { - if (param.typeName.isStruct) { + + // Include per (domain) parameters, private parameters, secret parameters, and parameters that interact with secrets + if (oldParam?.isPer || param.isPrivate || param.isSecret || param.interactsWithSecret || scope.getReferencedIndicator(oldParam)?.interactsWithSecret) { + if (param.typeName?.isStruct) { param.typeName.properties.forEach((prop: any) => { newNodes.generateProofNode.parameters.push(`${param.name}.${prop.name}${param.typeName.isConstantArray ? '.all' : ''}`); }); - } else newNodes.generateProofNode.parameters.push(`${param.name}${param.typeName.isConstantArray ? '.all' : ''}`); + } else { + newNodes.generateProofNode.parameters.push(`${param.name}${param.typeName?.isConstantArray ? '.all' : ''}`); + addedParams.add(param.name); + } } } if (state.publicInputs) { diff --git a/test/contracts/user-friendly-tests/NFT_Escrow_DomainParams.zol b/test/contracts/user-friendly-tests/NFT_Escrow_DomainParams.zol index b4c77e9b..727e7119 100644 --- a/test/contracts/user-friendly-tests/NFT_Escrow_DomainParams.zol +++ b/test/contracts/user-friendly-tests/NFT_Escrow_DomainParams.zol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: CC0 // NFT Escrow with Domain Parameters (Phase 3 Test) +// This contract supports multiple NFT contracts with proper domain isolation pragma solidity ^0.8.0; @@ -9,25 +10,28 @@ contract NFT_Escrow_DomainParams { // Domain parameter: nftContract // This allows tracking tokens from multiple NFT contracts + // Each nftContract address creates a separate cryptographic namespace // stateVarId = mimc2([mimc2([mappingId, nftContract]), tokenId]) secret mapping(uint256 => address) per(address nftContract) public tokenOwners; // Domain-scoped approvals: one namespace per nftContract secret mapping(address => address) per(address nftContract) public approvals; - IERC721 public erc721; - constructor(address _erc721) { - erc721 = IERC721(_erc721); - } + // No hardcoded ERC721 instance - we use the nftContract parameter directly + // This allows the escrow to work with any ERC721 contract // Function with domain parameter - // The per parameter must match the mapping's domain parameter - // Note: We use the stored erc721 instance for the actual transfer, - // but nftContract is used as the domain parameter for cryptographic separation + // The nftContract parameter is used for BOTH: + // 1. The actual NFT transfer (via IERC721 interface) + // 2. Cryptographic domain separation (state variable ID calculation) function deposit(per address nftContract, uint256 tokenId) public { require(nftContract != address(0), "NFT_Escrow: invalid nftContract"); - bool success = erc721.transferFrom(msg.sender, address(this), tokenId); + + // Use the nftContract parameter to instantiate the ERC721 interface + IERC721 nft = IERC721(nftContract); + bool success = nft.transferFrom(msg.sender, address(this), tokenId); require(success, "NFT_Escrow: ERC721 transfer failed"); + // stateVarId calculation includes nftContract domain parameter reinitialisable tokenOwners[tokenId] = msg.sender; } @@ -62,8 +66,12 @@ contract NFT_Escrow_DomainParams { function withdraw(per address nftContract, uint256 tokenId) public { require(nftContract != address(0), "NFT_Escrow: invalid nftContract"); require(tokenOwners[tokenId] == msg.sender); - bool success = erc721.transferFrom(address(this), msg.sender, tokenId); + + // Use the nftContract parameter to instantiate the ERC721 interface + IERC721 nft = IERC721(nftContract); + bool success = nft.transferFrom(address(this), msg.sender, tokenId); require(success, "ERC721 transfer failed"); + tokenOwners[tokenId] = address(0); } } From 5a1b4866a5b8ad394e730ace6e5dc36fd9e6ac77 Mon Sep 17 00:00:00 2001 From: Sebastian Paul Date: Wed, 26 Nov 2025 17:17:37 +0530 Subject: [PATCH 13/18] fix: burn error --- src/traverse/Binding.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/traverse/Binding.ts b/src/traverse/Binding.ts index 536c3c56..d71ddb61 100644 --- a/src/traverse/Binding.ts +++ b/src/traverse/Binding.ts @@ -599,17 +599,7 @@ export class VariableBinding extends Binding { } } } - // mapping[key] = msg.sender is owned by msg.sender => look for mapping[key] = 0 - // OR owner is some value (admin = address) => look for admin = 0 - if ( - ownerNode.name === 'msg' && - ownerNode.mappingOwnershipType === 'value' - ) { - // the owner is represented by the mapping value - we look through the modifyingPaths for 0 - this.searchModifyingPathsForZero(); - } else if (ownerBinding && ownerBinding instanceof VariableBinding) { - ownerBinding.searchModifyingPathsForZero(); - } + this.searchModifyingPathsForZero(); if (this.reinitialisable && !this.isBurned) throw new SyntaxUsageError( `The state ${this.name} has been marked as reinitialisable but we can't find anywhere to burn a commitment ready for reinitialisation.`, From 3058a15112ae2ef4f8bec18e6d57840061f780c6 Mon Sep 17 00:00:00 2001 From: Sebastian Paul Date: Mon, 1 Dec 2025 13:09:55 +0530 Subject: [PATCH 14/18] fix: activation NFT contract update --- .../common/boilerplate-docker-compose.yml | 2 +- .../solidity/raw/ContractBoilerplateGenerator.ts | 13 ++++++++++++- src/traverse/Indicator.ts | 2 +- test/contracts/user-friendly-tests/NFT_Escrow.zol | 14 ++++++++++++++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/boilerplate/common/boilerplate-docker-compose.yml b/src/boilerplate/common/boilerplate-docker-compose.yml index 29dd646a..8daecedf 100644 --- a/src/boilerplate/common/boilerplate-docker-compose.yml +++ b/src/boilerplate/common/boilerplate-docker-compose.yml @@ -58,7 +58,7 @@ services: timber: build: - context: https://ghcr.io/eyblockchain/timber-multicontract:latest + context: https://github.com/EYBlockchain/timber.git#starlight/zscaler:merkle-tree dockerfile: Dockerfile restart: on-failure depends_on: diff --git a/src/boilerplate/contract/solidity/raw/ContractBoilerplateGenerator.ts b/src/boilerplate/contract/solidity/raw/ContractBoilerplateGenerator.ts index 752d6f71..139251e2 100644 --- a/src/boilerplate/contract/solidity/raw/ContractBoilerplateGenerator.ts +++ b/src/boilerplate/contract/solidity/raw/ContractBoilerplateGenerator.ts @@ -219,7 +219,18 @@ class ContractBoilerplateGenerator { 'customInputs.length', ...(newNullifiers ? ['newNullifiers.length'] : []), ...(checkNullifiers ? ['checkNullifiers.length']: []), - ...(commitmentRoot ? ['(newNullifiers.length > 0 ? 1 : 0)'] : []), + ...(() => { + if (commitmentRoot){ + if (checkNullifiers && newNullifiers) { + return ['((newNullifiers.length + checkNullifiers.length) > 0 ? 1 : 0)']; + } + if (checkNullifiers) { + return ['((checkNullifiers.length) > 0 ? 1 : 0)']; + } + return ['(newNullifiers.length > 0 ? 1 : 0)']; + } + return []; + })(), ...(newCommitments ? ['newCommitments.length'] : []), ...(encryptionRequired ? ['encInputsLen'] : []), ].join(' + ')});`, diff --git a/src/traverse/Indicator.ts b/src/traverse/Indicator.ts index 75d70856..009fc13f 100644 --- a/src/traverse/Indicator.ts +++ b/src/traverse/Indicator.ts @@ -147,7 +147,7 @@ export class FunctionDefinitionIndicator extends ContractDefinitionIndicator { // if we have a indicator which is NOT burned, then we do need new commitments if ( stateVarIndicator.isSecret && - (!stateVarIndicator.isBurned || stateVarIndicator.newCommitmentsRequired) + (!stateVarIndicator.isBurned && stateVarIndicator.newCommitmentsRequired) ) { burnedOnly = false; break; diff --git a/test/contracts/user-friendly-tests/NFT_Escrow.zol b/test/contracts/user-friendly-tests/NFT_Escrow.zol index 7f80ea03..eb851ee4 100644 --- a/test/contracts/user-friendly-tests/NFT_Escrow.zol +++ b/test/contracts/user-friendly-tests/NFT_Escrow.zol @@ -6,6 +6,7 @@ import "./Escrow-imports/IERC721.sol"; contract NFT_Escrow { + secret mapping(uint256 => address) public isActivated; secret mapping(uint256 => address) public tokenOwners; // mapped-to by a tokenId secret mapping(address => address) public approvals; IERC721 public erc721; @@ -21,9 +22,11 @@ contract NFT_Escrow { } function transfer(secret address recipient, secret uint256 tokenId) public { + require(isActivated[tokenId] == msg.sender, "NFT_Escrow: token should be activated"); require(tokenOwners[tokenId] == msg.sender); require(recipient != address(0), "NFT_Escrow: transfer to the zero address"); tokenOwners[tokenId] = recipient; + isActivated[tokenId] = recipient; } function approve(secret address approvedAddress) public { @@ -36,6 +39,7 @@ contract NFT_Escrow { require(recipient != address(0), "NFT_Escrow: transfer to the zero address"); require(sender != address(0), "NFT_Escrow: transfer from the zero address"); tokenOwners[tokenId] = recipient; + isActivated[tokenId] = recipient; } function withdraw(uint256 tokenId) public { @@ -44,4 +48,14 @@ contract NFT_Escrow { require(success, "ERC721 transfer failed"); tokenOwners[tokenId] = address(0); } + + function activate(secret uint256 tokenId) public { + require(tokenOwners[tokenId] == msg.sender, "NFT_Escrow: Sender doesn't have access"); + reinitialisable isActivated[tokenId] = msg.sender; + } + + function deactivate(secret uint256 tokenId) public { + require(tokenOwners[tokenId] == msg.sender, "NFT_Escrow: Sender doesn't have access"); + isActivated[tokenId] = address(0); + } } From cbc27d6c0bbb5bba9cf1229d21b748a6dfe5795e Mon Sep 17 00:00:00 2001 From: Sebastian Paul Date: Thu, 4 Dec 2025 15:22:26 +0530 Subject: [PATCH 15/18] fix: timber stability --- src/boilerplate/common/boilerplate-docker-compose.yml | 4 ++-- src/boilerplate/common/config/default.js | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/boilerplate/common/boilerplate-docker-compose.yml b/src/boilerplate/common/boilerplate-docker-compose.yml index 8daecedf..8f27e001 100644 --- a/src/boilerplate/common/boilerplate-docker-compose.yml +++ b/src/boilerplate/common/boilerplate-docker-compose.yml @@ -58,7 +58,7 @@ services: timber: build: - context: https://github.com/EYBlockchain/timber.git#starlight/zscaler:merkle-tree + context: https://github.com/EYBlockchain/timber.git#multiple-contracts:merkle-tree dockerfile: Dockerfile restart: on-failure depends_on: @@ -71,7 +71,7 @@ services: environment: NODE_EXTRA_CA_CERTS: /usr/local/share/ca-certificates/ZscalerRootCertificate-2048-SHA256.crt HASH_TYPE: 'mimc' - LOG_LEVEL: 'silly' + LOG_LEVEL: 'debug' UNIQUE_LEAVES: 'true' BLOCKCHAIN_HOST: ws://ganache BLOCKCHAIN_PORT: 8545 diff --git a/src/boilerplate/common/config/default.js b/src/boilerplate/common/config/default.js index 3f7dc090..dd0ebeff 100644 --- a/src/boilerplate/common/config/default.js +++ b/src/boilerplate/common/config/default.js @@ -1,5 +1,5 @@ module.exports = { - log_level: 'info', + LOG_LEVEL: process.env.LOG_LEVEL, multiTenant: MULTI_TENANT_MODE, zokrates: { url: process.env.ZOKRATES_URL || 'http://zokrates:80', @@ -57,7 +57,7 @@ module.exports = { // contracts to filter: contracts: { // contract name: - CONTRACT_NAME: { + default: { treeHeight: 32, events: { // filter for the following event names: @@ -80,6 +80,7 @@ module.exports = { databaseName: 'merkle_tree', admin: 'admin', adminPassword: 'admin', + dbUrl: process.env.DB_URL || 'mongodb://admin:admin@timber-mongo:27017', }, MONGO_URL: 'mongodb://admin:admin@zapp-mongo:27017', COMMITMENTS_DB: process.env.MONGO_NAME, From 04b16dd9abfd7202c4eff541cb168d5324271c31 Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Fri, 5 Dec 2025 10:58:20 +0530 Subject: [PATCH 16/18] fix: supply chain poc consolidation --- .../user-friendly-tests/SupplyChainPoC.zol | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 test/contracts/user-friendly-tests/SupplyChainPoC.zol diff --git a/test/contracts/user-friendly-tests/SupplyChainPoC.zol b/test/contracts/user-friendly-tests/SupplyChainPoC.zol new file mode 100644 index 00000000..0be0ad87 --- /dev/null +++ b/test/contracts/user-friendly-tests/SupplyChainPoC.zol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: CC0 + +pragma solidity ^0.8.0; + +import "./Escrow-imports/IERC721.sol"; + +contract SupplyChainPoC { + + // Domain parameter: nftContract + // This allows tracking tokens from multiple NFT contracts + // Each nftContract address creates a separate cryptographic namespace + secret mapping(uint256 => address) per(address nftContract) public tokenOwners; + secret mapping(uint256 => address) per(address nftContract) public isActivated; + secret mapping(address => address) per(address nftContract) public approvals; + + // No hardcoded ERC721 instance for operations that involve the tokenized asset - we use the nftContract parameter directly + // This allows the escrow to work dynamically with any ERC721 contract + + // Below functions contain a domain parameter - The nftContract parameter is used for BOTH: + // 1. The actual NFT transfer (via IERC721 interface) + // 2. Cryptographic domain separation (state variable ID calculation) + + function deposit(per address nftContract, uint256 tokenId) public { + require(nftContract != address(0), "SupplyChainPoC: invalid nftContract"); + + // Use the nftContract parameter to instantiate the ERC721 interface + IERC721 nft = IERC721(nftContract); + + bool success = nft.transferFrom(msg.sender, address(this), tokenId); + require(success, "SupplyChainPoC: ERC721 transfer failed"); + + reinitialisable tokenOwners[tokenId] = msg.sender; + } + + function transfer(per address nftContract, secret address recipient, secret uint256 tokenId) public { + require(isActivated[tokenId] == msg.sender, "SupplyChainPoC: token should be activated"); + require(nftContract != address(0), "SupplyChainPoC: invalid nftContract"); + require(tokenOwners[tokenId] == msg.sender); + require(recipient != address(0), "SupplyChainPoC: transfer to the zero address"); + + tokenOwners[tokenId] = recipient; + isActivated[tokenId] = recipient; + } + + function approve(per address nftContract, secret address approvedAddress) public { + require(nftContract != address(0), "SupplyChainPoC: invalid nftContract"); + require(approvedAddress != address(0), "Escrow: approve to the zero address"); + + approvals[msg.sender] = approvedAddress; + } + + function transferFrom(per address nftContract, secret address sender, secret address recipient, secret uint256 tokenId) public { + require(isActivated[tokenId] == msg.sender, "SupplyChainPoC: token should be activated"); + require(nftContract != address(0), "SupplyChainPoC: invalid nftContract"); + require(recipient != address(0), "SupplyChainPoC: transfer to the zero address"); + require(sender != address(0), "SupplyChainPoC: transfer from the zero address"); + + // Approval: sender (owner) has approved msg.sender in this nftContract domain + require(approvals[sender] == msg.sender, "SupplyChainPoC: not approved"); + + // Ownership: sender actually owns tokenId in this nftContract's domain + require(tokenOwners[tokenId] == sender, "SupplyChainPoC: sender does not own token"); + tokenOwners[tokenId] = recipient; + isActivated[tokenId] = recipient; + } + + function withdraw(per address nftContract, uint256 tokenId) public { + require(isActivated[tokenId] == msg.sender, "SupplyChainPoC: token should be activated"); + require(nftContract != address(0), "SupplyChainPoC: invalid nftContract"); + require(tokenOwners[tokenId] == msg.sender); + + // Use the nftContract parameter to instantiate the ERC721 interface + IERC721 nft = IERC721(nftContract); + + bool success = nft.transferFrom(address(this), msg.sender, tokenId); + require(success, "ERC721 transfer failed"); + + tokenOwners[tokenId] = address(0); + } + + function activate(per address nftContract, uint256 tokenId) public { + require(nftContract != address(0), "SupplyChainPoC: invalid nftContract"); + require(tokenOwners[tokenId] == msg.sender, "SupplyChainPoC: Sender doesn't have access"); + + reinitialisable isActivated[tokenId] = msg.sender; + } + + function deactivate(per address nftContract, uint256 tokenId) public { + require(nftContract != address(0), "SupplyChainPoC: invalid nftContract"); + require(tokenOwners[tokenId] == msg.sender, "SupplyChainPoC: Sender doesn't have access"); + require(isActivated[tokenId] != address(0), "Token is not activated"); + + isActivated[tokenId] = address(0); + } +} From 6f80934a787b7a1800d7a318839893be63adef11 Mon Sep 17 00:00:00 2001 From: Sebastian Paul Date: Wed, 17 Dec 2025 12:28:37 +0530 Subject: [PATCH 17/18] fix: commitment db update --- src/boilerplate/common/commitment-storage.mjs | 7 +++-- .../javascript/raw/boilerplate-generator.ts | 3 +++ .../orchestration/files/toOrchestration.ts | 4 +++ .../user-friendly-tests/SupplyChainPoC.zol | 27 +++++++++++++++++-- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/boilerplate/common/commitment-storage.mjs b/src/boilerplate/common/commitment-storage.mjs index 8359fe3b..a0fb4742 100644 --- a/src/boilerplate/common/commitment-storage.mjs +++ b/src/boilerplate/common/commitment-storage.mjs @@ -52,6 +52,7 @@ export function formatCommitment (commitment, context) { isNullified: commitment.isNullified, nullifier: commitment.secretKey ? nullifierHash.hex(32) : null, accountId: context?.accountId || null, + blockNumber: commitment.blockNumber || null, } logger.debug(`Storing commitment ${data._id}${context?.accountId ? ` for accountId: ${context.accountId}` : ''}${domainParameters ? ` with domain parameters: ${JSON.stringify(domainParameters)}` : ''}`) } catch (error) { @@ -65,7 +66,8 @@ export function formatCommitment (commitment, context) { export async function persistCommitment (data) { const connection = await mongo.connection(MONGO_URL) const db = connection.db(COMMITMENTS_DB) - return db.collection(COMMITMENTS_COLLECTION).insertOne(data) + const doc = { ...data, createdAt: new Date() }; + return db.collection(COMMITMENTS_COLLECTION).insertOne(doc); } // function to format a commitment for a mongo db and store it export async function storeCommitment (commitment, context) { @@ -221,7 +223,7 @@ export async function updateCommitment(commitment, updates) { const connection = await mongo.connection(MONGO_URL); const db = connection.db(COMMITMENTS_DB); const query = { _id: commitment._id }; - const update = { $set: updates }; + const update = { $set: { ...updates, updatedAt: new Date() } }; return db.collection(COMMITMENTS_COLLECTION).updateOne(query, update); } @@ -240,6 +242,7 @@ export async function markNullified(commitmentHash, secretKey = null) { $set: { isNullified: true, nullifier: generalise(nullifier).hex(32), + updatedAt: new Date(), }, }; // updating the original tree diff --git a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts index 54b23b65..e4a329f3 100644 --- a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts +++ b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts @@ -717,6 +717,7 @@ sendTransaction = { \tpublicKey: ${stateName}_newOwnerPublicKey, }, secretKey: ${stateName}_newOwnerPublicKey.integer === ${isSharedSecret ? `sharedPublicKey.integer` : `publicKey.integer`} ? ${isSharedSecret ? `sharedSecretKey` : `secretKey`}: null, + blockNumber: Number(tx.blockNumber), isNullified: false, }, SAAS_CONTEXT_PARAM);` + errorCatch]; case 'decrement': @@ -736,6 +737,7 @@ sendTransaction = { \tpublicKey: ${stateName}_newOwnerPublicKey, }, secretKey: ${stateName}_newOwnerPublicKey.integer === ${isSharedSecret ? `sharedPublicKey.integer` : `publicKey.integer`} ? ${isSharedSecret ? `sharedSecretKey` : `secretKey`}: null, + blockNumber: Number(tx.blockNumber), isNullified: false, }, SAAS_CONTEXT_PARAM);`+ errorCatch]; case 'whole': @@ -775,6 +777,7 @@ sendTransaction = { \tpublicKey: ${stateName}_newOwnerPublicKey, }, secretKey: ${stateName}_newOwnerPublicKey.integer === ${isSharedSecret ? `sharedPublicKey.integer` : `publicKey.integer`} ? ${isSharedSecret ? `sharedSecretKey` : `secretKey`}: null, + blockNumber: Number(tx.blockNumber), isNullified: false, }, ${stateName}_recipientContext);` + errorCatch]; } diff --git a/src/codeGenerators/orchestration/files/toOrchestration.ts b/src/codeGenerators/orchestration/files/toOrchestration.ts index 01d8091b..9b8dd308 100644 --- a/src/codeGenerators/orchestration/files/toOrchestration.ts +++ b/src/codeGenerators/orchestration/files/toOrchestration.ts @@ -351,6 +351,7 @@ node.stateVariables?.forEach( publicKey: self.publicKey, }, secretKey: self.secretKey, + blockNumber: Number(tx.blockNumber), isNullified: false, }); console.log('Added commitment', newCommitment.hex(32)); @@ -400,6 +401,7 @@ node.stateVariables?.forEach( publicKey: self.publicKey, }, secretKey: self.secretKey, + blockNumber: Number(tx.blockNumber), isNullified: false, }); console.log('Added commitment', newCommitment.hex(32)); @@ -838,6 +840,7 @@ const prepareBackupVariable = (node: any) => { publicKey: kp.publicKey, }, secretKey: kp.secretKey, + blockNumber: Number(tx.blockNumber), isNullified: isNullified, }); } @@ -1051,6 +1054,7 @@ const prepareBackupDataRetriever = (node: any) => { publicKey: kp.publicKey, }, secretKey: kp.secretKey, + blockNumber: Number(tx.blockNumber), isNullified: isNullified, }); } diff --git a/test/contracts/user-friendly-tests/SupplyChainPoC.zol b/test/contracts/user-friendly-tests/SupplyChainPoC.zol index 0be0ad87..77d97e89 100644 --- a/test/contracts/user-friendly-tests/SupplyChainPoC.zol +++ b/test/contracts/user-friendly-tests/SupplyChainPoC.zol @@ -6,12 +6,19 @@ import "./Escrow-imports/IERC721.sol"; contract SupplyChainPoC { + struct AccessData{ + uint256 tokenId; + uint256 role; + address viewer; + } // Domain parameter: nftContract // This allows tracking tokens from multiple NFT contracts // Each nftContract address creates a separate cryptographic namespace secret mapping(uint256 => address) per(address nftContract) public tokenOwners; + secret mapping(uint256 => address) per(address nftContract) public minter; secret mapping(uint256 => address) per(address nftContract) public isActivated; secret mapping(address => address) per(address nftContract) public approvals; + secret mapping(uint256 => AccessData) per(address nftContract) public accessData; // No hardcoded ERC721 instance for operations that involve the tokenized asset - we use the nftContract parameter directly // This allows the escrow to work dynamically with any ERC721 contract @@ -28,7 +35,7 @@ contract SupplyChainPoC { bool success = nft.transferFrom(msg.sender, address(this), tokenId); require(success, "SupplyChainPoC: ERC721 transfer failed"); - + minter[tokenId] = msg.sender; reinitialisable tokenOwners[tokenId] = msg.sender; } @@ -37,7 +44,7 @@ contract SupplyChainPoC { require(nftContract != address(0), "SupplyChainPoC: invalid nftContract"); require(tokenOwners[tokenId] == msg.sender); require(recipient != address(0), "SupplyChainPoC: transfer to the zero address"); - + minter[tokenId] = minter[tokenId]; tokenOwners[tokenId] = recipient; isActivated[tokenId] = recipient; } @@ -92,4 +99,20 @@ contract SupplyChainPoC { isActivated[tokenId] = address(0); } + + function grantAccess(per address nftContract, secret uint256 accessId, secret address accessAddress, secret uint256 tokenId, secret uint256 role) public { + require(nftContract != address(0), "SupplyChainPoC: invalid nftContract"); + + accessData[accessId].tokenId=tokenId; + accessData[accessId].role=role; + accessData[accessId].viewer=accessAddress; + } + + function revokeAccess(per address nftContract,secret uint256 accessId) public { + require(nftContract != address(0), "SupplyChainPoC: invalid nftContract"); + + accessData[accessId].tokenId=0; + accessData[accessId].role=0; + accessData[accessId].viewer=address(0); + } } From 118cfa43a96a4c50d96e67c7f83cac2237442f8b Mon Sep 17 00:00:00 2001 From: Adarsh Ron Date: Wed, 21 Jan 2026 12:41:41 +0530 Subject: [PATCH 18/18] feat: maintained the namespacing code --- .gitignore | 1 + bin/index.mjs | 6 +- src/boilerplate/common/commitment-storage.mjs | 83 +-- src/boilerplate/common/config/default.js | 1 - src/boilerplate/common/contract.mjs | 64 +- .../common/encrypted-data-listener.mjs | 16 +- .../key-management/DatabaseKeyStorage.mjs | 411 ------------ .../common/key-management/FileKeyStorage.mjs | 264 -------- .../common/key-management/IKeyStorage.mjs | 116 ---- .../common/key-management/KeyManager.mjs | 221 ------- .../common/key-management/encryption.mjs | 208 ------ .../common/key-management/index.mjs | 73 --- .../common/middleware/saas-context.mjs | 194 ------ .../common/services/generic-api_services.mjs | 3 +- .../common/services/saas-key-services.mjs | 150 ----- .../javascript/nodes/boilerplate-generator.ts | 2 - .../javascript/raw/boilerplate-generator.ts | 600 +++--------------- .../javascript/raw/toOrchestration.ts | 31 - .../orchestration/files/toOrchestration.ts | 28 +- .../orchestration/nodejs/toOrchestration.ts | 6 +- src/transformers/toOrchestration.ts | 26 +- .../visitors/toOrchestrationVisitor.ts | 9 - src/types/orchestration-types.ts | 3 +- 23 files changed, 197 insertions(+), 2319 deletions(-) delete mode 100644 src/boilerplate/common/key-management/DatabaseKeyStorage.mjs delete mode 100644 src/boilerplate/common/key-management/FileKeyStorage.mjs delete mode 100644 src/boilerplate/common/key-management/IKeyStorage.mjs delete mode 100644 src/boilerplate/common/key-management/KeyManager.mjs delete mode 100644 src/boilerplate/common/key-management/encryption.mjs delete mode 100644 src/boilerplate/common/key-management/index.mjs delete mode 100644 src/boilerplate/common/middleware/saas-context.mjs delete mode 100644 src/boilerplate/common/services/saas-key-services.mjs diff --git a/.gitignore b/.gitignore index 7ac357a1..41801e56 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ zapps/ test-zapps/ temp-zapps/ truezapps/ +.github/*.md diff --git a/bin/index.mjs b/bin/index.mjs index 760ed43e..0473afa8 100755 --- a/bin/index.mjs +++ b/bin/index.mjs @@ -44,10 +44,7 @@ program '-m, --modify ', 'Ovewrite the file from truezapps folder', ) - .option( - '--multi-tenant', - 'Enable multi-user mode for zapps' - ); + ; program.parse(process.argv); const opts = program.opts(); @@ -77,7 +74,6 @@ const options = { contractsDirPath, orchestrationDirPath, modifyAST, - multiTenant: opts.multiTenant || false, }; const validateOptions = ({ diff --git a/src/boilerplate/common/commitment-storage.mjs b/src/boilerplate/common/commitment-storage.mjs index a0fb4742..9d870319 100644 --- a/src/boilerplate/common/commitment-storage.mjs +++ b/src/boilerplate/common/commitment-storage.mjs @@ -9,9 +9,9 @@ import gen from 'general-number'; import mongo from './mongo.mjs'; import logger from './logger.mjs'; import utils from 'zkp-utils'; -import { poseidonHash } from './number-theory.mjs'; +import { poseidonHash, sharedSecretKey } from './number-theory.mjs'; import { generateProof } from './zokrates.mjs'; -import { KeyManager } from './key-management/KeyManager.mjs'; +import { getStoredKeys } from './contract.mjs'; const { MONGO_URL, COMMITMENTS_DB, COMMITMENTS_COLLECTION } = config; const { generalise } = gen; @@ -51,10 +51,9 @@ export function formatCommitment (commitment, context) { preimage, isNullified: commitment.isNullified, nullifier: commitment.secretKey ? nullifierHash.hex(32) : null, - accountId: context?.accountId || null, blockNumber: commitment.blockNumber || null, } - logger.debug(`Storing commitment ${data._id}${context?.accountId ? ` for accountId: ${context.accountId}` : ''}${domainParameters ? ` with domain parameters: ${JSON.stringify(domainParameters)}` : ''}`) + logger.debug(`Storing commitment ${data._id}${domainParameters ? ` with domain parameters: ${JSON.stringify(domainParameters)}` : ''}`) } catch (error) { console.error('Error formatting commitment --->', error) console.error('Commitment object:', JSON.stringify(commitment, null, 2)) @@ -98,27 +97,22 @@ export async function getCommitmentsById(id) { } // function to retrieve commitment with a specified stateVarId -export async function getCurrentWholeCommitment(id, accountId) { +export async function getCurrentWholeCommitment(id) { const connection = await mongo.connection(MONGO_URL); const db = connection.db(COMMITMENTS_DB); const query = { 'preimage.stateVarId': generalise(id).hex(32), isNullified: false, }; - - if (accountId) { - query.accountId = accountId; - } const commitment = await db.collection(COMMITMENTS_COLLECTION).findOne(query); return commitment; } // function to retrieve commitment with a specified stateName -export async function getCommitmentsByState(name, mappingKey = null, accountId = null, domainParameters = null) { +export async function getCommitmentsByState(name, mappingKey = null, domainParameters = null) { const connection = await mongo.connection(MONGO_URL); const db = connection.db(COMMITMENTS_DB); const query = { name: name }; - if (accountId) query['accountId'] = accountId; if (mappingKey) query['mappingKey'] = generalise(mappingKey).integer; // Add domain parameter filters if provided @@ -161,13 +155,12 @@ export async function getNullifiedCommitments() { /** * @returns {Promise} The sum of the values ​​of all non-nullified commitments */ -export async function getBalance(accountId) { +export async function getBalance() { const connection = await mongo.connection(MONGO_URL); const db = connection.db(COMMITMENTS_DB); - const query = accountId ? { accountId } : {}; const commitments = await db .collection(COMMITMENTS_COLLECTION) - .find({ ...query, isNullified: false }) // no nullified + .find({ isNullified: false }) // no nullified .toArray(); let sumOfValues = 0; @@ -177,11 +170,10 @@ export async function getBalance(accountId) { return sumOfValues; } -export async function getBalanceByState(name, mappingKey = null, accountId=null, domainParameters = null) { +export async function getBalanceByState(name, mappingKey = null, domainParameters = null) { const connection = await mongo.connection(MONGO_URL); const db = connection.db(COMMITMENTS_DB); const query = { name: name }; - if (accountId) query['accountId'] = accountId; if (mappingKey) query['mappingKey'] = generalise(mappingKey).integer; // Add domain parameter filters if provided @@ -207,13 +199,12 @@ export async function getBalanceByState(name, mappingKey = null, accountId=null, /** * @returns all the commitments existent in this database. */ -export async function getAllCommitments(accountId) { +export async function getAllCommitments() { const connection = await mongo.connection(MONGO_URL); const db = connection.db(COMMITMENTS_DB); - const query = accountId ? { accountId } : {}; const allCommitments = await db .collection(COMMITMENTS_COLLECTION) - .find(query) + .find() .toArray(); return allCommitments; } @@ -486,36 +477,20 @@ export async function joinCommitments( .flat(Infinity); // Send transaction to the blockchain: - // Get tenant-specific keys - const keyManager = KeyManager.getInstance(); - const keys = await keyManager.getKeys(context); - if (!keys || !keys.ethPK || !keys.ethSK) { - throw new Error('Tenant Ethereum keys not found. Please register keys first.'); - } + const from = config.web3.options.defaultAccount || (await web3.eth.getAccounts())[0]; - const txData = await instance.methods + const sendTxn = await instance.methods .joinCommitments( [oldCommitment_0_nullifier.integer, oldCommitment_1_nullifier.integer], oldCommitment_root.integer, [newCommitment.integer], proof, ) - .encodeABI(); - - let txParams = { - from: keys.ethPK, - to: contractAddr, - gas: config.web3.options.defaultGas, - gasPrice: config.web3.options.defaultGasPrice, - data: txData, - chainId: await web3.eth.net.getId(), - }; - - const key = keys.ethSK; - - const signed = await web3.eth.accounts.signTransaction(txParams, key); - - const sendTxn = await web3.eth.sendSignedTransaction(signed.rawTransaction); + .send({ + from, + gas: config.web3.options.defaultGas, + gasPrice: config.web3.options.defaultGasPrice, + }); let tx = await instance.getPastEvents('allEvents', { fromBlock: sendTxn?.blockNumber || 0, @@ -732,25 +707,17 @@ export async function getSharedSecretskeys( context, ) { try { - // Use KeyManager for shared secret key management - const keyManager = KeyManager.getInstance(); - - logger.debug('Getting shared secret keys via KeyManager', { - recipientAddress: _recipientAddress, - multiTenant: !!context?.accountId - }); + const keys = getStoredKeys(); + if (!keys || !keys.secretKey) { + throw new Error('Secret key not found. Please register keys first.'); + } - const sharedPublicKey = await keyManager.getSharedSecretKeys( - _recipientAddress, - _recipientPublicKey, - context - ); + const recipientPublicKey = _recipientPublicKey || keys.publicKey; + const sharedKey = sharedSecretKey(generalise(keys.secretKey), generalise(recipientPublicKey)); - logger.info('Shared secret keys retrieved successfully', { - multiTenant: !!context?.accountId - }); + logger.info('Shared secret keys retrieved successfully'); - return sharedPublicKey; + return sharedKey; } catch (error) { logger.error('Failed to get shared secret keys:', error); throw error; diff --git a/src/boilerplate/common/config/default.js b/src/boilerplate/common/config/default.js index dd0ebeff..3d514bf8 100644 --- a/src/boilerplate/common/config/default.js +++ b/src/boilerplate/common/config/default.js @@ -1,6 +1,5 @@ module.exports = { LOG_LEVEL: process.env.LOG_LEVEL, - multiTenant: MULTI_TENANT_MODE, zokrates: { url: process.env.ZOKRATES_URL || 'http://zokrates:80', }, diff --git a/src/boilerplate/common/contract.mjs b/src/boilerplate/common/contract.mjs index 2cc60455..2f676245 100644 --- a/src/boilerplate/common/contract.mjs +++ b/src/boilerplate/common/contract.mjs @@ -1,13 +1,30 @@ import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; import config from 'config'; import GN from 'general-number'; -import utils from 'zkp-utils'; import Web3 from './web3.mjs'; import logger from './logger.mjs'; -import { KeyManager } from './key-management/KeyManager.mjs'; const web3 = Web3.connection(); const { generalise } = GN; +const keyDbPath = process.env.KEY_DB_PATH + || path.resolve(path.dirname(fileURLToPath(import.meta.url)), './db/key.json'); + +function loadKeysFromDisk() { + if (!fs.existsSync(keyDbPath)) return null; + try { + return JSON.parse(fs.readFileSync(keyDbPath, 'utf8')); + } catch (err) { + logger.warn('Unable to read key database, regenerating keys', err); + return null; + } +} + +function persistKeys(keys) { + fs.mkdirSync(path.dirname(keyDbPath), { recursive: true }); + fs.writeFileSync(keyDbPath, JSON.stringify(keys, null, 2)); +} export const contractPath = (contractName) => { return `/app/build/contracts/${contractName}.json`; @@ -121,32 +138,31 @@ export async function registerKey( context, ) { try { - // Use KeyManager for key registration - const keyManager = KeyManager.getInstance(); - - logger.debug('Registering key via KeyManager', { - contractName, - registerWithContract, - multiTenant: !!context?.accountId - }); - - const publicKeyInteger = await keyManager.registerKey( - _secretKey, - contractName, - registerWithContract, - context - ); - - const publicKey = generalise(publicKeyInteger); - + const secretKeyGN = generalise(_secretKey); + const secretKey = secretKeyGN.hex ? secretKeyGN.hex(32) : `${secretKeyGN}`; + const publicKeyGN = secretKeyGN; + const publicKey = publicKeyGN.hex ? publicKeyGN.hex(32) : `${publicKeyGN}`; + const sharedPublicKey = publicKey; + const sharedSecretKey = secretKey; + const keys = { + secretKey, + publicKey, + sharedPublicKey, + sharedSecretKey, + ethPK: config.web3.options.defaultAccount, + ethSK: config.web3.key, + }; + persistKeys(keys); logger.info('Key registered successfully', { - publicKey: publicKey.integer, - multiTenant: !!context?.accountId + publicKey, }); - - return publicKey; + return generalise(publicKey); } catch (error) { logger.error('Failed to register key:', error); throw error; } +} + +export function getStoredKeys() { + return loadKeysFromDisk(); } \ No newline at end of file diff --git a/src/boilerplate/common/encrypted-data-listener.mjs b/src/boilerplate/common/encrypted-data-listener.mjs index 37219072..8c21dac5 100644 --- a/src/boilerplate/common/encrypted-data-listener.mjs +++ b/src/boilerplate/common/encrypted-data-listener.mjs @@ -2,10 +2,9 @@ import fs from 'fs'; import utils from 'zkp-utils'; import config from 'config'; import { generalise } from 'general-number'; -import { getContractAddress, getContractInstance, registerKey } from './common/contract.mjs'; +import { getContractAddress, getContractInstance, registerKey, getStoredKeys } from './common/contract.mjs'; import { storeCommitment, formatCommitment, persistCommitment } from './common/commitment-storage.mjs'; import { decrypt, poseidonHash, } from './common/number-theory.mjs'; -import { KeyManager } from './key-management/KeyManager.mjs'; function decodeCommitmentData(decrypted){ @@ -36,19 +35,13 @@ export default class EncryptedDataEventListener { contractAddr, ); - // Use KeyManager for key retrieval - const keyManager = KeyManager.getInstance(); - - // Check if keys exist, if not register new ones - const hasKeys = await keyManager.hasKeys(this.context); - if (!hasKeys) { + let keys = getStoredKeys(); + if (!keys) { console.log('No keys found, registering new key pair...'); await registerKey(utils.randomHex(31), 'CONTRACT_NAME', true, this.context); + keys = getStoredKeys(); } - // Retrieve keys via KeyManager - const keys = await keyManager.getKeys(this.context); - if (!keys) { throw new Error('Failed to retrieve keys after registration'); } @@ -58,7 +51,6 @@ export default class EncryptedDataEventListener { this.ethAddress = keys.ethPK ? generalise(keys.ethPK) : generalise(config.web3.options.defaultAccount); console.log('Keys loaded successfully', { - multiTenant: !!this.context?.accountId, ethAddress: this.ethAddress.hex(), }); } catch (error) { diff --git a/src/boilerplate/common/key-management/DatabaseKeyStorage.mjs b/src/boilerplate/common/key-management/DatabaseKeyStorage.mjs deleted file mode 100644 index 0065fd65..00000000 --- a/src/boilerplate/common/key-management/DatabaseKeyStorage.mjs +++ /dev/null @@ -1,411 +0,0 @@ -/** - * @file DatabaseKeyStorage.mjs - * @description MongoDB-based key storage implementation for multi-tenant deployments. - * Provides complete isolation between users based on accountId. - */ - -import config from 'config'; -import GN from 'general-number'; -import utils from 'zkp-utils'; -import mongo from '../mongo.mjs'; -import logger from '../logger.mjs'; -import { IKeyStorage } from './IKeyStorage.mjs'; -import { encryptIfEnabled, decryptIfEncrypted } from './encryption.mjs'; -import { - scalarMult, - compressStarlightKey, - sharedSecretKey, -} from '../number-theory.mjs'; - -const { generalise } = GN; - -// Configuration -const MONGO_URL = process.env.MONGO_URL || config.MONGO_URL || 'mongodb://localhost:27017'; -const KEYS_DB = process.env.KEYS_DB || config.KEYS_DB || config.COMMITMENTS_DB || 'starlight_db'; -const USER_KEYS_COLLECTION = 'user_keys'; - -/** - * Database-based key storage implementation. - * Stores keys in MongoDB with complete isolation between users. - * Supports encryption at rest for sensitive key material. - * - * @extends IKeyStorage - */ -export class DatabaseKeyStorage extends IKeyStorage { - constructor() { - super(); - this.mongoUrl = MONGO_URL; - this.dbName = KEYS_DB; - this.collectionName = USER_KEYS_COLLECTION; - } - - /** - * Get MongoDB collection instance. - * - * @returns {Promise} - * @private - */ - async getCollection() { - const connection = await mongo.connection(this.mongoUrl); - const db = connection.db(this.dbName); - return db.collection(this.collectionName); - } - - /** - * Validate that context is provided and contains accountId. - * - * @param {import('./IKeyStorage.mjs').SaaSContext} context - * @throws {Error} If context is missing or invalid - * @private - */ - validateContext(context) { - if (!context || !context.accountId) { - throw new Error( - 'DatabaseKeyStorage requires a valid SaaS context with accountId. ' + - 'Ensure x-saas-context header is present in the request.' - ); - } - - // Validate accountId format (alphanumeric, hyphens, underscores only) - if (!/^[a-zA-Z0-9_-]+$/.test(context.accountId)) { - throw new Error('Invalid accountId format. Must contain only alphanumeric characters, hyphens, and underscores.'); - } - } - - /** - * Retrieve keys for a user from the database. - * - * @param {import('./IKeyStorage.mjs').SaaSContext} context - Required SaaS context - * @returns {Promise} User keys or null if not found - */ - async getKeys(context) { - this.validateContext(context); - - try { - const collection = await this.getCollection(); - const doc = await collection.findOne({ accountId: context.accountId }); - - if (!doc) { - logger.debug(`No keys found for accountId: ${context.accountId}`); - return null; - } - - // Update lastUsed timestamp - await collection.updateOne( - { accountId: context.accountId }, - { - $set: { - 'metadata.lastUsed': new Date(), - updatedAt: new Date() - } - } - ); - - // Decrypt sensitive keys - const keys = { - secretKey: decryptIfEncrypted(doc.secretKey), - publicKey: doc.publicKey, // Public key doesn't need decryption - ethSK: doc.ethSK ? decryptIfEncrypted(doc.ethSK) : null, // Ethereum private key (encrypted) - ethPK: doc.ethPK || null, - }; - - if (doc.sharedSecretKey) { - keys.sharedSecretKey = decryptIfEncrypted(doc.sharedSecretKey); - } - if (doc.sharedPublicKey) { - keys.sharedPublicKey = doc.sharedPublicKey; - } - - logger.debug(`Keys retrieved for accountId: ${context.accountId}`); - return keys; - } catch (error) { - logger.error(`Error retrieving keys for accountId ${context.accountId}:`, error); - throw new Error(`Failed to retrieve keys: ${error.message}`); - } - } - - /** - * Save keys for a user to the database. - * - * @param {import('./IKeyStorage.mjs').UserKeys} keys - User keys to save - * @param {import('./IKeyStorage.mjs').SaaSContext} context - Required SaaS context - * @returns {Promise} - */ - async saveKeys(keys, context) { - this.validateContext(context); - - try { - const collection = await this.getCollection(); - const now = new Date(); - - // Encrypt sensitive keys - const doc = { - accountId: context.accountId, - secretKey: encryptIfEnabled(keys.secretKey), - publicKey: keys.publicKey, // Public key doesn't need encryption - updatedAt: now, - }; - - // Include Ethereum keys if present - if (keys.ethSK) { - doc.ethSK = encryptIfEnabled(keys.ethSK); - } - if (keys.ethPK) { - doc.ethPK = keys.ethPK; - } - - // Include optional shared keys if present - if (keys.sharedSecretKey) { - doc.sharedSecretKey = encryptIfEnabled(keys.sharedSecretKey); - } - if (keys.sharedPublicKey) { - doc.sharedPublicKey = keys.sharedPublicKey; - } - - // Upsert: update if exists, insert if not - const result = await collection.updateOne( - { accountId: context.accountId }, - { - $set: doc, - $setOnInsert: { - createdAt: now, - metadata: { - keyVersion: 1, - registeredOnChain: false, - } - } - }, - { upsert: true } - ); - - if (result.upsertedCount > 0) { - logger.info(`Keys created for accountId: ${context.accountId}`); - } else { - logger.debug(`Keys updated for accountId: ${context.accountId}`); - } - } catch (error) { - logger.error(`Error saving keys for accountId ${context.accountId}:`, error); - throw new Error(`Failed to save keys: ${error.message}`); - } - } - - /** - * Register a new key pair. - * - * @param {string} _secretKey - Secret key to register (hex string) - * @param {string} contractName - Associated contract name - * @param {boolean} registerWithContract - Whether to register the key on-chain - * @param {import('./IKeyStorage.mjs').SaaSContext} context - Required SaaS context - * @returns {Promise} Public key (as integer string) - */ - async registerKey(_secretKey, contractName, registerWithContract, context) { - this.validateContext(context); - - try { - let secretKey = generalise(_secretKey); - let publicKeyPoint = generalise( - scalarMult(secretKey.hex(32), config.BABYJUBJUB.GENERATOR) - ); - let publicKey = compressStarlightKey(publicKeyPoint); - - // Regenerate if public key is too large - while (publicKey === null) { - logger.warn('Secret key created a large public key - regenerating'); - secretKey = generalise(utils.randomHex(31)); - publicKeyPoint = generalise( - scalarMult(secretKey.hex(32), config.BABYJUBJUB.GENERATOR) - ); - publicKey = compressStarlightKey(publicKeyPoint); - } - - const Web3 = await import('../web3.mjs'); - const web3 = Web3.default.connection(); - const ethAccount = web3.eth.accounts.create(); - const ethSK = ethAccount.privateKey; - const ethPK = ethAccount.address; - - logger.info(`Generated Ethereum address for tenant ${context.accountId}: ${ethPK}`); - - // AUTO-FUND tenant address with gas - const { autoFundIfNeeded } = await import('../gas-funding.mjs'); - await autoFundIfNeeded(ethPK, '0.1', '0.5'); - logger.info(`Auto-funded tenant address ${ethPK} with gas. Ready to send transactions!`); - - // Register on-chain if requested - if (registerWithContract) { - const { getContractInstance, getContractAddress } = await import('../contract.mjs'); - - const instance = await getContractInstance(contractName); - const contractAddr = await getContractAddress(contractName); - const txData = await instance.methods - .registerZKPPublicKey(publicKey.integer) - .encodeABI(); - - const txParams = { - from: ethPK, - to: contractAddr, - gas: config.web3.options.defaultGas, - gasPrice: config.web3.options.defaultGasPrice, - data: txData, - chainId: await web3.eth.net.getId(), - }; - - const signed = await web3.eth.accounts.signTransaction(txParams, ethSK); - await web3.eth.sendSignedTransaction(signed.rawTransaction); - logger.info(`Key registered on-chain for accountId: ${context.accountId}`); - } - - // Save keys to database with metadata - await this.saveKeys({ - secretKey: secretKey.integer, - publicKey: publicKey.integer, - ethSK, - ethPK, - }, context); - - // Update metadata - const collection = await this.getCollection(); - await collection.updateOne( - { accountId: context.accountId }, - { - $set: { - 'metadata.contractName': contractName, - 'metadata.registeredOnChain': registerWithContract, - } - } - ); - - logger.info(`Key registered successfully for accountId: ${context.accountId}`); - return publicKey.integer; - } catch (error) { - logger.error(`Error registering key for accountId ${context.accountId}:`, error); - throw new Error(`Failed to register key: ${error.message}`); - } - } - - /** - * Get or create shared secret keys for encrypted communication. - * - * @param {string} _recipientAddress - Recipient's Ethereum address - * @param {string|number} _recipientPublicKey - Recipient's public key (0 to fetch from contract) - * @param {import('./IKeyStorage.mjs').SaaSContext} context - Required SaaS context - * @returns {Promise} Shared public key - */ - async getSharedSecretKeys(_recipientAddress, _recipientPublicKey = 0, context) { - this.validateContext(context); - - try { - // Ensure keys exist - let keys = await this.getKeys(context); - if (!keys) { - await this.registerKey(utils.randomHex(31), null, false, context); - keys = await this.getKeys(context); - } - - const secretKey = generalise(keys.secretKey); - const publicKey = generalise(keys.publicKey); - let recipientPublicKey = generalise(_recipientPublicKey); - const recipientAddress = generalise(_recipientAddress); - - // Fetch recipient's public key from contract if not provided - if (_recipientPublicKey === 0) { - const { getContractInstance } = await import('../contract.mjs'); - const instance = await getContractInstance('CONTRACT_NAME'); - - recipientPublicKey = await instance.methods - .zkpPublicKeys(recipientAddress.hex(20)) - .call(); - recipientPublicKey = generalise(recipientPublicKey); - - if (recipientPublicKey.length === 0) { - throw new Error('Public key for given eth address not found'); - } - } - - // Generate shared secret - const sharedKey = sharedSecretKey(secretKey, recipientPublicKey); - logger.debug(`Shared key generated for accountId: ${context.accountId}`); - - // Update keys with shared secret - await this.saveKeys({ - secretKey: secretKey.integer, - publicKey: publicKey.integer, - sharedSecretKey: sharedKey[0].integer, - sharedPublicKey: sharedKey[1].integer, - }, context); - - return sharedKey[1]; - } catch (error) { - logger.error(`Error getting shared secret keys for accountId ${context.accountId}:`, error); - throw new Error(`Failed to get shared secret keys: ${error.message}`); - } - } - - /** - * Check if keys exist for a user in the database. - * - * @param {import('./IKeyStorage.mjs').SaaSContext} context - Required SaaS context - * @returns {Promise} True if keys exist - */ - async hasKeys(context) { - this.validateContext(context); - - try { - const collection = await this.getCollection(); - const count = await collection.countDocuments({ accountId: context.accountId }); - return count > 0; - } catch (error) { - logger.error(`Error checking keys for accountId ${context.accountId}:`, error); - throw new Error(`Failed to check keys: ${error.message}`); - } - } - - /** - * Delete keys for a user from the database. - * - * @param {import('./IKeyStorage.mjs').SaaSContext} context - Required SaaS context - * @returns {Promise} True if keys were deleted, false if they didn't exist - */ - async deleteKeys(context) { - this.validateContext(context); - - try { - const collection = await this.getCollection(); - const result = await collection.deleteOne({ accountId: context.accountId }); - - if (result.deletedCount > 0) { - logger.info(`Keys deleted for accountId: ${context.accountId}`); - return true; - } - - logger.debug(`No keys found to delete for accountId: ${context.accountId}`); - return false; - } catch (error) { - logger.error(`Error deleting keys for accountId ${context.accountId}:`, error); - throw new Error(`Failed to delete keys: ${error.message}`); - } - } - - async getAccountIdByEthAddress(ethAddress) { - try { - const collection = await this.getCollection(); - const doc = await collection.findOne( - { ethPK: ethAddress }, - { projection: { accountId: 1 } } - ); - - if (!doc) { - logger.debug(`No accountId found for Ethereum address: ${ethAddress}`); - return null; - } - - logger.debug(`Found accountId ${doc.accountId} for Ethereum address ${ethAddress}`); - return doc.accountId; - } catch (error) { - logger.error(`Error looking up accountId for Ethereum address ${ethAddress}:`, error); - throw new Error(`Failed to lookup accountId: ${error.message}`); - } - } -} - -export default DatabaseKeyStorage; diff --git a/src/boilerplate/common/key-management/FileKeyStorage.mjs b/src/boilerplate/common/key-management/FileKeyStorage.mjs deleted file mode 100644 index eb66de51..00000000 --- a/src/boilerplate/common/key-management/FileKeyStorage.mjs +++ /dev/null @@ -1,264 +0,0 @@ -/** - * @file FileKeyStorage.mjs - * @description File-based key storage implementation. - * This wraps the existing key.json file-based logic for backward compatibility. - */ - -import fs from 'fs'; -import config from 'config'; -import GN from 'general-number'; -import utils from 'zkp-utils'; -import logger from '../logger.mjs'; -import { IKeyStorage } from './IKeyStorage.mjs'; -import { - scalarMult, - compressStarlightKey, - sharedSecretKey, -} from '../number-theory.mjs'; - -const { generalise } = GN; -const keyDb = '/app/orchestration/common/db/key.json'; - -/** - * File-based key storage implementation. - * Stores keys in a single JSON file at /app/orchestration/common/db/key.json - * This is the legacy/default storage mechanism for single-tenant deployments. - * - * @extends IKeyStorage - */ -export class FileKeyStorage extends IKeyStorage { - constructor() { - super(); - this.keyFilePath = keyDb; - } - - /** - * Retrieve keys from the key.json file. - * - * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Ignored for file storage - * @returns {Promise} User keys or null if file doesn't exist - */ - async getKeys(context) { - try { - if (!fs.existsSync(this.keyFilePath)) { - logger.debug('Key file does not exist'); - return null; - } - - const keyData = fs.readFileSync(this.keyFilePath, 'utf-8'); - const keys = JSON.parse(keyData); - - logger.debug('Keys retrieved from file'); - return { - secretKey: keys.secretKey, - publicKey: keys.publicKey, - sharedSecretKey: keys.sharedSecretKey, - sharedPublicKey: keys.sharedPublicKey, - }; - } catch (error) { - logger.error('Error reading keys from file:', error); - throw new Error(`Failed to read keys from file: ${error.message}`); - } - } - - /** - * Save keys to the key.json file. - * - * @param {import('./IKeyStorage.mjs').UserKeys} keys - User keys to save - * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Ignored for file storage - * @returns {Promise} - */ - async saveKeys(keys, context) { - try { - // Ensure directory exists - const dir = '/app/orchestration/common/db'; - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - - const keyJson = { - secretKey: keys.secretKey, - publicKey: keys.publicKey, - }; - - // Include optional shared keys if present - if (keys.sharedSecretKey) { - keyJson.sharedSecretKey = keys.sharedSecretKey; - } - if (keys.sharedPublicKey) { - keyJson.sharedPublicKey = keys.sharedPublicKey; - } - - fs.writeFileSync(this.keyFilePath, JSON.stringify(keyJson, null, 4)); - logger.debug('Keys saved to file'); - } catch (error) { - logger.error('Error saving keys to file:', error); - throw new Error(`Failed to save keys to file: ${error.message}`); - } - } - - /** - * Register a new key pair. - * This replicates the logic from contract.mjs registerKey() function. - * - * @param {string} _secretKey - Secret key to register (hex string) - * @param {string} contractName - Associated contract name - * @param {boolean} registerWithContract - Whether to register the key on-chain - * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Ignored for file storage - * @returns {Promise} Public key (as integer string) - */ - async registerKey(_secretKey, contractName, registerWithContract, context) { - try { - let secretKey = generalise(_secretKey); - let publicKeyPoint = generalise( - scalarMult(secretKey.hex(32), config.BABYJUBJUB.GENERATOR) - ); - let publicKey = compressStarlightKey(publicKeyPoint); - - // Regenerate if public key is too large - while (publicKey === null) { - logger.warn('Secret key created a large public key - regenerating'); - secretKey = generalise(utils.randomHex(31)); - publicKeyPoint = generalise( - scalarMult(secretKey.hex(32), config.BABYJUBJUB.GENERATOR) - ); - publicKey = compressStarlightKey(publicKeyPoint); - } - - // Register on-chain if requested - if (registerWithContract) { - // Import here to avoid circular dependency - const { getContractInstance, getContractAddress } = await import('../contract.mjs'); - const Web3 = await import('../web3.mjs'); - const web3 = Web3.default.connection(); - - const instance = await getContractInstance(contractName); - const contractAddr = await getContractAddress(contractName); - const txData = await instance.methods - .registerZKPPublicKey(publicKey.integer) - .encodeABI(); - - const txParams = { - from: config.web3.options.defaultAccount, - to: contractAddr, - gas: config.web3.options.defaultGas, - gasPrice: config.web3.options.defaultGasPrice, - data: txData, - chainId: await web3.eth.net.getId(), - }; - - const key = config.web3.key; - const signed = await web3.eth.accounts.signTransaction(txParams, key); - await web3.eth.sendSignedTransaction(signed.rawTransaction); - logger.info('Key registered on-chain'); - } - - // Save keys to file - await this.saveKeys({ - secretKey: secretKey.integer, - publicKey: publicKey.integer, - }); - - logger.info('Key registered successfully'); - return publicKey.integer; - } catch (error) { - logger.error('Error registering key:', error); - throw new Error(`Failed to register key: ${error.message}`); - } - } - - /** - * Get or create shared secret keys for encrypted communication. - * This replicates the logic from commitment-storage.mjs getSharedSecretskeys() function. - * - * @param {string} _recipientAddress - Recipient's Ethereum address - * @param {string|number} _recipientPublicKey - Recipient's public key (0 to fetch from contract) - * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Ignored for file storage - * @returns {Promise} Shared public key - */ - async getSharedSecretKeys(_recipientAddress, _recipientPublicKey = 0, context) { - try { - // Ensure keys exist - if (!fs.existsSync(this.keyFilePath)) { - await this.registerKey(utils.randomHex(31), null, false); - } - - const keys = await this.getKeys(); - const secretKey = generalise(keys.secretKey); - const publicKey = generalise(keys.publicKey); - let recipientPublicKey = generalise(_recipientPublicKey); - const recipientAddress = generalise(_recipientAddress); - - // Fetch recipient's public key from contract if not provided - if (_recipientPublicKey === 0) { - // Import here to avoid circular dependency - const { getContractInstance } = await import('../contract.mjs'); - const instance = await getContractInstance('CONTRACT_NAME'); - - recipientPublicKey = await instance.methods - .zkpPublicKeys(recipientAddress.hex(20)) - .call(); - recipientPublicKey = generalise(recipientPublicKey); - - if (recipientPublicKey.length === 0) { - throw new Error('Public key for given eth address not found'); - } - } - - // Generate shared secret - const sharedKey = sharedSecretKey(secretKey, recipientPublicKey); - logger.debug('Shared key generated:', sharedKey[1]); - - // Update keys with shared secret - await this.saveKeys({ - secretKey: secretKey.integer, - publicKey: publicKey.integer, - sharedSecretKey: sharedKey[0].integer, - sharedPublicKey: sharedKey[1].integer, - }); - - return sharedKey[1]; - } catch (error) { - logger.error('Error getting shared secret keys:', error); - throw new Error(`Failed to get shared secret keys: ${error.message}`); - } - } - - /** - * Check if keys exist in the file. - * - * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Ignored for file storage - * @returns {Promise} True if key file exists - */ - async hasKeys(context) { - return fs.existsSync(this.keyFilePath); - } - - /** - * Delete the key file. - * - * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Ignored for file storage - * @returns {Promise} True if file was deleted, false if it didn't exist - */ - async deleteKeys(context) { - try { - if (fs.existsSync(this.keyFilePath)) { - fs.unlinkSync(this.keyFilePath); - logger.info('Key file deleted'); - return true; - } - return false; - } catch (error) { - logger.error('Error deleting key file:', error); - throw new Error(`Failed to delete key file: ${error.message}`); - } - } - - async getAccountIdByEthAddress(ethAddress) { - logger.debug(`getAccountIdByEthAddress not supported in single-tenant mode (address: ${ethAddress})`); - return null; - } -} - -export default FileKeyStorage; - diff --git a/src/boilerplate/common/key-management/IKeyStorage.mjs b/src/boilerplate/common/key-management/IKeyStorage.mjs deleted file mode 100644 index f650e0a1..00000000 --- a/src/boilerplate/common/key-management/IKeyStorage.mjs +++ /dev/null @@ -1,116 +0,0 @@ -/** - * @file IKeyStorage.mjs - * @description Interface definition for key storage implementations. - * This file defines the contract that both FileKeyStorage and DatabaseKeyStorage must implement. - */ - -/** - * @typedef {Object} SaaSContext - * @property {string} accountId - Unique identifier for the user/account in multi-tenant mode - */ - -/** - * @typedef {Object} UserKeys - * @property {string} secretKey - User's ZKP secret key (as integer string) - * @property {string} publicKey - User's ZKP public key (as integer string) - * @property {string} ethSK - User's Ethereum private key - * @property {string} ethPK - User's Ethereum address - * @property {string} [sharedSecretKey] - Optional shared secret key for encrypted communication - * @property {string} [sharedPublicKey] - Optional shared public key - */ - -/** - * @typedef {Object} KeyMetadata - * @property {number} keyVersion - Version number for key rotation support - * @property {string} contractName - Associated contract name - * @property {boolean} registeredOnChain - Whether the key is registered on-chain - * @property {Date} [lastUsed] - Last time the key was accessed - */ - -/** - * Base class for key storage implementations. - * This class defines the interface that all key storage implementations must follow. - * - * @abstract - */ -export class IKeyStorage { - /** - * Retrieve keys for a user. - * - * @param {SaaSContext} [context] - Optional SaaS context for multi-tenant mode - * @returns {Promise} User keys or null if not found - * @abstract - */ - async getKeys(context) { - throw new Error('getKeys() must be implemented by subclass'); - } - - /** - * Save keys for a user. - * - * @param {UserKeys} keys - User keys to save - * @param {SaaSContext} [context] - Optional SaaS context for multi-tenant mode - * @returns {Promise} - * @abstract - */ - async saveKeys(keys, context) { - throw new Error('saveKeys() must be implemented by subclass'); - } - - /** - * Register a new key pair. - * Generates a public key from the secret key and optionally registers it on-chain. - * - * @param {string} secretKey - Secret key to register (hex string) - * @param {string} contractName - Associated contract name - * @param {boolean} registerWithContract - Whether to register the key on-chain - * @param {SaaSContext} [context] - Optional SaaS context for multi-tenant mode - * @returns {Promise} Public key (as integer string) - * @abstract - */ - async registerKey(secretKey, contractName, registerWithContract, context) { - throw new Error('registerKey() must be implemented by subclass'); - } - - /** - * Get or create shared secret keys for encrypted communication with another user. - * - * @param {string} recipientAddress - Recipient's Ethereum address - * @param {string|number} recipientPublicKey - Recipient's public key - * @param {SaaSContext} [context] - Optional SaaS context for multi-tenant mode - * @returns {Promise} Shared public key - * @abstract - */ - async getSharedSecretKeys(recipientAddress, recipientPublicKey, context) { - throw new Error('getSharedSecretKeys() must be implemented by subclass'); - } - - /** - * Check if keys exist for a user. - * - * @param {SaaSContext} [context] - Optional SaaS context for multi-tenant mode - * @returns {Promise} True if keys exist, false otherwise - * @abstract - */ - async hasKeys(context) { - throw new Error('hasKeys() must be implemented by subclass'); - } - - /** - * Delete keys for a user (optional, for key rotation or cleanup). - * - * @param {SaaSContext} [context] - Optional SaaS context for multi-tenant mode - * @returns {Promise} True if keys were deleted, false if they didn't exist - * @abstract - */ - async deleteKeys(context) { - throw new Error('deleteKeys() must be implemented by subclass'); - } - - async getAccountIdByEthAddress(ethAddress) { - throw new Error('getAccountIdByEthAddress() must be implemented by subclass'); - } -} - -export default IKeyStorage; - diff --git a/src/boilerplate/common/key-management/KeyManager.mjs b/src/boilerplate/common/key-management/KeyManager.mjs deleted file mode 100644 index 0ec5e2d2..00000000 --- a/src/boilerplate/common/key-management/KeyManager.mjs +++ /dev/null @@ -1,221 +0,0 @@ -/** - * @file KeyManager.mjs - * @description Singleton key manager that routes to appropriate storage based on context. - * Provides a unified interface for key management that works in both single-tenant (file-based) - * and multi-tenant (database-based) modes. - */ - -import logger from '../logger.mjs'; -import FileKeyStorage from './FileKeyStorage.mjs'; -import DatabaseKeyStorage from './DatabaseKeyStorage.mjs'; - -/** - * KeyManager singleton class. - * Routes key operations to FileKeyStorage or DatabaseKeyStorage based on context. - * - * Usage: - * const keyManager = KeyManager.getInstance(); - * - * // Single-tenant mode (no context) - * const keys = await keyManager.getKeys(); - * - * // Multi-tenant mode (with context) - * const keys = await keyManager.getKeys({ accountId: 'user-123' }); - */ -export class KeyManager { - /** - * @private - * @type {KeyManager} - */ - static instance = null; - - /** - * @private - */ - constructor() { - if (KeyManager.instance) { - throw new Error('KeyManager is a singleton. Use KeyManager.getInstance() instead.'); - } - - this.fileStorage = new FileKeyStorage(); - this.dbStorage = new DatabaseKeyStorage(); - - logger.debug('KeyManager initialized'); - } - - /** - * Get the singleton instance of KeyManager. - * - * @returns {KeyManager} - */ - static getInstance() { - if (!KeyManager.instance) { - KeyManager.instance = new KeyManager(); - } - return KeyManager.instance; - } - - /** - * Reset the singleton instance (useful for testing). - * - * @private - */ - static resetInstance() { - KeyManager.instance = null; - } - - /** - * Get the appropriate storage implementation based on context. - * - * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Optional SaaS context - * @returns {import('./IKeyStorage.mjs').IKeyStorage} Storage implementation - * @private - */ - getStorage(context) { - if (context && context.accountId) { - logger.debug(`Using DatabaseKeyStorage for accountId: ${context.accountId}`); - return this.dbStorage; - } - - logger.debug('Using FileKeyStorage (single-tenant mode)'); - return this.fileStorage; - } - - /** - * Retrieve keys for a user. - * - * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Optional SaaS context for multi-tenant mode - * @returns {Promise} User keys or null if not found - */ - async getKeys(context) { - try { - const storage = this.getStorage(context); - return await storage.getKeys(context); - } catch (error) { - logger.error('KeyManager.getKeys failed:', error); - throw error; - } - } - - /** - * Save keys for a user. - * - * @param {import('./IKeyStorage.mjs').UserKeys} keys - User keys to save - * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Optional SaaS context for multi-tenant mode - * @returns {Promise} - */ - async saveKeys(keys, context) { - try { - const storage = this.getStorage(context); - return await storage.saveKeys(keys, context); - } catch (error) { - logger.error('KeyManager.saveKeys failed:', error); - throw error; - } - } - - /** - * Register a new key pair. - * - * @param {string} secretKey - Secret key to register (hex string) - * @param {string} contractName - Associated contract name - * @param {boolean} registerWithContract - Whether to register the key on-chain - * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Optional SaaS context for multi-tenant mode - * @returns {Promise} Public key (as integer string) - */ - async registerKey(secretKey, contractName, registerWithContract, context) { - try { - const storage = this.getStorage(context); - return await storage.registerKey(secretKey, contractName, registerWithContract, context); - } catch (error) { - logger.error('KeyManager.registerKey failed:', error); - throw error; - } - } - - /** - * Get or create shared secret keys for encrypted communication. - * - * @param {string} recipientAddress - Recipient's Ethereum address - * @param {string|number} recipientPublicKey - Recipient's public key (0 to fetch from contract) - * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Optional SaaS context for multi-tenant mode - * @returns {Promise} Shared public key - */ - async getSharedSecretKeys(recipientAddress, recipientPublicKey, context) { - try { - const storage = this.getStorage(context); - return await storage.getSharedSecretKeys(recipientAddress, recipientPublicKey, context); - } catch (error) { - logger.error('KeyManager.getSharedSecretKeys failed:', error); - throw error; - } - } - - /** - * Check if keys exist for a user. - * - * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Optional SaaS context for multi-tenant mode - * @returns {Promise} True if keys exist, false otherwise - */ - async hasKeys(context) { - try { - const storage = this.getStorage(context); - return await storage.hasKeys(context); - } catch (error) { - logger.error('KeyManager.hasKeys failed:', error); - throw error; - } - } - - /** - * Delete keys for a user. - * - * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Optional SaaS context for multi-tenant mode - * @returns {Promise} True if keys were deleted, false if they didn't exist - */ - async deleteKeys(context) { - try { - const storage = this.getStorage(context); - return await storage.deleteKeys(context); - } catch (error) { - logger.error('KeyManager.deleteKeys failed:', error); - throw error; - } - } - - /** - * Get storage mode information (for debugging/monitoring). - * - * @param {import('./IKeyStorage.mjs').SaaSContext} [context] - Optional SaaS context - * @returns {Object} Storage mode information - */ - getStorageInfo(context) { - const storage = this.getStorage(context); - return { - mode: storage instanceof DatabaseKeyStorage ? 'database' : 'file', - multiTenant: !!context?.accountId, - accountId: context?.accountId || null, - }; - } - - async getAccountIdByEthAddress(ethAddress) { - try { - return await this.dbStorage.getAccountIdByEthAddress(ethAddress); - } catch (error) { - logger.error('KeyManager.getAccountIdByEthAddress failed:', error); - throw error; - } - } -} - -/** - * Convenience function to get the KeyManager instance. - * - * @returns {KeyManager} - */ -export function getKeyManager() { - return KeyManager.getInstance(); -} - -export default KeyManager; - diff --git a/src/boilerplate/common/key-management/encryption.mjs b/src/boilerplate/common/key-management/encryption.mjs deleted file mode 100644 index 4d866551..00000000 --- a/src/boilerplate/common/key-management/encryption.mjs +++ /dev/null @@ -1,208 +0,0 @@ -/** - * @file encryption.mjs - * @description Encryption utilities for securing sensitive key data at rest. - * Uses AES-256-GCM for authenticated encryption. - */ - -import crypto from 'crypto'; -import logger from '../logger.mjs'; - -const ALGORITHM = 'aes-256-gcm'; -const KEY_LENGTH = 32; // 256 bits -const IV_LENGTH = 16; // 128 bits -const AUTH_TAG_LENGTH = 16; // 128 bits -const SALT_LENGTH = 32; // 256 bits - -/** - * Get or generate the encryption key from environment variable. - * The key should be a 64-character hex string (32 bytes). - * - * @returns {Buffer} Encryption key - * @throws {Error} If KEY_ENCRYPTION_KEY is not set or invalid - */ -function getEncryptionKey() { - const keyHex = process.env.KEY_ENCRYPTION_KEY; - - if (!keyHex) { - // In development/single-tenant mode, we can use a default key - // In production multi-tenant mode, this MUST be set - const defaultKey = '0'.repeat(64); // 32 bytes of zeros - logger.warn( - 'KEY_ENCRYPTION_KEY environment variable not set. Using default key. ' + - 'THIS IS INSECURE FOR PRODUCTION USE!' - ); - return Buffer.from(defaultKey, 'hex'); - } - - // Validate key format - if (!/^[0-9a-fA-F]{64}$/.test(keyHex)) { - throw new Error( - 'KEY_ENCRYPTION_KEY must be a 64-character hexadecimal string (32 bytes)' - ); - } - - return Buffer.from(keyHex, 'hex'); -} - -/** - * Check if encryption is enabled. - * Encryption is enabled if KEY_ENCRYPTION_ENABLED is set to 'true' or if running in multi-tenant mode. - * - * @returns {boolean} True if encryption is enabled - */ -export function isEncryptionEnabled() { - return process.env.KEY_ENCRYPTION_ENABLED === 'true'; -} - -/** - * Encrypt a plaintext value using AES-256-GCM. - * - * Format: encrypted:AES256GCM:iv:authTag:ciphertext - * All components are hex-encoded. - * - * @param {string} plaintext - The value to encrypt - * @returns {string} Encrypted value in the format above - * @throws {Error} If encryption fails - */ -export function encrypt(plaintext) { - try { - const key = getEncryptionKey(); - const iv = crypto.randomBytes(IV_LENGTH); - - const cipher = crypto.createCipheriv(ALGORITHM, key, iv); - - let ciphertext = cipher.update(plaintext, 'utf8', 'hex'); - ciphertext += cipher.final('hex'); - - const authTag = cipher.getAuthTag(); - - // Format: encrypted:AES256GCM:iv:authTag:ciphertext - const encrypted = [ - 'encrypted', - 'AES256GCM', - iv.toString('hex'), - authTag.toString('hex'), - ciphertext - ].join(':'); - - logger.debug('Value encrypted successfully'); - return encrypted; - } catch (error) { - logger.error('Encryption failed:', error); - throw new Error(`Encryption failed: ${error.message}`); - } -} - -/** - * Decrypt a value that was encrypted with the encrypt() function. - * - * @param {string} encryptedValue - The encrypted value to decrypt - * @returns {string} Decrypted plaintext - * @throws {Error} If decryption fails or format is invalid - */ -export function decrypt(encryptedValue) { - try { - // Check if value is encrypted - if (!encryptedValue.startsWith('encrypted:AES256GCM:')) { - throw new Error('Invalid encrypted value format: missing prefix'); - } - - const parts = encryptedValue.split(':'); - if (parts.length !== 5) { - throw new Error( - `Invalid encrypted value format: expected 5 parts, got ${parts.length}` - ); - } - - const [prefix, algorithm, ivHex, authTagHex, ciphertext] = parts; - - // Validate components - if (prefix !== 'encrypted' || algorithm !== 'AES256GCM') { - throw new Error('Invalid encrypted value format: invalid prefix or algorithm'); - } - - const key = getEncryptionKey(); - const iv = Buffer.from(ivHex, 'hex'); - const authTag = Buffer.from(authTagHex, 'hex'); - - // Validate lengths - if (iv.length !== IV_LENGTH) { - throw new Error(`Invalid IV length: expected ${IV_LENGTH}, got ${iv.length}`); - } - if (authTag.length !== AUTH_TAG_LENGTH) { - throw new Error(`Invalid auth tag length: expected ${AUTH_TAG_LENGTH}, got ${authTag.length}`); - } - - const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); - decipher.setAuthTag(authTag); - - let plaintext = decipher.update(ciphertext, 'hex', 'utf8'); - plaintext += decipher.final('utf8'); - - logger.debug('Value decrypted successfully'); - return plaintext; - } catch (error) { - logger.error('Decryption failed:', error); - throw new Error(`Decryption failed: ${error.message}`); - } -} - -/** - * Check if a value is encrypted. - * - * @param {string} value - The value to check - * @returns {boolean} True if the value appears to be encrypted - */ -export function isEncrypted(value) { - return typeof value === 'string' && value.startsWith('encrypted:AES256GCM:'); -} - -/** - * Conditionally encrypt a value based on whether encryption is enabled. - * If encryption is disabled, returns the value as-is. - * - * @param {string} value - The value to potentially encrypt - * @returns {string} Encrypted value or original value - */ -export function encryptIfEnabled(value) { - if (isEncryptionEnabled()) { - return encrypt(value); - } - return value; -} - -/** - * Conditionally decrypt a value if it's encrypted. - * If the value is not encrypted, returns it as-is. - * - * @param {string} value - The value to potentially decrypt - * @returns {string} Decrypted value or original value - */ -export function decryptIfEncrypted(value) { - if (isEncrypted(value)) { - return decrypt(value); - } - return value; -} - -/** - * Generate a random encryption key suitable for KEY_ENCRYPTION_KEY. - * This is a utility function for initial setup. - * - * @returns {string} 64-character hex string (32 bytes) - */ -export function generateEncryptionKey() { - const key = crypto.randomBytes(KEY_LENGTH); - return key.toString('hex'); -} - -export default { - encrypt, - decrypt, - isEncrypted, - encryptIfEnabled, - decryptIfEncrypted, - isEncryptionEnabled, - generateEncryptionKey -}; - diff --git a/src/boilerplate/common/key-management/index.mjs b/src/boilerplate/common/key-management/index.mjs deleted file mode 100644 index f7eb0403..00000000 --- a/src/boilerplate/common/key-management/index.mjs +++ /dev/null @@ -1,73 +0,0 @@ -/** - * @file index.mjs - * @description Main entry point for the key management system. - * Exports all key management components for easy importing. - */ - -// Core components -export { IKeyStorage } from './IKeyStorage.mjs'; -export { FileKeyStorage } from './FileKeyStorage.mjs'; -export { DatabaseKeyStorage } from './DatabaseKeyStorage.mjs'; -export { KeyManager, getKeyManager } from './KeyManager.mjs'; - -// Encryption utilities -export { - encrypt, - decrypt, - isEncrypted, - encryptIfEnabled, - decryptIfEncrypted, - isEncryptionEnabled, - generateEncryptionKey -} from './encryption.mjs'; - -// Middleware -export { - saasContextMiddleware, - requireSaasContext, - forbidSaasContext, - getSaasContext, - isMultiTenant -} from '../middleware/saas-context.mjs'; - -/** - * Convenience function to get a configured KeyManager instance. - * This is the recommended way to access key management functionality. - * - * @returns {KeyManager} - * - * @example - * import { getKeyManager } from './key-management/index.mjs'; - * - * const keyManager = getKeyManager(); - * const keys = await keyManager.getKeys(req.saasContext); - */ -export function getKeyManager() { - return KeyManager.getInstance(); -} - -export default { - // Core - IKeyStorage, - FileKeyStorage, - DatabaseKeyStorage, - KeyManager, - getKeyManager, - - // Encryption - encrypt, - decrypt, - isEncrypted, - encryptIfEnabled, - decryptIfEncrypted, - isEncryptionEnabled, - generateEncryptionKey, - - // Middleware - saasContextMiddleware, - requireSaasContext, - forbidSaasContext, - getSaasContext, - isMultiTenant, -}; - diff --git a/src/boilerplate/common/middleware/saas-context.mjs b/src/boilerplate/common/middleware/saas-context.mjs deleted file mode 100644 index f6cea6e9..00000000 --- a/src/boilerplate/common/middleware/saas-context.mjs +++ /dev/null @@ -1,194 +0,0 @@ -/** - * @file saas-context.mjs - * @description Express middleware for parsing and validating x-saas-context header. - * This middleware enables multi-tenant mode by extracting the accountId from the request header. - */ - -import logger from '../logger.mjs'; -import config from 'config'; - -/** - * Middleware to parse and validate the x-saas-context header. - * - * Header format: - * x-saas-context: {"accountId": "user-123"} - * - * Behavior depends on config.multiTenant setting: - * - If config.multiTenant is true (strict mode): - * * Header is REQUIRED - returns 400 if missing - * * All requests must include valid x-saas-context header - * - If config.multiTenant is false (permissive mode): - * * Header is optional - proceeds in single-tenant mode if missing - * * Backward compatible with single-tenant deployments - * - * If the header is present and valid, attaches req.saasContext with the parsed data. - * If the header is present but invalid, returns a 400 error. - * - * @param {import('express').Request} req - Express request object - * @param {import('express').Response} res - Express response object - * @param {import('express').NextFunction} next - Express next function - */ -export function saasContextMiddleware(req, res, next) { - try { - const headerValue = req.headers['x-saas-context']; - const isStrictMode = config.multiTenant === true; - - // If header is not present - if (!headerValue) { - // In strict multi-tenant mode, header is required - if (isStrictMode) { - logger.warn('x-saas-context header required in multi-tenant mode but not provided'); - return res.status(400).json({ - error: 'SaaS context required', - message: 'This application is running in multi-tenant mode and requires the x-saas-context header', - example: '{"accountId": "user-123"}', - hint: 'Add the x-saas-context header to your request' - }); - } - - // In permissive mode, proceed in single-tenant mode - logger.debug('No x-saas-context header - using single-tenant mode'); - req.saasContext = undefined; - return next(); - } - - // Parse the header value - let context; - try { - context = JSON.parse(headerValue); - } catch (parseError) { - logger.warn('Invalid JSON in x-saas-context header:', parseError); - return res.status(400).json({ - error: 'Invalid x-saas-context header', - message: 'Header value must be valid JSON', - example: '{"accountId": "user-123"}' - }); - } - - // Validate accountId is present - if (!context.accountId) { - logger.warn('x-saas-context header missing accountId'); - return res.status(400).json({ - error: 'Invalid x-saas-context header', - message: 'accountId is required', - example: '{"accountId": "user-123"}' - }); - } - - // Validate accountId is a string - if (typeof context.accountId !== 'string') { - logger.warn('x-saas-context accountId is not a string:', typeof context.accountId); - return res.status(400).json({ - error: 'Invalid x-saas-context header', - message: 'accountId must be a string', - received: typeof context.accountId - }); - } - - // Validate accountId format (alphanumeric, hyphens, underscores only) - // This prevents injection attacks and ensures compatibility with database queries - if (!/^[a-zA-Z0-9_-]+$/.test(context.accountId)) { - logger.warn('x-saas-context accountId has invalid format:', context.accountId); - return res.status(400).json({ - error: 'Invalid x-saas-context header', - message: 'accountId must contain only alphanumeric characters, hyphens, and underscores', - pattern: '^[a-zA-Z0-9_-]+$', - received: context.accountId - }); - } - - // Validate accountId length (prevent excessively long IDs) - if (context.accountId.length > 128) { - logger.warn('x-saas-context accountId too long:', context.accountId.length); - return res.status(400).json({ - error: 'Invalid x-saas-context header', - message: 'accountId must be 128 characters or less', - received: context.accountId.length - }); - } - - // Attach validated context to request - req.saasContext = { - accountId: context.accountId - }; - - logger.debug(`SaaS context set for accountId: ${context.accountId}`); - next(); - } catch (error) { - // Catch any unexpected errors - logger.error('Unexpected error in saasContextMiddleware:', error); - return res.status(500).json({ - error: 'Internal server error', - message: 'Failed to process x-saas-context header' - }); - } -} - -/** - * Middleware to require SaaS context (multi-tenant mode). - * Use this middleware on routes that MUST have a SaaS context. - * - * @param {import('express').Request} req - Express request object - * @param {import('express').Response} res - Express response object - * @param {import('express').NextFunction} next - Express next function - */ -export function requireSaasContext(req, res, next) { - if (!req.saasContext || !req.saasContext.accountId) { - logger.warn('SaaS context required but not provided'); - return res.status(400).json({ - error: 'SaaS context required', - message: 'This endpoint requires the x-saas-context header', - example: '{"accountId": "user-123"}' - }); - } - next(); -} - -/** - * Middleware to forbid SaaS context (single-tenant mode only). - * Use this middleware on routes that should NOT accept a SaaS context. - * - * @param {import('express').Request} req - Express request object - * @param {import('express').Response} res - Express response object - * @param {import('express').NextFunction} next - Express next function - */ -export function forbidSaasContext(req, res, next) { - if (req.saasContext && req.saasContext.accountId) { - logger.warn('SaaS context provided but not allowed on this endpoint'); - return res.status(400).json({ - error: 'SaaS context not allowed', - message: 'This endpoint does not support multi-tenant mode' - }); - } - next(); -} - -/** - * Get the SaaS context from a request object. - * Returns undefined if no context is present (single-tenant mode). - * - * @param {import('express').Request} req - Express request object - * @returns {import('../key-management/IKeyStorage.mjs').SaaSContext|undefined} - */ -export function getSaasContext(req) { - return req.saasContext; -} - -/** - * Check if a request is in multi-tenant mode. - * - * @param {import('express').Request} req - Express request object - * @returns {boolean} - */ -export function isMultiTenant(req) { - return !!(req.saasContext && req.saasContext.accountId); -} - -export default { - saasContextMiddleware, - requireSaasContext, - forbidSaasContext, - getSaasContext, - isMultiTenant -}; - diff --git a/src/boilerplate/common/services/generic-api_services.mjs b/src/boilerplate/common/services/generic-api_services.mjs index c118feef..0a7ed169 100644 --- a/src/boilerplate/common/services/generic-api_services.mjs +++ b/src/boilerplate/common/services/generic-api_services.mjs @@ -38,8 +38,7 @@ export class ServiceManager{ try { await startEventFilter('CONTRACT_NAME'); const FUNCTION_SIG; - SAAS_CONTEXT_HANDLING - const { tx , encEvent, encBackupEvent, _RESPONSE_} = await this.FUNCTION_NAME.FUNCTION_NAME(FUNCTION_SIG, SAAS_CONTEXT_PARAM); + const { tx , encEvent, encBackupEvent, _RESPONSE_} = await this.FUNCTION_NAME.FUNCTION_NAME(FUNCTION_SIG); // prints the tx console.log(tx); const txSerialized = serializeBigInt(tx); diff --git a/src/boilerplate/common/services/saas-key-services.mjs b/src/boilerplate/common/services/saas-key-services.mjs deleted file mode 100644 index 199f8175..00000000 --- a/src/boilerplate/common/services/saas-key-services.mjs +++ /dev/null @@ -1,150 +0,0 @@ -/* eslint-disable prettier/prettier, camelcase, prefer-const, no-unused-vars */ -import config from "config"; -import logger from "./common/logger.mjs"; -import { - getOrCreateKeys, - extractAccountId, - getKeysFromDB, - storeKeysInDB, - generateKeyPair -} from "./common/key-management.mjs"; - -export async function service_getSharedKeys(req, res) { - try { - const accountId = extractAccountId(req); - const { targetAccountId } = req.body; - - if (!targetAccountId) { - return res.status(400).send({ error: 'targetAccountId is required' }); - } - - let sharedKeys = {}; - - if (accountId) { - // SaaS mode - get keys from database - const currentUserKeys = await getKeysFromDB(accountId); - const targetUserKeys = await getKeysFromDB(targetAccountId); - - if (!currentUserKeys || !targetUserKeys) { - return res.status(404).send({ error: 'Keys not found for one or both accounts' }); - } - - sharedKeys = { - currentUserPublicKey: currentUserKeys.publicKey, - targetUserPublicKey: targetUserKeys.publicKey, - sharedPublicKey: currentUserKeys.sharedPublicKey, - sharedSecretKey: currentUserKeys.sharedSecretKey, - }; - } else { - // File mode - return error as shared keys require multi-user context - return res.status(400).send({ error: 'Shared keys require SaaS context (multi-user mode)' }); - } - - res.send({ sharedKeys }); - } catch (err) { - logger.error('Error getting shared keys:', err); - res.status(500).send({ error: err.message }); - } -} - -/** - * Service to rotate keys for a user - */ -export async function service_rotateKeys(req, res) { - try { - const accountId = extractAccountId(req); - const { contractName, registerWithContract = false } = req.body; - - if (!accountId) { - return res.status(400).send({ error: 'SaaS context required for key rotation' }); - } - - // Generate new keys - const newKeys = generateKeyPair(); - - // Store new keys - await storeKeysInDB(accountId, newKeys); - - // Register with contract if requested - if (registerWithContract && contractName) { - const { registerKeyWithContract } = await import('./common/key-management.mjs'); - await registerKeyWithContract(newKeys.publicKey, contractName); - } - - res.send({ - message: 'Keys rotated successfully', - publicKey: newKeys.publicKey, - sharedPublicKey: newKeys.sharedPublicKey - }); - } catch (err) { - logger.error('Error rotating keys:', err); - res.status(500).send({ error: err.message }); - } -} - -/** - * Service to get current user's public keys - */ -export async function service_getUserKeys(req, res) { - try { - const accountId = extractAccountId(req); - - if (!accountId) { - return res.status(400).send({ error: 'SaaS context required' }); - } - - const keys = await getKeysFromDB(accountId); - - if (!keys) { - return res.status(404).send({ error: 'Keys not found' }); - } - - // Only return public keys for security - const publicKeys = { - publicKey: keys.publicKey, - sharedPublicKey: keys.sharedPublicKey, - accountId: accountId, - }; - - res.send({ keys: publicKeys }); - } catch (err) { - logger.error('Error getting user keys:', err); - res.status(500).send({ error: err.message }); - } -} - -/** - * Service to initialize keys for a new user - */ -export async function service_initializeUserKeys(req, res) { - try { - const accountId = extractAccountId(req); - const { contractName, registerWithContract = false } = req.body; - - if (!accountId) { - return res.status(400).send({ error: 'SaaS context required for user key initialization' }); - } - - // Check if keys already exist - const existingKeys = await getKeysFromDB(accountId); - if (existingKeys) { - return res.status(409).send({ - error: 'Keys already exist for this account', - publicKey: existingKeys.publicKey, - sharedPublicKey: existingKeys.sharedPublicKey - }); - } - - // Get or create keys (will create new ones since they don't exist) - const keys = await getOrCreateKeys(accountId, contractName, registerWithContract); - - res.send({ - message: 'Keys initialized successfully', - publicKey: keys.publicKey, - sharedPublicKey: keys.sharedPublicKey - }); - } catch (err) { - logger.error('Error initializing user keys:', err); - res.status(500).send({ error: err.message }); - } -} diff --git a/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts b/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts index 82ba7424..da2232fb 100644 --- a/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts +++ b/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts @@ -457,7 +457,6 @@ export function buildBoilerplateNode(nodeType: string, fields: any = {}): any { functions = [], constructorParams = [], contractImports = [], - multiTenant = false, } = fields; return { nodeType, @@ -466,7 +465,6 @@ export function buildBoilerplateNode(nodeType: string, fields: any = {}): any { functions, constructorParams, contractImports, - multiTenant, }; } case 'IntegrationApiRoutesBoilerplate': { diff --git a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts index e4a329f3..3289e788 100644 --- a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts +++ b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts @@ -42,7 +42,7 @@ class BoilerplateGenerator { \n // Initialise commitment preimage of whole accessed state: ${stateVarIds.join('\n')} \nlet ${stateName}_commitmentExists = true; - \nconst ${stateName}_commitment = await getCurrentWholeCommitment(${stateName}_stateVarId, SAAS_CONTEXT_PARAM?.accountId); + \nconst ${stateName}_commitment = await getCurrentWholeCommitment(${stateName}_stateVarId); \nconst ${stateName}_preimage = ${stateName}_commitment.preimage; \nconst ${stateName} = generalise(${stateName}_preimage.value);`]; default: @@ -51,7 +51,7 @@ class BoilerplateGenerator { ${stateVarIds.join('\n')} \nlet ${stateName}_commitmentExists = true; let ${stateName}_witnessRequired = true; - \nconst ${stateName}_commitment = await getCurrentWholeCommitment(${stateName}_stateVarId, SAAS_CONTEXT_PARAM?.accountId); + \nconst ${stateName}_commitment = await getCurrentWholeCommitment(${stateName}_stateVarId); \nlet ${stateName}_preimage = { \tvalue: ${structProperties ? `{` + structProperties.map(p => `${p}: 0`) + `}` : `0`}, \tsalt: 0, @@ -70,22 +70,20 @@ class BoilerplateGenerator { initialiseKeys = { - postStatements(contractName, onChainKeyRegistry, msgSenderParam): string[] { - const msgSenderLine = msgSenderParam ? `\nconst msgSender = generalise(keys.ethPK);` : ''; + postStatements(contractName, onChainKeyRegistry): string[] { return [ ` - \n\n// Read keys using KeyManager - \nconst keyManager = KeyManager.getInstance(); - \nlet keys = await keyManager.getKeys(context); - \nif (!keys) { - \n // No keys found, register new ones - \n await registerKey(utils.randomHex(31), '${contractName}', ${onChainKeyRegistry}, context); - \n keys = await keyManager.getKeys(context); - \n} - \nconst secretKey = generalise(keys.secretKey); - \nconst publicKey = generalise(keys.publicKey); - \nconst sharedPublicKey = keys.sharedPublicKey ? generalise(keys.sharedPublicKey) : null; - \nconst sharedSecretKey = keys.sharedSecretKey ? generalise(keys.sharedSecretKey) : null;${msgSenderLine} + \n\n// Read dbs for keys and previous commitment values: + \nif (!fs.existsSync(keyDb)) await registerKey(utils.randomHex(31), '${contractName}', ${onChainKeyRegistry}); + const keys = JSON.parse( + fs.readFileSync(keyDb, 'utf-8', err => { + console.log(err); + }), + ); + const secretKey = generalise(keys.secretKey); + const publicKey = generalise(keys.publicKey); + const sharedPublicKey = generalise(keys.sharedPublicKey); + const sharedSecretKey = generalise(keys.sharedSecretKey); ` ]; }, @@ -184,32 +182,27 @@ class BoilerplateGenerator { }; getInputCommitments = { - postStatements({ - stateName, + postStatements(): string[] { + return ['']; + }, + }; + + membershipWitness = { + postStatements({ stateName, contractName, - stateType, - mappingName, - structProperties, - isSharedSecret, - stateVarIds, - }): string[] { + stateType, mappingName, structProperties, isSharedSecret, stateVarIds }): string[] { const stateVarId: string[] = []; - if (stateVarIds.length > 1) { - stateVarId.push(stateVarIds[0].split(' = ')[1].split(';')[0]); + if(stateVarIds.length > 1){ + stateVarId.push((stateVarIds[0].split(" = ")[1]).split(";")[0]); stateVarId.push(`${stateName}_stateVarId_key`); - } else stateVarId.push(`${stateName}_stateVarId`); + } else + stateVarId.push(`${stateName}_stateVarId`); switch (stateType) { case 'partitioned': if (structProperties) - return [ - ` + return [` \n\n// First check if required commitments exist or not - \nconst ${stateName}_newCommitmentValue = generalise([${Object.values( - structProperties, - ).map( - sp => - `generalise(parseInt(${stateName}_${sp}_newCommitmentValue.integer, 10) - parseInt(${stateName}_${sp}_newCommitmentValue_inc.integer, 10))`, - )}]).all; + \nconst ${stateName}_newCommitmentValue = generalise([${Object.values(structProperties).map((sp) => `generalise(parseInt(${stateName}_${sp}_newCommitmentValue.integer, 10) - parseInt(${stateName}_${sp}_newCommitmentValue_inc.integer, 10))`)}]).all; \nlet [${stateName}_commitmentFlag, ${stateName}_0_oldCommitment, ${stateName}_1_oldCommitment] = getInputCommitments( publicKey.hex(32), ${stateName}_newCommitmentValue.integer, @@ -220,10 +213,19 @@ class BoilerplateGenerator { \nlet ${stateName}_witness_0; \nlet ${stateName}_witness_1; - `, - ]; - return [ - ` + const ${stateName}_0_prevSalt = generalise(${stateName}_0_oldCommitment.preimage.salt); + const ${stateName}_1_prevSalt = generalise(${stateName}_1_oldCommitment.preimage.salt); + const ${stateName}_0_prev = generalise(${stateName}_0_oldCommitment.preimage.value); + const ${stateName}_1_prev = generalise(${stateName}_1_oldCommitment.preimage.value); + \n\n// generate witness for partitioned state + ${stateName}_witness_0 = await getMembershipWitness('${contractName}', generalise(${stateName}_0_oldCommitment._id).integer); + ${stateName}_witness_1 = await getMembershipWitness('${contractName}', generalise(${stateName}_1_oldCommitment._id).integer); + const ${stateName}_0_index = generalise(${stateName}_witness_0.index); + const ${stateName}_1_index = generalise(${stateName}_witness_1.index); + const ${stateName}_root = generalise(${stateName}_witness_0.root); + const ${stateName}_0_path = generalise(${stateName}_witness_0.path).all; + const ${stateName}_1_path = generalise(${stateName}_witness_1.path).all;\n`]; + return [` \n\n// First check if required commitments exist or not \n${stateName}_newCommitmentValue = generalise(parseInt(${stateName}_newCommitmentValue.integer, 10) - parseInt(${stateName}_newCommitmentValue_inc.integer, 10)); \nlet [${stateName}_commitmentFlag, ${stateName}_0_oldCommitment, ${stateName}_1_oldCommitment] = getInputCommitments( @@ -236,7 +238,7 @@ class BoilerplateGenerator { if(${stateName}_1_oldCommitment === null && ${stateName}_commitmentFlag){ \n${stateName}_witness_0 = await getMembershipWitness('${contractName}', generalise(${stateName}_0_oldCommitment._id).integer); - \n const tx = await splitCommitments('${contractName}', '${mappingName}', ${stateName}_newCommitmentValue, secretKey, publicKey, [${stateVarId.join(' , ')}], ${stateName}_0_oldCommitment, ${stateName}_witness_0, instance, contractAddr, web3, context); + \n const tx = await splitCommitments('${contractName}', '${mappingName}', ${stateName}_newCommitmentValue, secretKey, publicKey, [${stateVarId.join(' , ')}], ${stateName}_0_oldCommitment, ${stateName}_witness_0, instance, contractAddr, web3); ${stateName}_preimage = await getCommitmentsById(${stateName}_stateVarId); [${stateName}_commitmentFlag, ${stateName}_0_oldCommitment, ${stateName}_1_oldCommitment] = getInputCommitments( @@ -250,57 +252,16 @@ class BoilerplateGenerator { \n${stateName}_witness_0 = await getMembershipWitness('${contractName}', generalise(${stateName}_0_oldCommitment._id).integer); \n${stateName}_witness_1 = await getMembershipWitness('${contractName}', generalise(${stateName}_1_oldCommitment._id).integer); - \n const tx = await joinCommitments('${contractName}', '${mappingName}', ${isSharedSecret? `sharedSecretKey, sharedPublicKey`: `secretKey, publicKey`}, [${stateVarId.join(' , ')}], [${stateName}_0_oldCommitment, ${stateName}_1_oldCommitment], [${stateName}_witness_0, ${stateName}_witness_1], instance, contractAddr, web3, context); + \n const tx = await joinCommitments('${contractName}', '${mappingName}', ${isSharedSecret? `sharedSecretKey, sharedPublicKey`: `secretKey, publicKey`}, [${stateVarId.join(' , ')}], [${stateName}_0_oldCommitment, ${stateName}_1_oldCommitment], [${stateName}_witness_0, ${stateName}_witness_1], instance, contractAddr, web3); ${stateName}_preimage = await getCommitmentsById(${stateName}_stateVarId); [${stateName}_commitmentFlag, ${stateName}_0_oldCommitment, ${stateName}_1_oldCommitment] = getInputCommitments( - ${ - isSharedSecret - ? `sharedPublicKey.hex(32)` - : `publicKey.hex(32)` - }, + ${isSharedSecret ? `sharedPublicKey.hex(32)` : `publicKey.hex(32)`}, ${stateName}_newCommitmentValue.integer, ${stateName}_preimage, ); } - `, - ]; - default: - throw new TypeError(stateType); - } - }, - }; - - membershipWitness = { - postStatements({ - stateName, - contractName, - stateType, - mappingName, - structProperties, - isSharedSecret, - stateVarIds, - }): string[] { - switch (stateType) { - case 'partitioned': - if (structProperties) - return [ - ` - const ${stateName}_0_prevSalt = generalise(${stateName}_0_oldCommitment.preimage.salt); - const ${stateName}_1_prevSalt = generalise(${stateName}_1_oldCommitment.preimage.salt); - const ${stateName}_0_prev = generalise(${stateName}_0_oldCommitment.preimage.value); - const ${stateName}_1_prev = generalise(${stateName}_1_oldCommitment.preimage.value); - \n\n// generate witness for partitioned state - ${stateName}_witness_0 = await getMembershipWitness('${contractName}', generalise(${stateName}_0_oldCommitment._id).integer); - ${stateName}_witness_1 = await getMembershipWitness('${contractName}', generalise(${stateName}_1_oldCommitment._id).integer); - const ${stateName}_0_index = generalise(${stateName}_witness_0.index); - const ${stateName}_1_index = generalise(${stateName}_witness_1.index); - const ${stateName}_root = generalise(${stateName}_witness_0.root); - const ${stateName}_0_path = generalise(${stateName}_witness_0.path).all; - const ${stateName}_1_path = generalise(${stateName}_witness_1.path).all;\n`]; - return [ - ` const ${stateName}_0_prevSalt = generalise(${stateName}_0_oldCommitment.preimage.salt); const ${stateName}_1_prevSalt = generalise(${stateName}_1_oldCommitment.preimage.salt); const ${stateName}_0_prev = generalise(${stateName}_0_oldCommitment.preimage.value); @@ -312,11 +273,9 @@ class BoilerplateGenerator { const ${stateName}_1_index = generalise(${stateName}_witness_1.index); const ${stateName}_root = generalise(${stateName}_witness_0.root); const ${stateName}_0_path = generalise(${stateName}_witness_0.path).all; - const ${stateName}_1_path = generalise(${stateName}_witness_1.path).all;\n`, - ]; + const ${stateName}_1_path = generalise(${stateName}_witness_1.path).all;\n`]; case 'whole': - return [ - ` + return [` \n\n// generate witness for whole state const ${stateName}_emptyPath = new Array(32).fill(0); const ${stateName}_witness = ${stateName}_witnessRequired @@ -324,22 +283,19 @@ class BoilerplateGenerator { \t: { index: 0, path: ${stateName}_emptyPath, root: await getRoot('${contractName}') || 0 }; const ${stateName}_index = generalise(${stateName}_witness.index); const ${stateName}_root = generalise(${stateName}_witness.root); - const ${stateName}_path = generalise(${stateName}_witness.path).all;\n`, - ]; + const ${stateName}_path = generalise(${stateName}_witness.path).all;\n`]; case 'accessedOnly': - return [ - ` + return [` \n\n// generate witness for whole accessed state const ${stateName}_witness = await getMembershipWitness('${contractName}', ${stateName}_currentCommitment.integer); const ${stateName}_index = generalise(${stateName}_witness.index); const ${stateName}_root = generalise(${stateName}_witness.root); - const ${stateName}_path = generalise(${stateName}_witness.path).all;\n`, - ]; + const ${stateName}_path = generalise(${stateName}_witness.path).all;\n`]; default: throw new TypeError(stateType); } - }, - }; + } +}; calculateNullifier = { @@ -473,14 +429,10 @@ class BoilerplateGenerator { `\nimport { storeCommitment, getCurrentWholeCommitment, getCommitmentsById, getAllCommitments, getInputCommitments, joinCommitments, splitCommitments, markNullified} from './common/commitment-storage.mjs';`, `\nimport { generateProof } from './common/zokrates.mjs';`, `\nimport { getMembershipWitness, getRoot } from './common/timber.mjs';`, - `\nimport { decompressStarlightKey, compressStarlightKey, encrypt, decrypt, poseidonHash, scalarMult } from './common/number-theory.mjs';`, - `\nimport { KeyManager } from './common/key-management/KeyManager.mjs';`, - `\nimport { autoFundIfNeeded } from './common/gas-funding.mjs';`, - `\nimport logger from './common/logger.mjs'; + `\nimport { decompressStarlightKey, compressStarlightKey, encrypt, decrypt, poseidonHash, scalarMult } from './common/number-theory.mjs'; \n`, `\nconst { generalise } = GN;`, `\nconst db = '/app/orchestration/common/db/preimage.json';`, - `\n// Legacy keyDb path - keys now managed through KeyManager`, `\nconst keyDb = '/app/orchestration/common/db/key.json';\n\n`, ]; }, @@ -686,6 +638,9 @@ sendTransaction = { perParameters, }): string[] { let value; + const domainParamsCode = perParameters && perParameters.length > 0 + ? `domainParameters: { ${perParameters.map(p => `${p.name}: ${p.name}_init`).join(', ')} },\n ` + : ''; const errorCatch = `\n console.log("Added commitment", newCommitment.hex(32)); } catch (e) { if (e.toString().includes("E11000 duplicate key")) { @@ -696,32 +651,29 @@ sendTransaction = { ); } }`; - - // Generate domain parameters object if perParameters exist - const domainParamsCode = perParameters && perParameters.length > 0 - ? `domainParameters: { ${perParameters.map(p => `${p.name}: ${p.name}_init`).join(', ')} },\n ` - : ''; - switch (stateType) { case 'increment': - value = structProperties ? `{ ${structProperties.map((p, i) => `${p}: ${stateName}_newCommitmentValue.integer[${i}]`)} }` : `${stateName}_newCommitmentValue`; + value = structProperties + ? `{ ${structProperties.map((p, i) => `${p}: ${stateName}_newCommitmentValue.integer[${i}]`)} }` + : `${stateName}_newCommitmentValue`; return [`try { \nawait storeCommitment({ hash: ${stateName}_newCommitment, name: '${mappingName}', mappingKey: ${mappingKey === `` ? `null` : `${mappingKey}`}, ${domainParamsCode}preimage: { - \tstateVarId: generalise(${stateName}_stateVarId), - \tvalue: ${value}, - \tsalt: ${stateName}_newSalt, - \tpublicKey: ${stateName}_newOwnerPublicKey, + stateVarId: generalise(${stateName}_stateVarId), + value: ${value}, + salt: ${stateName}_newSalt, + publicKey: ${stateName}_newOwnerPublicKey, }, secretKey: ${stateName}_newOwnerPublicKey.integer === ${isSharedSecret ? `sharedPublicKey.integer` : `publicKey.integer`} ? ${isSharedSecret ? `sharedSecretKey` : `secretKey`}: null, - blockNumber: Number(tx.blockNumber), isNullified: false, - }, SAAS_CONTEXT_PARAM);` + errorCatch]; + });` + errorCatch]; case 'decrement': - value = structProperties ? `{ ${structProperties.map((p, i) => `${p}: ${stateName}_change.integer[${i}]`)} }` : `${stateName}_change`; + value = structProperties + ? `{ ${structProperties.map((p, i) => `${p}: ${stateName}_change.integer[${i}]`)} }` + : `${stateName}_change`; return [` \nawait markNullified(generalise(${stateName}_0_oldCommitment._id), secretKey.hex(32)); \nawait markNullified(generalise(${stateName}_1_oldCommitment._id), secretKey.hex(32)); @@ -731,66 +683,50 @@ sendTransaction = { name: '${mappingName}', mappingKey: ${mappingKey === `` ? `null` : `${mappingKey}`}, ${domainParamsCode}preimage: { - \tstateVarId: generalise(${stateName}_stateVarId), - \tvalue: ${value}, - \tsalt: ${stateName}_2_newSalt, - \tpublicKey: ${stateName}_newOwnerPublicKey, + stateVarId: generalise(${stateName}_stateVarId), + value: ${value}, + salt: ${stateName}_2_newSalt, + publicKey: ${stateName}_newOwnerPublicKey, }, secretKey: ${stateName}_newOwnerPublicKey.integer === ${isSharedSecret ? `sharedPublicKey.integer` : `publicKey.integer`} ? ${isSharedSecret ? `sharedSecretKey` : `secretKey`}: null, - blockNumber: Number(tx.blockNumber), isNullified: false, - }, SAAS_CONTEXT_PARAM);`+ errorCatch]; + });` + errorCatch]; case 'whole': switch (burnedOnly) { case true: return [` \nawait markNullified(${stateName}_currentCommitment, secretKey.hex(32));`]; default: - value = structProperties ? `{ ${structProperties.map(p => `${p}: ${stateName}.${p}`)} }` : `${stateName}`; + value = structProperties + ? `{ ${structProperties.map(p => `${p}: ${stateName}.${p}`)} }` + : `${stateName}`; return [` - \n${reinitialisedOnly ? ' ': `if (${stateName}_commitmentExists) await markNullified(${stateName}_currentCommitment, secretKey.hex(32)); + \n${reinitialisedOnly ? ' ' : `if (${stateName}_commitmentExists) await markNullified(${stateName}_currentCommitment, secretKey.hex(32)); `} - \n// Look up recipient's accountId for proper multi-tenant isolation - \nlet ${stateName}_recipientContext = SAAS_CONTEXT_PARAM; - \nif (SAAS_CONTEXT_PARAM && ${stateName}_newOwnerPublicKey.integer !== ${isSharedSecret ? `sharedPublicKey.integer` : `publicKey.integer`}) { - \n// Commitment is being transferred to a different user - \nconst ${stateName}_recipientAddress = recipient.hex ? recipient.hex(20) : generalise(recipient).hex(20); - \nconst keyManager = KeyManager.getInstance(); - \nconst ${stateName}_recipientAccountId = await keyManager.getAccountIdByEthAddress(${stateName}_recipientAddress); - \nif (${stateName}_recipientAccountId) { - \n${stateName}_recipientContext = { accountId: ${stateName}_recipientAccountId }; - \nlogger.debug(\`Storing commitment for recipient accountId: \${${stateName}_recipientAccountId}\`); - \n} else { - \nlogger.debug(\`Recipient \${${stateName}_recipientAddress} not registered, storing without accountId\`); - \n${stateName}_recipientContext = undefined; - \n} - \n} \n try { \nawait storeCommitment({ hash: ${stateName}_newCommitment, name: '${mappingName}', mappingKey: ${mappingKey === `` ? `null` : `${mappingKey}`}, ${domainParamsCode}preimage: { - \tstateVarId: generalise(${stateName}_stateVarId), - \tvalue: ${value}, - \tsalt: ${stateName}_newSalt, - \tpublicKey: ${stateName}_newOwnerPublicKey, + stateVarId: generalise(${stateName}_stateVarId), + value: ${value}, + salt: ${stateName}_newSalt, + publicKey: ${stateName}_newOwnerPublicKey, }, secretKey: ${stateName}_newOwnerPublicKey.integer === ${isSharedSecret ? `sharedPublicKey.integer` : `publicKey.integer`} ? ${isSharedSecret ? `sharedSecretKey` : `secretKey`}: null, - blockNumber: Number(tx.blockNumber), isNullified: false, - }, ${stateName}_recipientContext);` + errorCatch]; + });` + errorCatch]; } default: throw new TypeError(stateType); - } // TODO: we might eventually import some underflow/overflow functions. + } }, -}; + }; integrationTestBoilerplate = { import(): string { - return `import { FUNCTION_CAP_NAMEManager } from './FUNCTION_NAME.mjs';\n - `; + return `import { FUNCTION_NAME } from './FUNCTION_NAME.mjs';\n`; }, encryption(): string { return ` @@ -804,12 +740,6 @@ sendTransaction = { const plainText = decrypt(encryption.msgs, secretKey, encryption.key); console.log('Decrypted plainText:'); console.log(plainText); - const salt = plainText[plainText.length - 1]; - const commitmentSet = await getAllCommitments(); - const thisCommit = commitmentSet.find(c => generalise(c.preimage.salt).integer === generalise(salt).integer); - assert.equal(!!thisCommit, true); - - } catch (err) { logger.error(err); process.exit(1); } @@ -859,7 +789,7 @@ integrationApiServicesBoilerplate = { ` }, preStatements(): string{ - return ` import { startEventFilter, getSiblingPath } from './common/timber.mjs';\nimport fs from "fs";\nimport logger from './common/logger.mjs';\nimport { decrypt } from "./common/number-theory.mjs";\nimport { getAllCommitments, getCommitmentsByState, getBalance, getSharedSecretskeys , getBalanceByState } from "./common/commitment-storage.mjs";\nimport { backupDataRetriever } from "./BackupDataRetriever.mjs";\nimport { backupVariable } from "./BackupVariable.mjs";\nimport web3 from './common/web3.mjs';\nimport { KeyManager } from './common/key-management/KeyManager.mjs';\n\n + return ` import { startEventFilter, getSiblingPath } from './common/timber.mjs';\nimport fs from "fs";\nimport logger from './common/logger.mjs';\nimport { decrypt } from "./common/number-theory.mjs";\nimport { getAllCommitments, getCommitmentsByState, getBalance, getSharedSecretskeys , getBalanceByState } from "./common/commitment-storage.mjs";\nimport { backupDataRetriever } from "./BackupDataRetriever.mjs";\nimport { backupVariable } from "./BackupVariable.mjs";\nimport web3 from './common/web3.mjs';\n\n /** NOTE: this is the api service file, if you need to call any function use the correct url and if Your input contract has two functions, add() and minus(). minus() cannot be called before an initial add(). */ @@ -893,8 +823,7 @@ integrationApiServicesBoilerplate = { return ` export async function service_allCommitments(req, res, next) { try { - const accountId = req.saasContext?.accountId; - const commitments = await getAllCommitments(accountId); + const commitments = await getAllCommitments(); res.send({ commitments }); await sleep(10); } catch (err) { @@ -904,8 +833,8 @@ integrationApiServicesBoilerplate = { } export async function service_getBalance(req, res, next) { try { - const accountId = req.saasContext?.accountId; - const sum = await getBalance(accountId); + + const sum = await getBalance(); res.send( {"totalBalance": sum} ); } catch (error) { console.error("Error in calculation :", error); @@ -915,22 +844,20 @@ integrationApiServicesBoilerplate = { export async function service_getBalanceByState(req, res, next) { try { - const { name, mappingKey, domainParameters } = req.body; - const accountId = req.saasContext?.accountId; - const balance = await getBalanceByState(name, mappingKey, accountId, domainParameters); + const { name, mappingKey } = req.body; + const balance = await getBalanceByState(name, mappingKey); res.send( {"totalBalance": balance} ); } catch (error) { console.error("Error in calculation :", error); res.status(500).send({ error: err.message }); } } - - + + export async function service_getCommitmentsByState(req, res, next) { try { - const { name, mappingKey, domainParameters } = req.body; - const accountId = req.saasContext?.accountId; - const commitments = await getCommitmentsByState(name, mappingKey, accountId, domainParameters); + const { name, mappingKey } = req.body; + const commitments = await getCommitmentsByState(name, mappingKey); res.send({ commitments }); await sleep(10); } catch (err) { @@ -942,8 +869,7 @@ integrationApiServicesBoilerplate = { export async function service_backupData(req, res, next) { try { - SAAS_CONTEXT_HANDLING - await backupDataRetriever(SAAS_CONTEXT_DIRECT); + await backupDataRetriever(); res.send("Complete"); await sleep(10); } catch (err) { @@ -954,8 +880,7 @@ integrationApiServicesBoilerplate = { export async function service_backupVariable(req, res, next) { try { const { name } = req.body; - SAAS_CONTEXT_HANDLING - await backupVariable(name, SAAS_CONTEXT_PARAM); + await backupVariable(name); res.send("Complete"); await sleep(10); } catch (err) { @@ -967,284 +892,17 @@ integrationApiServicesBoilerplate = { try { const { recipientAddress } = req.body; const recipientPubKey = req.body.recipientPubKey || 0 - SAAS_CONTEXT_HANDLING - const SharedKeys = await getSharedSecretskeys(recipientAddress, recipientPubKey, SAAS_CONTEXT_PARAM); + const SharedKeys = await getSharedSecretskeys(recipientAddress, recipientPubKey ); res.send({ SharedKeys }); await sleep(10); } catch (err) { logger.error(err); res.send({ errors: [err.message] }); } - } - export async function service_registerKeys(req, res, next) { - try { - SAAS_CONTEXT_HANDLING - - const keyManager = KeyManager.getInstance(); - let keys = await keyManager.getKeys(SAAS_CONTEXT_PARAM); - - if (keys) { - return res.send({ - success: true, - message: 'Keys already registered', - address: keys.ethPK, - publicKey: keys.publicKey - }); - } - - logger.info('Registering new keys', { accountId: SAAS_CONTEXT_PARAM?.accountId }); - - const utils = await import('zkp-utils'); - const { registerKey } = await import('./common/contract.mjs'); - - const publicKey = await registerKey( - utils.default.randomHex(31), - 'CONTRACT_NAME', - true, - SAAS_CONTEXT_PARAM - ); - - keys = await keyManager.getKeys(SAAS_CONTEXT_PARAM); - - res.send({ - success: true, - message: 'Keys registered successfully', - address: keys.ethPK, - publicKey: keys.publicKey, - zkpPublicKey: publicKey.integer - }); - } catch (err) { - logger.error('Failed to register keys:', err); - res.send({ errors: [err.message] }); - } - } - - export async function service_getAddress(req, res, next) { - try { - SAAS_CONTEXT_HANDLING - - const keyManager = KeyManager.getInstance(); - let keys = await keyManager.getKeys(SAAS_CONTEXT_PARAM); - - if (!keys) { - return res.send({ - success: false, - message: 'No keys found. Please call /registerKeys first.' - }); - } - - res.send({ - address: keys.ethPK, - publicKey: keys.publicKey - }); - } catch (err) { - logger.error(err); - res.send({ errors: [err.message] }); - } - } - - export async function service_mintNFT(req, res, next) { - try { - const { tokenId, nftContract } = req.body; - SAAS_CONTEXT_HANDLING - - const keyManager = KeyManager.getInstance(); - let keys = await keyManager.getKeys(SAAS_CONTEXT_PARAM); - - if (!keys) { - return res.send({ - success: false, - message: 'No keys found. Please call /registerKeys first.' - }); - } - - const { getContractAddress, getContractInterface } = await import('./common/contract.mjs'); - // Use provided nftContract address or fall back to default ERC721 - const erc721Address = nftContract || await getContractAddress('ERC721'); - const erc721Interface = await getContractInterface('ERC721'); - const Web3 = await import('./common/web3.mjs'); - const web3 = Web3.default.connection(); - const erc721 = new web3.eth.Contract(erc721Interface.abi, erc721Address); - - try { - const owner = await erc721.methods.ownerOf(tokenId).call(); - if (owner && owner !== '0x0000000000000000000000000000000000000000') { - return res.send({ - success: false, - message: \`Token \${tokenId} already exists (owner: \${owner})\` - }); - } - } catch (error) { - // Token doesn't exist - expected - } - - const accounts = await web3.eth.getAccounts(); - const defaultAccount = accounts[0]; - - logger.info(\`Minting token \${tokenId} to \${keys.ethPK}\`); - - const mintTx = await erc721.methods - .mint(keys.ethPK, tokenId) - .send({ from: defaultAccount, gas: 500000 }); - - res.send({ - success: true, - tokenId: tokenId, - owner: keys.ethPK, - txHash: mintTx.transactionHash - }); - } catch (err) { - logger.error('Failed to mint NFT:', err); - res.send({ errors: [err.message] }); - } - } - - export async function service_approveNFT(req, res, next) { - try { - const { tokenId, nftContract } = req.body; - SAAS_CONTEXT_HANDLING - - const keyManager = KeyManager.getInstance(); - let keys = await keyManager.getKeys(SAAS_CONTEXT_PARAM); - - if (!keys) { - return res.send({ - success: false, - message: 'No keys found. Please call /registerKeys first.' - }); - } - - const { getContractAddress, getContractInterface } = await import('./common/contract.mjs'); - // Use provided nftContract address or fall back to default ERC721 - const erc721Address = nftContract || await getContractAddress('ERC721'); - const erc721Interface = await getContractInterface('ERC721'); - const Web3 = await import('./common/web3.mjs'); - const web3 = Web3.default.connection(); - const erc721 = new web3.eth.Contract(erc721Interface.abi, erc721Address); - - const escrowAddress = await getContractAddress('CONTRACT_NAME'); - - const currentApproval = await erc721.methods.getApproved(tokenId).call(); - - if (currentApproval.toLowerCase() === escrowAddress.toLowerCase()) { - return res.send({ - success: true, - message: 'Token already approved', - tokenId: tokenId, - spender: escrowAddress - }); - } - - logger.info(\`Approving token \${tokenId} for escrow\`); - - const txData = await erc721.methods - .approve(escrowAddress, tokenId) - .encodeABI(); - - const config = await import('config'); - - let txParams = { - from: keys.ethPK, - to: erc721Address, - gas: 500000, - gasPrice: config.default.web3.options.defaultGasPrice, - data: txData, - chainId: await web3.eth.net.getId(), - }; - - const signed = await web3.eth.accounts.signTransaction(txParams, keys.ethSK); - const sendTxn = await web3.eth.sendSignedTransaction(signed.rawTransaction); - - res.send({ - success: true, - tokenId: tokenId, - spender: escrowAddress, - txHash: sendTxn.transactionHash - }); - } catch (err) { - logger.error('Failed to approve NFT:', err); - res.send({ errors: [err.message] }); - } - } - - export async function service_deployNFT(req, res, next) { - try { - const { name, symbol } = req.body; - SAAS_CONTEXT_HANDLING - - const keyManager = KeyManager.getInstance(); - let keys = await keyManager.getKeys(SAAS_CONTEXT_PARAM); - - if (!keys) { - return res.send({ - success: false, - message: 'No keys found. Please call /registerKeys first.' - }); - } - - if (!name || !symbol) { - return res.send({ - errors: ['name and symbol are required'] - }); - } - - logger.info(\`Deploying new ERC721 contract: \${name} (\${symbol})\`); - - const { getContractInterface, getContractBytecode } = await import('./common/contract.mjs'); - const Web3 = await import('./common/web3.mjs'); - const web3 = Web3.default.connection(); - - // Get the ERC721 contract interface and bytecode - const erc721Interface = await getContractInterface('ERC721'); - const erc721Bytecode = await getContractBytecode('ERC721'); - - // Create a new contract instance - const erc721Contract = new web3.eth.Contract(erc721Interface.abi); - - // Deploy the contract - const deployTx = erc721Contract.deploy({ - data: erc721Bytecode, - arguments: [name, symbol] - }); - - const config = await import('config'); - - // Auto-fund tenant address if needed (same as other endpoints) - const { autoFundIfNeeded } = await import('./common/gas-funding.mjs'); - await autoFundIfNeeded(keys.ethPK, '0.1', '0.5'); - - // Estimate gas for deployment - const gas = await deployTx.estimateGas({ from: keys.ethPK }); - - // Send the deployment transaction using tenant's keys - const txParams = { - from: keys.ethPK, - data: deployTx.encodeABI(), - gas: Math.floor(Number(gas) * 1.2), // Add 20% buffer - gasPrice: config.default.web3.options.defaultGasPrice, - chainId: await web3.eth.net.getId() - }; - - const signed = await web3.eth.accounts.signTransaction(txParams, keys.ethSK); - const receipt = await web3.eth.sendSignedTransaction(signed.rawTransaction); - - const deployedAddress = receipt.contractAddress; - - logger.info(\`ERC721 contract deployed at: \${deployedAddress}\`); - - res.send({ - success: true, - contractAddress: deployedAddress, - name: name, - symbol: symbol, - txHash: receipt.transactionHash - }); - } catch (err) { - logger.error('Failed to deploy NFT contract:', err); - res.send({ errors: [err.message] }); - } - } - `; + }` + + + ; } @@ -1264,30 +922,24 @@ integrationApiRoutesBoilerplate = { return `router.post('/FUNCTION_NAME', this.serviceMgr.service_FUNCTION_NAME.bind(this.serviceMgr),);` }, commitmentImports(): string { - return `import { service_allCommitments, service_getCommitmentsByState, service_getSharedKeys, service_getBalance, service_getBalanceByState, service_backupData, service_backupVariable, service_registerKeys, service_getAddress, service_mintNFT, service_approveNFT, service_deployNFT, } from "./api_services.mjs";\n`; + return `import { service_allCommitments, service_getCommitmentsByState, service_getSharedKeys, service_getBalance, service_getBalanceByState, service_backupData, service_backupVariable,} from "./api_services.mjs";\n`; }, commitmentRoutes(): string { return `// commitment getter routes router.get("/getAllCommitments", service_allCommitments); - router.post("/getCommitmentsByVariableName", service_getCommitmentsByState); + router.get("/getCommitmentsByVariableName", service_getCommitmentsByState); router.get("/getBalance", service_getBalance); - router.post("/getBalanceByState", service_getBalanceByState); + router.get("/getBalanceByState", service_getBalanceByState); router.post("/getSharedKeys", service_getSharedKeys); // backup route router.post("/backupDataRetriever", service_backupData); router.post("/backupVariable", service_backupVariable); - // key management routes - router.post("/registerKeys", service_registerKeys); - router.get("/getAddress", service_getAddress); - router.post("/mintNFT", service_mintNFT); - router.post("/approveNFT", service_approveNFT); - router.post("/deployNFT", service_deployNFT); `; } }; -zappFilesBoilerplate = (multiTenant = false) => { - const baseFiles = [ +zappFilesBoilerplate = () => { + return [ { readPath: pathPrefix + '/config/default.js', writePath: '/config/default.js', @@ -1379,48 +1031,6 @@ zappFilesBoilerplate = (multiTenant = false) => { generic: false, }, ]; - -if (multiTenant) { - baseFiles.push( - { - readPath: pathPrefix + '/middleware/saas-context.mjs', - writePath: './orchestration/common/middleware/saas-context.mjs', - generic: false, - }, - { - readPath: pathPrefix + '/key-management/IKeyStorage.mjs', - writePath: './orchestration/common/key-management/IKeyStorage.mjs', - generic: false, - }, - { - readPath: pathPrefix + '/key-management/FileKeyStorage.mjs', - writePath: './orchestration/common/key-management/FileKeyStorage.mjs', - generic: false, - }, - { - readPath: pathPrefix + '/key-management/DatabaseKeyStorage.mjs', - writePath: './orchestration/common/key-management/DatabaseKeyStorage.mjs', - generic: false, - }, - { - readPath: pathPrefix + '/key-management/KeyManager.mjs', - writePath: './orchestration/common/key-management/KeyManager.mjs', - generic: false, - }, - { - readPath: pathPrefix + '/key-management/encryption.mjs', - writePath: './orchestration/common/key-management/encryption.mjs', - generic: false, - }, - { - readPath: pathPrefix + '/key-management/index.mjs', - writePath: './orchestration/common/key-management/index.mjs', - generic: false, - } - ); - } - - return baseFiles; } } diff --git a/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts b/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts index a82bde10..3dacb06b 100644 --- a/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts +++ b/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts @@ -724,7 +724,6 @@ export const OrchestrationCodeBoilerPlate: any = (node: any) => { `${Orchestrationbp.initialiseKeys.postStatements( node.contractName, states[0], - node.msgSenderParam, ) }`, ], }; @@ -807,36 +806,6 @@ export const OrchestrationCodeBoilerPlate: any = (node: any) => { ], }; - case 'GetInputCommitments': - for ([stateName, stateNode] of Object.entries(node.privateStates)) { - const stateVarIds = stateVariableIds({ - privateStateName: stateName, - stateNode, - }); - if (node.isConstructor) { - continue; - } - if (stateNode.isPartitioned) { - lines.push( - Orchestrationbp.getInputCommitments.postStatements({ - stateName, - contractName: node.contractName, - stateType: 'partitioned', - mappingName: stateNode.mappingName || stateName, - structProperties: stateNode.structProperties, - isSharedSecret: stateNode.isSharedSecret, - stateVarIds, - }), - ); - } - } - return { - statements: [ - `\n// Get input commitments for partitioned states: \n\n`, - ...lines, - ], - }; - case 'MembershipWitness': for ([stateName, stateNode] of Object.entries(node.privateStates)) { const stateVarIds = stateVariableIds({ diff --git a/src/codeGenerators/orchestration/files/toOrchestration.ts b/src/codeGenerators/orchestration/files/toOrchestration.ts index 9b8dd308..4e0281a3 100644 --- a/src/codeGenerators/orchestration/files/toOrchestration.ts +++ b/src/codeGenerators/orchestration/files/toOrchestration.ts @@ -213,18 +213,15 @@ const prepareIntegrationApiServices = (node: any) => { fnboilerplate = fnboilerplate.replace(/_RESPONSE_/g, returnParams + publicReturns); - // Handle SaaS context placeholders based on multi-tenant flag + // Always single-tenant fnboilerplate = fnboilerplate.replace( /SAAS_CONTEXT_HANDLING/g, - node.multiTenant - ? `// Pass context for multi-tenant support (available via saasContextMiddleware) - const context = req.saasContext;` - : `// Single-tenant mode - no context needed`, + `// Single-tenant mode - no context needed`, ); fnboilerplate = fnboilerplate.replace( /SAAS_CONTEXT_PARAM/g, - node.multiTenant ? `context` : `undefined`, + `undefined`, ); // replace function imports at top of file @@ -237,22 +234,19 @@ const prepareIntegrationApiServices = (node: any) => { // add linting and config const preprefix = `/* eslint-disable prettier/prettier, camelcase, prefer-const, no-unused-vars */ \nimport config from 'config';\nimport assert from 'assert';\n`; - // Handle SaaS context in commitments functions + // Handle SaaS context in commitments functions (single-tenant) let commitmentsCode = genericApiServiceFile.commitments(); commitmentsCode = commitmentsCode.replace( /SAAS_CONTEXT_HANDLING/g, - node.multiTenant - ? `// Pass context for multi-tenant support - const context = req.saasContext;` - : `// Single-tenant mode - no context needed`, + `// Single-tenant mode - no context needed`, ); commitmentsCode = commitmentsCode.replace( /SAAS_CONTEXT_PARAM/g, - node.multiTenant ? `context` : `undefined`, + `undefined`, ); commitmentsCode = commitmentsCode.replace( /SAAS_CONTEXT_DIRECT/g, - node.multiTenant ? `context` : `undefined`, + `undefined`, ); commitmentsCode = commitmentsCode.replace( /CONTRACT_NAME/g, @@ -873,8 +867,8 @@ const prepareBackupDataRetriever = (node: any) => { import { getContractInstance, getContractAddress, + getStoredKeys, } from "./common/contract.mjs"; - import { KeyManager } from "./common/key-management/KeyManager.mjs"; import Web3 from "./common/web3.mjs"; import { @@ -917,9 +911,7 @@ const prepareBackupDataRetriever = (node: any) => { const backDataEvent = await instance.getPastEvents('EncryptedBackupData',{fromBlock: 0, toBlock: 'latest'} ); - // Use KeyManager for key retrieval - const keyManager = KeyManager.getInstance(); - const keys = await keyManager.getKeys(context); + const keys = getStoredKeys(); if (!keys) { throw new Error('No keys found. Please register keys first.'); @@ -1091,7 +1083,7 @@ export default function fileGenerator(node: any) { fileContent = fileContent.replace( /SAAS_CONTEXT_PARAM/g, - node.multiTenant ? `context` : `undefined`, + `undefined`, ); return [ { diff --git a/src/codeGenerators/orchestration/nodejs/toOrchestration.ts b/src/codeGenerators/orchestration/nodejs/toOrchestration.ts index 6197d511..6630e2b9 100644 --- a/src/codeGenerators/orchestration/nodejs/toOrchestration.ts +++ b/src/codeGenerators/orchestration/nodejs/toOrchestration.ts @@ -337,7 +337,11 @@ export default function codeGenerator(node: any, options: any = {}): any { case 'SendPublicTransaction': case 'Imports': case 'KeyRegistrationFunction': - return `${OrchestrationCodeBoilerPlate(node).statements.join('')}`; + { + const boilerplate = OrchestrationCodeBoilerPlate(node); + const statements = boilerplate?.statements ?? []; + return `${statements.join('')}`; + } // And if we haven't recognized the node, we'll throw an error. default: throw new TypeError(node.nodeType); diff --git a/src/transformers/toOrchestration.ts b/src/transformers/toOrchestration.ts index 07d0e19f..e247e18c 100644 --- a/src/transformers/toOrchestration.ts +++ b/src/transformers/toOrchestration.ts @@ -25,7 +25,6 @@ export default function toOrchestration(ast: any, options: any) { newCommitmentsRequired: true, nullifiersRequired: true, circuitAST:options.circuitAST, - multiTenant: options.multiTenant }; logger.debug('Transforming the .zol AST to a .mjs AST...'); @@ -72,7 +71,7 @@ export default function toOrchestration(ast: any, options: any) { `Saving backend files to the zApp output directory ${options.outputDirPath}...`, ); // TODO merge this process with above - const zappFilesBP = Orchestrationbp.zappFilesBoilerplate(options.multiTenant); + const zappFilesBP = Orchestrationbp.zappFilesBoilerplate(); if (!(zappFilesBP instanceof Array)) throw new Error('Boilerplate files not read correctly!'); let fileObj: any; // we go through the below process in the codeGenerator for other files @@ -101,29 +100,12 @@ export default function toOrchestration(ast: any, options: any) { : ` `, ); - // Handle SaaS middleware based on multi-tenant flag - file = file.replace( - /SAAS_MIDDLEWARE_IMPORT/g, - options.multiTenant - ? `import { saasContextMiddleware } from './common/middleware/saas-context.mjs';` - : ``, - ); + file = file.replace(/SAAS_MIDDLEWARE_IMPORT/g, ''); - file = file.replace( - /SAAS_MIDDLEWARE_USAGE/g, - options.multiTenant - ? `// Add SaaS context middleware for multi-tenant support -// This middleware parses the x-saas-context header and attaches req.saasContext -// If no header is present, the app operates in single-tenant mode (backward compatible) -app.use(saasContextMiddleware);` - : ``, - ); + file = file.replace(/SAAS_MIDDLEWARE_USAGE/g, ''); // Replace multi-tenant mode configuration - file = file.replace( - /MULTI_TENANT_MODE/g, - options.multiTenant ? `true` : `false`, - ); + file = file.replace(/MULTI_TENANT_MODE/g, `false`); } const dir = pathjs.dirname(filepath); logger.debug(`About to save to ${filepath}...`); diff --git a/src/transformers/visitors/toOrchestrationVisitor.ts b/src/transformers/visitors/toOrchestrationVisitor.ts index 94faa8d8..59b4e5cd 100644 --- a/src/transformers/visitors/toOrchestrationVisitor.ts +++ b/src/transformers/visitors/toOrchestrationVisitor.ts @@ -376,7 +376,6 @@ const visitor = { const newNode = buildNode('File', { fileName: 'test', fileExtension: '.mjs', - multiTenant: state.multiTenant, nodes: [ buildNode('IntegrationTestBoilerplate', { contractName, @@ -394,12 +393,10 @@ const visitor = { newNode = buildNode('File', { fileName: 'api_services', fileExtension: '.mjs', - multiTenant: state.multiTenant, nodes: [ buildNode('IntegrationApiServicesBoilerplate', { contractName, contractImports: state.contractImports, - multiTenant: state.multiTenant, }), ], }); @@ -407,7 +404,6 @@ const visitor = { newNode = buildNode('File', { fileName: 'api_routes', fileExtension: '.mjs', - multiTenant: state.multiTenant, nodes: [ buildNode('IntegrationApiRoutesBoilerplate', { contractName, @@ -419,7 +415,6 @@ const visitor = { newNode = buildNode('File', { fileName: 'BackupDataRetriever', fileExtension: '.mjs', - multiTenant: state.multiTenant, nodes: [ buildNode('BackupDataRetrieverBoilerplate', { contractName, @@ -431,7 +426,6 @@ const visitor = { newNode = buildNode('File', { fileName: 'BackupVariable', fileExtension: '.mjs', - multiTenant: state.multiTenant, nodes: [ buildNode('BackupVariableBoilerplate', { contractName, @@ -444,7 +438,6 @@ const visitor = { newNode = buildNode('File', { fileName: 'encrypted-data-listener', fileExtension: '.mjs', - multiTenant: state.multiTenant, nodes: [ buildNode('IntegrationEncryptedListenerBoilerplate', { contractName, @@ -505,7 +498,6 @@ const visitor = { const newNode = buildNode('File', { fileName: fnName, // the name of this function fileExtension: '.mjs', - multiTenant: state.multiTenant, nodes: [ buildNode('Imports'), buildNode('FunctionDefinition', { name: node.name, contractName, stateMutability: node.stateMutability}), @@ -553,7 +545,6 @@ const visitor = { const newNode = buildNode('File', { fileName: fnName, fileExtension: '.mjs', - multiTenant: state.multiTenant, nodes: [ buildNode('Imports'), buildNode('FunctionDefinition', { name: node.name, contractName, stateMutability: node.stateMutability }), diff --git a/src/types/orchestration-types.ts b/src/types/orchestration-types.ts index c8eca101..30cd3818 100644 --- a/src/types/orchestration-types.ts +++ b/src/types/orchestration-types.ts @@ -9,13 +9,12 @@ import { buildBoilerplateNode } from '../boilerplate/orchestration/javascript/no export default function buildNode(nodeType: string, fields: any = {}): any { switch (nodeType) { case 'File': { - const { fileName, fileExtension = '.mjs', nodes = [], multiTenant } = fields; + const { fileName, fileExtension = '.mjs', nodes = [] } = fields; return { nodeType, fileName, fileExtension, nodes, - multiTenant, }; } case 'Imports': {