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 a1a035b8..0473afa8 100755 --- a/bin/index.mjs +++ b/bin/index.mjs @@ -43,7 +43,8 @@ program .option( '-m, --modify ', 'Ovewrite the file from truezapps folder', - ); + ) + ; program.parse(process.argv); const opts = program.opts(); diff --git a/mintAndApprove.mjs b/mintAndApprove.mjs new file mode 100644 index 00000000..648609df --- /dev/null +++ b/mintAndApprove.mjs @@ -0,0 +1,435 @@ +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 = 2; + 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); + } + + 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..c63aa713 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,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/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/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/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/boilerplate-docker-compose.yml b/src/boilerplate/common/boilerplate-docker-compose.yml index f6ccd0d2..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 @@ -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/commitment-storage.mjs b/src/boilerplate/common/commitment-storage.mjs index da54e583..9d870319 100644 --- a/src/boilerplate/common/commitment-storage.mjs +++ b/src/boilerplate/common/commitment-storage.mjs @@ -9,18 +9,14 @@ 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 { sharedSecretKey } from './number-theory.mjs'; +import { poseidonHash, sharedSecretKey } from './number-theory.mjs'; import { generateProof } from './zokrates.mjs'; -import { hlt } from './hash-lookup.mjs'; -import { registerKey } from './contract.mjs'; +import { getStoredKeys } from './contract.mjs'; const { MONGO_URL, COMMITMENTS_DB, COMMITMENTS_COLLECTION } = config; const { generalise } = gen; -const keyDb = '/app/orchestration/common/db/key.json'; - -export function formatCommitment (commitment) { +export function formatCommitment (commitment, context) { let data try { const nullifierHash = commitment.secretKey @@ -34,19 +30,34 @@ export function formatCommitment (commitment) { 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 + nullifier: commitment.secretKey ? nullifierHash.hex(32) : null, + blockNumber: commitment.blockNumber || null, } - logger.debug(`Storing commitment ${data._id}`) + logger.debug(`Storing commitment ${data._id}${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 } @@ -54,12 +65,24 @@ export function formatCommitment (commitment) { 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) { - const data = formatCommitment(commitment) - return persistCommitment(data) +export async function storeCommitment (commitment, context) { + const data = formatCommitment(commitment, context) + 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 @@ -77,19 +100,28 @@ export async function getCommitmentsById(id) { export async function getCurrentWholeCommitment(id) { 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, - }); + }; + 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) { +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 (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) @@ -138,11 +170,19 @@ export async function getBalance() { return sumOfValues; } -export async function getBalanceByState(name, mappingKey = 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 (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) @@ -174,7 +214,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); } @@ -193,6 +233,7 @@ export async function markNullified(commitmentHash, secretKey = null) { $set: { isNullified: true, nullifier: generalise(nullifier).hex(32), + updatedAt: new Date(), }, }; // updating the original tree @@ -328,6 +369,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', @@ -435,29 +477,20 @@ export async function joinCommitments( .flat(Infinity); // Send transaction to the blockchain: - const txData = await instance.methods + const from = config.web3.options.defaultAccount || (await web3.eth.getAccounts())[0]; + + const sendTxn = await instance.methods .joinCommitments( [oldCommitment_0_nullifier.integer, oldCommitment_1_nullifier.integer], oldCommitment_root.integer, [newCommitment.integer], proof, ) - .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); + .send({ + from, + gas: config.web3.options.defaultGas, + gasPrice: config.web3.options.defaultGasPrice, + }); let tx = await instance.getPastEvents('allEvents', { fromBlock: sendTxn?.blockNumber || 0, @@ -671,39 +704,22 @@ 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 { + const keys = getStoredKeys(); + if (!keys || !keys.secretKey) { + throw new Error('Secret key not found. Please register keys first.'); } - } - 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)); + const recipientPublicKey = _recipientPublicKey || keys.publicKey; + const sharedKey = sharedSecretKey(generalise(keys.secretKey), generalise(recipientPublicKey)); + + logger.info('Shared secret keys retrieved successfully'); - return sharedKey[1]; + 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 a483d2fd..3d514bf8 100644 --- a/src/boilerplate/common/config/default.js +++ b/src/boilerplate/common/config/default.js @@ -1,10 +1,12 @@ module.exports = { - log_level: 'info', + LOG_LEVEL: process.env.LOG_LEVEL, zokrates: { url: process.env.ZOKRATES_URL || 'http://zokrates:80', }, 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', @@ -54,7 +56,7 @@ module.exports = { // contracts to filter: contracts: { // contract name: - CONTRACT_NAME: { + default: { treeHeight: 32, events: { // filter for the following event names: @@ -77,6 +79,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, diff --git a/src/boilerplate/common/contract.mjs b/src/boilerplate/common/contract.mjs index 787e06e5..2f676245 100644 --- a/src/boilerplate/common/contract.mjs +++ b/src/boilerplate/common/contract.mjs @@ -1,19 +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 { - scalarMult, - compressStarlightKey, - poseidonHash, -} from './number-theory.mjs'; - const web3 = Web3.connection(); const { generalise } = GN; -const keyDb = '/app/orchestration/common/db/key.json'; +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`; @@ -79,7 +90,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( @@ -118,41 +135,34 @@ 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), - ); - 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(), + try { + 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, }; - const key = config.web3.key; - const signed = await web3.eth.accounts.signTransaction(txParams, key); - const sendTxn = await web3.eth.sendSignedTransaction(signed.rawTransaction); + persistKeys(keys); + logger.info('Key registered successfully', { + publicKey, + }); + return generalise(publicKey); + } catch (error) { + logger.error('Failed to register key:', error); + throw error; } - const keyJson = { - secretKey: secretKey.integer, - publicKey: publicKey.integer, // not req - }; - fs.writeFileSync(keyDb, JSON.stringify(keyJson, null, 4)); +} - return publicKey; +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 c8f89b52..8c21dac5 100644 --- a/src/boilerplate/common/encrypted-data-listener.mjs +++ b/src/boilerplate/common/encrypted-data-listener.mjs @@ -2,12 +2,10 @@ 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'; -const keyDb = '/app/orchestration/common/db/key.json'; - function decodeCommitmentData(decrypted){ const stateVarId = generalise(decrypted[0]); @@ -19,10 +17,11 @@ function decodeCommitmentData(decrypted){ } export default class EncryptedDataEventListener { - constructor(web3) { + constructor(web3, context) { this.web3 = web3; - this.ethAddress = generalise(config.web3.options.defaultAccount); + this.ethAddress = null; this.contractMetadata = {}; + this.context = context; } async init() { @@ -36,12 +35,24 @@ export default class EncryptedDataEventListener { contractAddr, ); - if (!fs.existsSync(keyDb)) await registerKey(utils.randomHex(31), 'CONTRACT_NAME', true); + 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(); + } - const { secretKey, publicKey } = JSON.parse(fs.readFileSync(keyDb)); + if (!keys) { + throw new Error('Failed to retrieve keys after registration'); + } - this.secretKey = generalise(secretKey); - this.publicKey = generalise(publicKey); + 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', { + ethAddress: this.ethAddress.hex(), + }); } catch (error) { console.error( 'encrypted-data-listener', 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/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/common/services/generic-read-only-api_services.mjs b/src/boilerplate/common/services/generic-read-only-api_services.mjs index 2a441a3e..603c5b5c 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..02d81515 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/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..139251e2 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 [ ` @@ -218,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/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts b/src/boilerplate/orchestration/javascript/nodes/boilerplate-generator.ts index 43fd6aa8..da2232fb 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, @@ -132,6 +141,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, @@ -141,6 +152,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 @@ -163,6 +175,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, @@ -174,6 +188,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, @@ -192,12 +207,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, @@ -235,12 +253,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, @@ -268,11 +289,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': { @@ -283,11 +305,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 adaafc7f..3289e788 100644 --- a/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts +++ b/src/boilerplate/orchestration/javascript/raw/boilerplate-generator.ts @@ -182,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, @@ -218,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( @@ -253,52 +257,11 @@ class BoilerplateGenerator { ${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); @@ -310,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 @@ -322,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 = { @@ -676,9 +634,13 @@ sendTransaction = { burnedOnly, reinitialisedOnly, structProperties, - isConstructor + isConstructor, + 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")) { @@ -691,23 +653,27 @@ sendTransaction = { }`; 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}`}, - preimage: { - \tstateVarId: generalise(${stateName}_stateVarId), - \tvalue: ${value}, - \tsalt: ${stateName}_newSalt, - \tpublicKey: ${stateName}_newOwnerPublicKey, + ${domainParamsCode}preimage: { + 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, isNullified: false, });` + 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)); @@ -716,35 +682,37 @@ sendTransaction = { hash: ${stateName}_2_newCommitment, name: '${mappingName}', mappingKey: ${mappingKey === `` ? `null` : `${mappingKey}`}, - preimage: { - \tstateVarId: generalise(${stateName}_stateVarId), - \tvalue: ${value}, - \tsalt: ${stateName}_2_newSalt, - \tpublicKey: ${stateName}_newOwnerPublicKey, + ${domainParamsCode}preimage: { + 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, isNullified: false, - });`+ 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 try { \nawait storeCommitment({ hash: ${stateName}_newCommitment, name: '${mappingName}', mappingKey: ${mappingKey === `` ? `null` : `${mappingKey}`}, - preimage: { - \tstateVarId: generalise(${stateName}_stateVarId), - \tvalue: ${value}, - \tsalt: ${stateName}_newSalt, - \tpublicKey: ${stateName}_newOwnerPublicKey, + ${domainParamsCode}preimage: { + 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, isNullified: false, @@ -752,14 +720,13 @@ sendTransaction = { } 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 ` @@ -773,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); } diff --git a/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts b/src/boilerplate/orchestration/javascript/raw/toOrchestration.ts index 056a9868..3dacb06b 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(config.web3.options.defaultAccount); // emulates msg.sender`, + `\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; @@ -166,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; @@ -176,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: @@ -333,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')) { @@ -350,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 @@ -374,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) @@ -501,9 +581,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;`); @@ -573,7 +650,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 +662,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 +673,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}`, @@ -677,6 +754,7 @@ export const OrchestrationCodeBoilerPlate: any = (node: any) => { structProperties: stateNode.structProperties, isConstructor: node.isConstructor, reinitialisedOnly: false, + perParameters: stateNode.perParameters, })); break; @@ -695,6 +773,7 @@ export const OrchestrationCodeBoilerPlate: any = (node: any) => { structProperties: stateNode.structProperties, isConstructor: node.isConstructor, reinitialisedOnly: stateNode.reinitialisedOnly, + perParameters: stateNode.perParameters, })); break; @@ -715,6 +794,7 @@ export const OrchestrationCodeBoilerPlate: any = (node: any) => { reinitialisedOnly: stateNode.reinitialisedOnly, structProperties: stateNode.structProperties, isConstructor: node.isConstructor, + perParameters: stateNode.perParameters, })); } } @@ -726,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({ @@ -1054,18 +1104,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'}); @@ -1156,17 +1208,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/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 7a3ac14c..4e0281a3 100644 --- a/src/codeGenerators/orchestration/files/toOrchestration.ts +++ b/src/codeGenerators/orchestration/files/toOrchestration.ts @@ -213,6 +213,17 @@ const prepareIntegrationApiServices = (node: any) => { fnboilerplate = fnboilerplate.replace(/_RESPONSE_/g, returnParams + publicReturns); + // Always single-tenant + fnboilerplate = fnboilerplate.replace( + /SAAS_CONTEXT_HANDLING/g, + `// Single-tenant mode - no context needed`, + ); + + fnboilerplate = fnboilerplate.replace( + /SAAS_CONTEXT_PARAM/g, + `undefined`, + ); + // 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 +233,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 (single-tenant) + let commitmentsCode = genericApiServiceFile.commitments(); + commitmentsCode = commitmentsCode.replace( + /SAAS_CONTEXT_HANDLING/g, + `// Single-tenant mode - no context needed`, + ); + commitmentsCode = commitmentsCode.replace( + /SAAS_CONTEXT_PARAM/g, + `undefined`, + ); + commitmentsCode = commitmentsCode.replace( + /SAAS_CONTEXT_DIRECT/g, + `undefined`, + ); + commitmentsCode = commitmentsCode.replace( + /CONTRACT_NAME/g, + node.contractName, + ); + outputApiServiceFile = `${preprefix}\n${outputApiServiceFile}}\n ${commitmentsCode}\n`; return outputApiServiceFile; }; const prepareIntegrationApiRoutes = (node: any) => { @@ -315,6 +345,7 @@ node.stateVariables?.forEach( publicKey: self.publicKey, }, secretKey: self.secretKey, + blockNumber: Number(tx.blockNumber), isNullified: false, }); console.log('Added commitment', newCommitment.hex(32)); @@ -364,6 +395,7 @@ node.stateVariables?.forEach( publicKey: self.publicKey, }, secretKey: self.secretKey, + blockNumber: Number(tx.blockNumber), isNullified: false, }); console.log('Added commitment', newCommitment.hex(32)); @@ -420,18 +452,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), @@ -492,56 +524,60 @@ const prepareMigrationsFile = (file: localFile, node: any) => { ) \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, 'ERC1155', "/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 @@ -798,6 +834,7 @@ const prepareBackupVariable = (node: any) => { publicKey: kp.publicKey, }, secretKey: kp.secretKey, + blockNumber: Number(tx.blockNumber), isNullified: isNullified, }); } @@ -830,6 +867,7 @@ const prepareBackupDataRetriever = (node: any) => { import { getContractInstance, getContractAddress, + getStoredKeys, } from "./common/contract.mjs"; import Web3 from "./common/web3.mjs"; @@ -849,7 +887,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); @@ -873,11 +911,11 @@ 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); - }) - ); + const keys = getStoredKeys(); + + 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); @@ -1008,6 +1046,7 @@ const prepareBackupDataRetriever = (node: any) => { publicKey: kp.publicKey, }, secretKey: kp.secretKey, + blockNumber: Number(tx.blockNumber), isNullified: isNullified, }); } @@ -1040,13 +1079,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, + `undefined`, + ); return [ { filepath: path.join( `./orchestration`, `${node.fileName}${node.fileExtension}`, ), - file: node.nodes.map(codeGenerator).join(''), + file: fileContent, }, ]; // case 'ImportStatementList': 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/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/toOrchestration.ts b/src/transformers/toOrchestration.ts index 4e4df63e..e247e18c 100644 --- a/src/transformers/toOrchestration.ts +++ b/src/transformers/toOrchestration.ts @@ -24,7 +24,7 @@ export default function toOrchestration(ast: any, options: any) { snarkVerificationRequired: true, newCommitmentsRequired: true, nullifiersRequired: true, - circuitAST:options.circuitAST + circuitAST:options.circuitAST, }; logger.debug('Transforming the .zol AST to a .mjs AST...'); @@ -99,6 +99,13 @@ export default function toOrchestration(ast: any, options: any) { await eventListener.start()` : ` `, ); + + file = file.replace(/SAAS_MIDDLEWARE_IMPORT/g, ''); + + file = file.replace(/SAAS_MIDDLEWARE_USAGE/g, ''); + + // Replace multi-tenant mode configuration + 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/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/common.ts b/src/transformers/visitors/common.ts index df32c6c7..e6dbae2c 100644 --- a/src/transformers/visitors/common.ts +++ b/src/transformers/visitors/common.ts @@ -22,11 +22,15 @@ 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'); + // 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.getInputCommitmentsNode = buildNode('GetInputCommitments', { 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/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 12a44985..59b4e5cd 100644 --- a/src/transformers/visitors/toOrchestrationVisitor.ts +++ b/src/transformers/visitors/toOrchestrationVisitor.ts @@ -990,19 +990,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/src/traverse/Binding.ts b/src/traverse/Binding.ts index 536c3c56..bbfb7d6b 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' @@ -599,17 +614,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.`, @@ -654,4 +659,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/Indicator.ts b/src/traverse/Indicator.ts index db20579d..009fc13f 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(); @@ -145,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/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.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); + } } 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..727e7119 --- /dev/null +++ b/test/contracts/user-friendly-tests/NFT_Escrow_DomainParams.zol @@ -0,0 +1,78 @@ +// 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; + +import "./Escrow-imports/IERC721.sol"; + +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; + + // 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 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"); + + // 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; + } + + 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); + + // 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); + } +} + diff --git a/test/contracts/user-friendly-tests/SupplyChainPoC.zol b/test/contracts/user-friendly-tests/SupplyChainPoC.zol new file mode 100644 index 00000000..77d97e89 --- /dev/null +++ b/test/contracts/user-friendly-tests/SupplyChainPoC.zol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: CC0 + +pragma solidity ^0.8.0; + +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 + + // 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"); + minter[tokenId] = msg.sender; + 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"); + minter[tokenId] = minter[tokenId]; + 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); + } + + 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); + } +}