diff --git a/examples/sign_primitive.js b/examples/sign_primitive.js index e939763..ae6ef37 100644 --- a/examples/sign_primitive.js +++ b/examples/sign_primitive.js @@ -7,19 +7,20 @@ const { ethers } = require("hardhat"); * yarn hardhat node * * In another terminal, deploy the contracts: - * yarn hardhat ignition deploy ignition/modules/0_development.js --network localhost + * yarn hardhat ignition deploy ignition/modules/messenger.js --network localhost * - * The command above should procude output like this: + * The command above should produce output like this: * * Deployed Addresses * - * BookingTokenProxyModule#BookingToken - 0x5FbDB2315678afecb367f032d93F642f64180aa3 - * CMAccountManagerModule#CMAccount - 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 - * ManagerProxyModule#CMAccountManager - 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 - * BookingTokenProxyModule#ERC1967Proxy - 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 - * ManagerProxyModule#ERC1967Proxy - 0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9 - * CMAccountManagerModule#BookingToken - 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 - * CMAccountManagerModule#CMAccountManager - 0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9 + * CaminoMessengerModule#BookingToken - 0x5FbDB2315678afecb367f032d93F642f64180aa3 + * CaminoMessengerModule#BookingTokenOperator - 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 + * CaminoMessengerModule#CMAccountManager - 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 + * CaminoMessengerModule#CMAccount - 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 + * CaminoMessengerModule#ManagerERC1967Proxy - 0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9 + * CaminoMessengerModule#ManagerProxy - 0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9 + * CaminoMessengerModule#BookingTokenERC1967Proxy - 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707 + * CaminoMessengerModule#BookingTokenProxy - 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707 * * Now you are ready to run this script: * yarn hardhat run examples/sign_primitive.js --network localhost @@ -30,49 +31,92 @@ const { ethers } = require("hardhat"); * yarn run v1.22.19 * $ /hgst/work/github.com/chain4travel/camino-messenger-contracts/node_modules/.bin/hardhat run examples/sign_primitive.js --network localhost * + * ----------------------- Prepare CMAccount ------------------------------------------------ + * šŸ”‘ Signer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 + * * Getting CMAccountManager and creating a CMAccount... + * * Created CMAccount at address: 0x8271373aC5cD66E9e7dC752c0e39da5d12988Ec6 + * Registering address as a bot (Cheque signer)... + * Done! + * * ----------------------- creating cheque and signatures (off-chain) ----------------------- - * Signer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 * Cheque: { - * fromCMAccount: '0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65', - * toCMAccount: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC', + * fromCMAccount: '0x8271373aC5cD66E9e7dC752c0e39da5d12988Ec6', + * toCMAccount: '0x8271373aC5cD66E9e7dC752c0e39da5d12988Ec6', * toBot: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', - * counter: 1, + * counter: 1n, * amount: 1000000000000000000n, - * createdAt: 1722565705, - * expiresAt: 1722565825 + * createdAt: 1726063332, + * expiresAt: 1726063452 * } * - * ----------------------- Calculated Values ----------------------- + * ----------------------- Calculated Values (off-chain) ------------------------------------ * MESSENGER_CHEQUE_TYPEHASH: 0x87b38f131334165ac2b361f08966c9fcff3a953fa7d9d9c2861b7f0b50445bcb * DOMAIN_TYPEHASH: 0xc2f8787176b8ac6bf7215b4adcc1e069bf4ab82d9ab1df05a57a91d425935b6e * DOMAIN_SEPARATOR: 0x792acc3adab7297918d2cdaeb59ac5f091943a65aba244c580164ec2ec307451 - * Cheque Hash: 0x91add1c1535d9bc4cc3cd2d7fbef1b30e1b2d8dead0087d7c568c8dca1b63430 - * Typed Data Hash: 0x208cfb408b5d2498fb4fa978535ab394be394918e3c17fd9ee9f3260b84056cc - * Signature: 0x10576abbb5e8c2a813fde2809c371cf70d682a79c8003e4c677d3c25187022410f5587dee3c798a9978adadf31bca336ffd158910b6df12ca8bba4fb30699caf1b - * Recovered Address: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (Should be same as the 'Signer') + * Cheque Hash: 0xaf8cf3a3b4742c5a8ab6ec5d5d14e48237bde9d0111af466f1babbf79d9fe574 + * Typed Data Hash: 0x3a6686e6234a0effbdbb5d9a754dee7e57b2f89f7d3a600b9abd9078a0136cd0 + * Signature: 0xaef61cbfd3dbd39f1184a425113707cd3ac2a732b5e379d19788e9d174aca8c638a56ecad452bbfedd1a2ccd0c79ce0f942d05ecd33c87af25e239abd33d2ad91c + * šŸ”‘ Recovered Signer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 <== Should be same as the 'Signer' * - * ----------------------- Trying with a CMAccount (on-chain) ----------------------- - * * Getting CMAccountManager and creating a CMAccount... - * * Created CMAccount at address: 0x856e4424f806D16E8CBC702B3c0F2ede5468eae5 - * * Calling CMAccount.recoverSigner(cheque, signature)... - * Recovered (on-chain): 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (Should be same as the 'Signer') - * Done in 1.66s. + * ----------------------- Trying with a CMAccount (on-chain) ------------------------------- + * * Calling CMAccount.verifyCheque... + * šŸ”‘ Recovered (on-chain): 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 <== Should be same as the off-chain 'Signer' above + * Done in 1.94s. */ async function main() { - console.log("\n----------------------- creating cheque and signatures (off-chain) -----------------------"); - + console.log("\n----------------------- Prepare CMAccount ------------------------------------------------"); // First signer from hardhat local: // address: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 const [wallet] = await ethers.getSigners(); - console.log("Signer:", wallet.address); + console.log("šŸ”‘ Signer:", wallet.address); + // First we need to create a CMAccount. So, we get the contract at the address + // below. Which is always the same address on a fresh hardhat local network + // because the transaction creating it is the same. (same nonce, same owner + // etc..) + console.log("* Getting CMAccountManager and creating a CMAccount..."); + const cmAccountManagerAddress = "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9"; + const manager = await ethers.getContractAt("CMAccountManager", cmAccountManagerAddress); + + // Create a CMAccount + const tx = await manager.createCMAccount(wallet.address, wallet.address, { + value: ethers.parseEther("200"), + }); + + // Get the tx receipt + const receipt = await tx.wait(); + + // Parse event to get the CMAccount address + const event = receipt.logs.find((log) => { + try { + return manager.interface.parseLog(log).name === "CMAccountCreated"; + } catch (e) { + return false; + } + }); + + const parsedEvent = manager.interface.parseLog(event); + const cmAccountAddress = parsedEvent.args.account; + + console.log("* Created CMAccount at address:", cmAccountAddress); + + // Get newly created CM Account contract using the parsed address + const cmAccount = await ethers.getContractAt("CMAccount", cmAccountAddress); + + console.log("Registering address as a bot (Cheque signer)..."); - // Create a sample cheque, addresses used here are just dummy account addresses - // from hardhat local node + // Register address as a bot + const tx2 = await cmAccount.addMessengerBot(wallet.address, 0); + const receipt2 = await tx2.wait(); + console.log("Done!"); + + console.log("\n----------------------- creating cheque and signatures (off-chain) -----------------------"); + + // Create a sample cheque, CM account addresses used here are same as above. Bot address is just dummy address. const cheque = { - fromCMAccount: "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65", - toCMAccount: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + fromCMAccount: cmAccountAddress, + toCMAccount: cmAccountAddress, toBot: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", counter: 1n, amount: ethers.parseUnits("1.0", "ether"), // 1 ETH @@ -115,7 +159,8 @@ async function main() { ); // Print out what we've calculated so far - console.log("\n----------------------- Calculated Values (off-chain) -----------------------"); + console.log("\n----------------------- Calculated Values (off-chain) ------------------------------------"); + console.log("ā›“ļø Chain ID:", chainId); console.log("MESSENGER_CHEQUE_TYPEHASH:", MESSENGER_CHEQUE_TYPEHASH); console.log("DOMAIN_TYPEHASH:", DOMAIN_TYPEHASH); console.log("DOMAIN_SEPARATOR:", DOMAIN_SEPARATOR); @@ -167,67 +212,26 @@ async function main() { // Recover the address from the digest and signature const recoveredAddress = ethers.recoverAddress(ethers.getBytes(typedDataHash), signature); - console.log("Recovered Address:", recoveredAddress, "(Should be same as the 'Signer')"); + console.log("\nšŸ”‘ Recovered Signer (off-chain):", recoveredAddress, "<== Should be same as the 'Signer'"); - // Try to recover the same address from the CMAccount's `recoverSigner(cheque, - // signature)` function. - console.log("\n----------------------- Trying with a CMAccount (on-chain) -----------------------"); + // Try to recover the same address from the CMAccount's `verifyCheque(cheque ..., signature)` function. + console.log("\n----------------------- Trying with a CMAccount (on-chain) -------------------------------"); - // First we need to create a CMAccount. So, we get the contract at the address - // below. Which is always the same address on a fresh hardhat local network - // because the transaction creating it is the same. (same nonce, same owner - // etc..) - console.log("* Getting CMAccountManager and creating a CMAccount..."); - const cmAccountManagerAddress = "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707"; - const manager = await ethers.getContractAt("CMAccountManager", cmAccountManagerAddress); + console.log("* Calling CMAccount.verifyCheque..."); - // Create a CMAccount - const tx = await manager.createCMAccount(wallet.address, wallet.address, { - value: ethers.parseEther("200"), - }); - - // Get the tx receipt - const receipt = await tx.wait(); - - // Parse event to get the CMAccount address - const event = receipt.logs.find((log) => { - try { - return manager.interface.parseLog(log).name === "CMAccountCreated"; - } catch (e) { - return false; - } - }); - - const parsedEvent = manager.interface.parseLog(event); - const cmAccountAddress = parsedEvent.args.account; - - console.log("* Created CMAccount at address:", cmAccountAddress); - - // Get newly created CM Account contract using the parsed address - const cmAccount = await ethers.getContractAt("CMAccount", cmAccountAddress); - - console.log("* Calling CMAccount.recoverSigner..."); - - // Call `recoverSigner` to verify if the recovered signer is the same as our + // Call `verifyCheque` to verify if the recovered signer is the same as our // original signer's address - - // Update: We changed the recoverSigner function to internal. So it's not - // visible anymore on the CMAccount contract. If you really want to test it - // change the visibility to public, redeploy the contracts and enable the - // following lines below. Then the run this script again. - console.log("**** Visibility of recoverSigner is switched to internal. Read the comments in the file. ****"); - - // const res = await cmAccount.recoverSigner( - // cheque.fromCMAccount, - // cheque.toCMAccount, - // cheque.toBot, - // cheque.counter, - // cheque.amount, - // cheque.createdAt, - // cheque.expiresAt, - // signature.serialized, - // ); - // console.log("Recovered (on-chain):", res, "(Should be same as the 'Signer')"); + const res = await cmAccount.verifyCheque( + cheque.fromCMAccount, + cheque.toCMAccount, + cheque.toBot, + cheque.counter, + cheque.amount, + cheque.createdAt, + cheque.expiresAt, + signature.serialized, + ); + console.log("\nšŸ”‘ Recovered Signer (on-chain):", res[0], "<== Should be same as the off-chain 'Signer' above\n"); } main().catch(console.error); diff --git a/tasks/account.js b/tasks/account.js index 728cd75..d7f4c0f 100644 --- a/tasks/account.js +++ b/tasks/account.js @@ -74,19 +74,19 @@ function handleTransactionError(error, contract) { } ACCOUNT_SCOPE.task("role:grant", "Grant role") - .addParam("privateKey", "Private key to use") + .addOptionalParam("privateKey", "Private key to use", process.env.CMACCOUNT_PK) .addParam("role", "Role to grant. Ex: SERVICE_ADMIN_ROLE") .addParam("address", "Address to grant role to") - .addParam("cmAccount", "CMAccount address") + .addOptionalParam("cmAccount", "CMAccount address", process.env.CMACCOUNT_ADDRESS) .setAction(async (taskArgs, hre) => { await handleRoles(taskArgs, hre, "grantRole"); }); ACCOUNT_SCOPE.task("role:revoke", "Revoke role") - .addParam("privateKey", "Private key to use") + .addOptionalParam("privateKey", "Private key to use", process.env.CMACCOUNT_PK) .addParam("role", "Role to grant. Ex: SERVICE_ADMIN_ROLE") .addParam("address", "Address to revoke role to") - .addParam("cmAccount", "CMAccount address") + .addOptionalParam("cmAccount", "CMAccount address", process.env.CMACCOUNT_ADDRESS) .setAction(async (taskArgs, hre) => { await handleRoles(taskArgs, hre, "revokeRole"); }); @@ -94,7 +94,7 @@ ACCOUNT_SCOPE.task("role:revoke", "Revoke role") ACCOUNT_SCOPE.task("role:has", "Check if address has role") .addParam("role", "Role to check. Ex: SERVICE_ADMIN_ROLE") .addParam("address", "Address to check") - .addParam("cmAccount", "CMAccount address") + .addOptionalParam("cmAccount", "CMAccount address", process.env.CMACCOUNT_ADDRESS) .setAction(async (taskArgs, hre) => { const cmAccount = await getCMAccount(taskArgs.cmAccount); console.log("CMAccount:", taskArgs.cmAccount); @@ -113,7 +113,7 @@ ACCOUNT_SCOPE.task("role:has", "Check if address has role") ACCOUNT_SCOPE.task("role:members", "List role members") .addParam("role", "Role to list. Ex: SERVICE_ADMIN_ROLE") - .addParam("cmAccount", "CMAccount address") + .addOptionalParam("cmAccount", "CMAccount address", process.env.CMACCOUNT_ADDRESS) .setAction(async (taskArgs, hre) => { const cmAccount = await getCMAccount(taskArgs.cmAccount); console.log("CMAccount:", taskArgs.cmAccount); @@ -137,7 +137,7 @@ ACCOUNT_SCOPE.task("role:members", "List role members") }); ACCOUNT_SCOPE.task("create", "Create CMAccount") - .addParam("privateKey", "Private key to use") + .addOptionalParam("privateKey", "Private key to use", process.env.CMACCOUNT_PK) .setAction(async (taskArgs, hre) => { const manager = await getManager(hre); try { @@ -171,9 +171,89 @@ ACCOUNT_SCOPE.task("create", "Create CMAccount") } }); +ACCOUNT_SCOPE.task("bot:add", "Add bot to the CMAccount") + .addOptionalParam("privateKey", "Private key to use", process.env.CMACCOUNT_PK) + .addOptionalParam("cmAccount", "CMAccount address", process.env.CMACCOUNT_ADDRESS) + .addParam("bot", "Bot address") + .addOptionalParam( + "gasMoney", + "Gas money in CAM. This amount will be transferred from the CMAccount to the bot address (Ex: 1 or 0.1)", + "0", + types.string, + ) + .setAction(async (taskArgs, hre) => { + const cmAccount = await getCMAccount(taskArgs.cmAccount); + console.log("CMAccount:", taskArgs.cmAccount); + console.log("Bot:", taskArgs.bot); + console.log( + "Gas:", + taskArgs.gasMoney, + "(This amount will be transferred from the CMAccount to the bot address)", + ); + + try { + const signer = new ethers.Wallet(taskArgs.privateKey, ethers.provider); + console.log("Adding bot to CMAccount..."); + console.log("Signer:", signer.address); + + const gasMoney = ethers.parseEther(taskArgs.gasMoney); + + const tx = await cmAccount.connect(signer).addMessengerBot(taskArgs.bot, gasMoney); + const receipt = await tx.wait(); + console.log("Tx:", receipt.hash); + } catch (error) { + handleTransactionError(error, cmAccount); + } + }); + +ACCOUNT_SCOPE.task("bot:remove", "Remove bot from the CMAccount") + .addOptionalParam("privateKey", "Private key to use", process.env.CMACCOUNT_PK) + .addOptionalParam("cmAccount", "CMAccount address", process.env.CMACCOUNT_ADDRESS) + .addParam("bot", "Bot address") + .setAction(async (taskArgs, hre) => { + const cmAccount = await getCMAccount(taskArgs.cmAccount); + console.log("CMAccount:", taskArgs.cmAccount); + console.log("Bot:", taskArgs.bot); + + try { + const signer = new ethers.Wallet(taskArgs.privateKey, ethers.provider); + console.log("Removing bot from CMAccount..."); + console.log("Signer:", signer.address); + + const tx = await cmAccount.connect(signer).removeMessengerBot(taskArgs.bot); + const receipt = await tx.wait(); + console.log("Tx:", receipt.hash); + } catch (error) { + handleTransactionError(error, cmAccount); + } + }); + +ACCOUNT_SCOPE.task("bot:list", "List all bots from CMAccount") + .addOptionalParam("cmAccount", "CMAccount address", process.env.CMACCOUNT_ADDRESS) + .setAction(async (taskArgs, hre) => { + console.log("CMAccount:", taskArgs.cmAccount, "\n"); + + console.log("šŸ“¢ A bot is an address that has been granted some special roles on the CMAccount."); + + const role1 = "CHEQUE_OPERATOR_ROLE"; + console.log("\nšŸ¤–", role1, "(Can sign cheques that are valid for the CMAccount)"); + console.log("======================================================"); + await hre.run({ scope: "account", task: "role:members" }, { role: role1, cmAccount: taskArgs.cmAccount }); + + const role2 = "BOOKING_OPERATOR_ROLE"; + console.log("\nšŸ¤–", role2, "(Can mint and buy Booking Tokens for the CMAccount)"); + console.log("======================================================"); + await hre.run({ scope: "account", task: "role:members" }, { role: role2, cmAccount: taskArgs.cmAccount }); + + const role3 = "GAS_WITHDRAWER_ROLE"; + console.log("\nšŸ¤–", role3, "(Can withdraw gas from the CMAccount)"); + console.log("======================================================"); + await hre.run({ scope: "account", task: "role:members" }, { role: role3, cmAccount: taskArgs.cmAccount }); + }); + ACCOUNT_SCOPE.task("wanted:add", "Add wanted service to CMAccount") - .addParam("privateKey", "Private key to use") - .addParam("cmAccount", "CMAccount address") + .addOptionalParam("privateKey", "Private key to use", process.env.CMACCOUNT_PK) + .addOptionalParam("cmAccount", "CMAccount address", process.env.CMACCOUNT_ADDRESS) .addParam("serviceName", "Name of service to add") .setAction(async (taskArgs, hre) => { const cmAccount = await getCMAccount(taskArgs.cmAccount); @@ -195,8 +275,8 @@ ACCOUNT_SCOPE.task("wanted:add", "Add wanted service to CMAccount") }); ACCOUNT_SCOPE.task("wanted:remove", "Remove wanted service from CMAccount") - .addParam("privateKey", "Private key to use") - .addParam("cmAccount", "CMAccount address") + .addOptionalParam("privateKey", "Private key to use", process.env.CMACCOUNT_PK) + .addOptionalParam("cmAccount", "CMAccount address", process.env.CMACCOUNT_ADDRESS) .addParam("serviceName", "Name of service to remove") .setAction(async (taskArgs, hre) => { const cmAccount = await getCMAccount(taskArgs.cmAccount); @@ -218,7 +298,7 @@ ACCOUNT_SCOPE.task("wanted:remove", "Remove wanted service from CMAccount") }); ACCOUNT_SCOPE.task("wanted:list", "List all wanted service from CMAccount") - .addParam("cmAccount", "CMAccount address") + .addOptionalParam("cmAccount", "CMAccount address", process.env.CMACCOUNT_ADDRESS) .setAction(async (taskArgs, hre) => { const cmAccount = await getCMAccount(taskArgs.cmAccount); console.log("CMAccount:", taskArgs.cmAccount); @@ -235,8 +315,8 @@ ACCOUNT_SCOPE.task("wanted:list", "List all wanted service from CMAccount") }); ACCOUNT_SCOPE.task("service:add", "Add supported service to CMAccount") - .addParam("privateKey", "Private key to use") - .addParam("cmAccount", "CMAccount address") + .addOptionalParam("privateKey", "Private key to use", process.env.CMACCOUNT_PK) + .addOptionalParam("cmAccount", "CMAccount address", process.env.CMACCOUNT_ADDRESS) .addParam("serviceName", "Name of service to add") .addParam("fee", "Fee of the service in aCAM (wei in ETH terminology)") .addParam("restrictedRate", "Restricted rate of the service", false, types.boolean) @@ -268,8 +348,8 @@ ACCOUNT_SCOPE.task("service:add", "Add supported service to CMAccount") }); ACCOUNT_SCOPE.task("service:remove", "Remove wanted service from CMAccount") - .addParam("privateKey", "Private key to use") - .addParam("cmAccount", "CMAccount address") + .addOptionalParam("privateKey", "Private key to use", process.env.CMACCOUNT_PK) + .addOptionalParam("cmAccount", "CMAccount address", process.env.CMACCOUNT_ADDRESS) .addParam("serviceName", "Name of service to remove") .setAction(async (taskArgs, hre) => { const cmAccount = await getCMAccount(taskArgs.cmAccount); @@ -291,7 +371,7 @@ ACCOUNT_SCOPE.task("service:remove", "Remove wanted service from CMAccount") }); ACCOUNT_SCOPE.task("service:list", "List supported services from CMAccount") - .addParam("cmAccount", "CMAccount address") + .addOptionalParam("cmAccount", "CMAccount address", process.env.CMACCOUNT_ADDRESS) .setAction(async (taskArgs, hre) => { const cmAccount = await getCMAccount(taskArgs.cmAccount); console.log("CMAccount:", taskArgs.cmAccount); @@ -324,9 +404,9 @@ ACCOUNT_SCOPE.task("service:list", "List supported services from CMAccount") } }); -ACCOUNT_SCOPE.task("upgrade", "Upgrade CMAccount implementation") - .addParam("privateKey", "Private key to use") - .addParam("cmAccount", "CMAccount address") +ACCOUNT_SCOPE.task("upgrade", "Upgrade CMAccount to latest implementation") + .addOptionalParam("privateKey", "Private key to use", process.env.CMACCOUNT_PK) + .addOptionalParam("cmAccount", "CMAccount address", process.env.CMACCOUNT_ADDRESS) .setAction(async (taskArgs, hre) => { const cmAccount = await getCMAccount(taskArgs.cmAccount); console.log("CMAccount:", taskArgs.cmAccount);