diff --git a/README.md b/README.md index 9a4f495..fa251d5 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ The steps below are a quick start if you have already set up your [develoment en ### Set up the Rust toolchain You need the Rust toolchain to develop smart contracts. ```bash -$ cd contract $ rustup install $(cat rust-toolchain) $ rustup target add --toolchain $(cat rust-toolchain) wasm32-unknown-unknown ``` @@ -17,24 +16,45 @@ $ rustup target add --toolchain $(cat rust-toolchain) wasm32-unknown-unknown ### Compile Account Code (Smart Contracts) Create a WASM file that will be used by the JS client. ```bash +$ cd contract $ cargo build --release ``` -### Install the JS packages -```bash -$ cd client -$ npm install -``` - ## Prepare the local `nctl` network 1. Set up [nctl](https://github.com/CasperLabs/casper-node/tree/master/utils/nctl) -2. Update `client/src/utils.js`: - - Set `baseKeyPath` to your nctl faucet key. -## Run -Run the client code. -```bash -$ cd client -$ node src/keys-manager.js -$ node src/keys-manager-set-all.js +## Running prepared client-scenarios + +### Installation + +Just run `npm i` in `./client`. + +### Env configuration + +Environment variables needs to be set in `.env` file in `./client`. + +``` +BASE_KEY_PATH=... # absolute path to keys directory +NODE_URL=... # optional, defaults to standard NCTL address http://localhost:40101/rpc +WASM_PATH=... # optional, defaults to ../contract/target/wasm32-unknown-unknown/release/keys-manager.wasm +NETWORK_NAME=... # optional, defaults to casper-net-1 +FUND_AMOUNT=10000000000000 # defaults to 10000000000000 = 10000CSPR +PAYMENT_AMOUNT=100000000000 # defaults to 100000000000 = 100CSPR +TRANSFER_AMOUNT=2500000000 # defaults to 2500000000 = 2.5CSPR ``` + +You can also run run both scripts providing custom `.env` path by running + +`npm run start:atomic dotenv_config_path=./example-env-file` + +### Set-all + +`npm run start:all` + +### Step-by-step + +`npm run start:atomic` + +### Interactive mode + +To run a script in interactive mode just add `interactive` to the above commands. diff --git a/client/README.md b/client/README.md deleted file mode 100644 index 7d388fc..0000000 --- a/client/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Key manager - -## Running prepared scenarios - -### Set-all - -`npm run start:all` - -### Step-by-step - -`npm run start:atomic` - -### Env configuration - -Environment variables needs to be set in `.env` file. - -``` -BASE_KEY_PATH=... # absolute path to keys directory -AMOUNT=2500000000 # amount used in the examples -NODE_URL=... # optional -WASM_PATH=... # optional -NETWORK_NAME=... # optional -``` - -You can also run run both scripts providing custom `.env` path by running - -`npm run start:atomic dotenv_config_path=./example-env-file` diff --git a/client/src/key-manager.js b/client/src/key-manager.js index b364f1e..f92e5e5 100644 --- a/client/src/key-manager.js +++ b/client/src/key-manager.js @@ -13,34 +13,31 @@ const { CLValueBuilder, } = require('casper-js-sdk'); -const { getAccountFromKeyPair, randomSeed, toAccountHashString, sleep } = require('./utils'); +const { getAccountFromKeyPair, randomSeed, toAccountHashString, sleep, pauseAndWaitForKeyPress } = require('./utils'); -const FUND_AMOUNT = 10000000000000; -const PAYMENT_AMOUNT = 100000000000; +const FUND_AMOUNT = process.env.FUND_AMOUNT || 10000000000000; +const PAYMENT_AMOUNT = process.env.PAYMENT_AMOUNT || 100000000000; const NODE_URL = process.env.NODE_URL || 'http://localhost:40101/rpc'; const WASM_PATH = process.env.WASM_PATH || '../contract/target/wasm32-unknown-unknown/release/keys-manager.wasm'; const NETWORK_NAME = process.env.NETWORK_NAME || 'casper-net-1'; const BASE_KEY_PATH = process.env.BASE_KEY_PATH; +// Get a faucet account from provided path const faucetAccount = getAccountFromKeyPair(BASE_KEY_PATH); -// Create a client connect to Casper Node -let client = new CasperClient(NODE_URL); +// Create a client connected to Casper Node +const client = new CasperClient(NODE_URL); -async function sendDeploy(deploy, signingKeys) { - for(let key of signingKeys){ - console.log(`Signed by: ${toAccountHashString(key.publicKey)}`); - deploy = client.signDeploy(deploy, key); - } - let deployHash = await client.putDeploy(deploy); - await printDeploy(deployHash); -} +// +// Helper methods +// +// Helper method for geting a deploy in a defined time period (30s) async function getDeploy(deployHash) { let i = 300; while (i != 0) { - let [deploy, raw] = await client.getDeploy(deployHash); + const [deploy, raw] = await client.getDeploy(deployHash); if (raw.execution_results.length !== 0){ if (raw.execution_results[0].result.Success) { return deploy; @@ -56,47 +53,129 @@ async function getDeploy(deployHash) { throw Error('Timeout after ' + i + 's. Something\'s wrong'); } +// Helper method for getting the current state of the account async function getAccount(publicKey) { - let c = new CasperServiceByJsonRPC(NODE_URL); - let stateRootHash = (await c.getLatestBlockInfo()).block.header.state_root_hash; - let account = await c.getBlockState( + const c = new CasperServiceByJsonRPC(NODE_URL); + const stateRootHash = (await c.getLatestBlockInfo()).block.header.state_root_hash; + const account = await c.getBlockState( stateRootHash, - 'account-hash-' + toAccountHashString(publicKey), + publicKey.toAccountHashStr(), [] ).then(res => res.Account); return account; } +// Helper method for sending deploy and displaying signing keys +async function sendDeploy(deploy, signingKeys) { + for(let key of signingKeys){ + console.log(`Signed by: ${key.publicKey.toAccountHashStr()}`); + deploy = client.signDeploy(deploy, key); + } + const deployHash = await client.putDeploy(deploy); + await printDeploy(deployHash); +} + +// Helper method to create a new hierarchical deterministic wallet +function randomMasterKey() { + const seed = new Uint8Array(randomSeed()); + return client.newHdWallet(seed); +} + +// Helper method for printing deploy result async function printDeploy(deployHash) { console.log("Deploy hash: " + deployHash); console.log("Deploy result:"); console.log(DeployUtil.deployToJson(await getDeploy(deployHash))); } +// Helper method for printing account info async function printAccount(account) { console.log("\n[x] Current state of the account:"); - console.log(JSON.stringify(await getAccount(account.publicKey), null, 2)); + console.log(JSON.parse(JSON.stringify(await getAccount(account.publicKey), null, 2))); + await pauseAndWaitForKeyPress(); } -function randomMasterKey() { - const seed = new Uint8Array(randomSeed()); - return client.newHdWallet(seed); +// +// Transfers +// + +// Builds native transfer deploy +function transferDeploy(fromAccount, toAccount, amount) { + const deployParams = new DeployUtil.DeployParams( + fromAccount.publicKey, + NETWORK_NAME + ); + const transferParams = DeployUtil.ExecutableDeployItem.newTransfer( + amount, + toAccount.publicKey, + null, + 1 + ); + const payment = DeployUtil.standardPayment(PAYMENT_AMOUNT); + return DeployUtil.makeDeploy(deployParams, transferParams, payment); +} + +// Helper method for funding the specified account from a faucetAccount +async function fundAccount(account) { + const deploy = transferDeploy(faucetAccount, account, FUND_AMOUNT); + await sendDeploy(deploy, [faucetAccount]); } -// Key manager +// +// Contract deploy related methods +// + +// Builds a deploy that will install key-manager contract on specified account +function buildContractInstallDeploy(baseAccount) { + const deployParams = new DeployUtil.DeployParams( + baseAccount.publicKey, + NETWORK_NAME + ); + const session = new Uint8Array(fs.readFileSync(WASM_PATH, null).buffer); + const runtimeArgs = RuntimeArgs.fromMap({}); + const sessionModule = DeployUtil.ExecutableDeployItem.newModuleBytes( + session, + runtimeArgs + ); + const payment = DeployUtil.standardPayment(PAYMENT_AMOUNT); -function setAll(fromAccount, deployThereshold, keyManagementThreshold, accountWeights) { - let accounts = accountWeights.map(x => x.publicKey); - let weights = accountWeights.map(x => CLValueBuilder.u8(x.weight)); + return DeployUtil.makeDeploy(deployParams, sessionModule, payment); +} + +// Builds key-manager deploy that takes entrypoint and args +function buildKeyManagerDeploy(baseAccount, entrypoint, args) { + const deployParams = new DeployUtil.DeployParams( + baseAccount.publicKey, + NETWORK_NAME + ); + const runtimeArgs = RuntimeArgs.fromMap(args); + const sessionModule = DeployUtil.ExecutableDeployItem.newStoredContractByName( + "keys_manager", + entrypoint, + runtimeArgs + ); + const payment = DeployUtil.standardPayment(PAYMENT_AMOUNT); + return DeployUtil.makeDeploy(deployParams, sessionModule, payment); +} + +// +// Key-manager contract specific methods +// + +// Sets deploy threshold, key management threshold, and weights for the specified accounts +function setAll(fromAccount, deployThreshold, keyManagementThreshold, accountWeights) { + const accounts = accountWeights.map(x => x.publicKey); + const weights = accountWeights.map(x => CLValueBuilder.u8(x.weight)); return buildKeyManagerDeploy(fromAccount, "set_all", { - deployment_thereshold: CLValueBuilder.u8(deployThereshold), + deployment_thereshold: CLValueBuilder.u8(deployThreshold), key_management_threshold: CLValueBuilder.u8(keyManagementThreshold), accounts: CLValueBuilder.list(accounts), weights: CLValueBuilder.list(weights), }); } +// Sets key with a specified weight function setKeyWeightDeploy(fromAccount, account, weight) { return buildKeyManagerDeploy(fromAccount, "set_key_weight", { account: account.publicKey, @@ -104,74 +183,23 @@ function setKeyWeightDeploy(fromAccount, account, weight) { }); } +// Sets deploys threshold function setDeploymentThresholdDeploy(fromAccount, weight) { return buildKeyManagerDeploy(fromAccount, "set_deployment_threshold", { weight: CLValueBuilder.u8(weight) }); } +// Sets key-management threshold function setKeyManagementThresholdDeploy(fromAccount, weight) { return buildKeyManagerDeploy(fromAccount, "set_key_management_threshold", { weight: CLValueBuilder.u8(weight) }); } -function buildKeyManagerDeploy(baseAccount, entrypoint, args) { - let deployParams = new DeployUtil.DeployParams( - baseAccount.publicKey, - NETWORK_NAME - ); - let runtimeArgs = RuntimeArgs.fromMap(args); - let sessionModule = DeployUtil.ExecutableDeployItem.newStoredContractByName( - "keys_manager", - entrypoint, - runtimeArgs - ); - let payment = DeployUtil.standardPayment(PAYMENT_AMOUNT); - return DeployUtil.makeDeploy(deployParams, sessionModule, payment); -} - -function buildContractInstallDeploy(baseAccount) { - let deployParams = new DeployUtil.DeployParams( - baseAccount.publicKey, - NETWORK_NAME - ); - const session = new Uint8Array(fs.readFileSync(WASM_PATH, null).buffer); - let runtimeArgs = RuntimeArgs.fromMap({}); - - let sessionModule = DeployUtil.ExecutableDeployItem.newModuleBytes( - session, - runtimeArgs - ); - let payment = DeployUtil.standardPayment(PAYMENT_AMOUNT); - return DeployUtil.makeDeploy(deployParams, sessionModule, payment); -} - -// Funding -function transferDeploy(fromAccount, toAccount, amount) { - let deployParams = new DeployUtil.DeployParams( - fromAccount.publicKey, - NETWORK_NAME - ); - let transferParams = DeployUtil.ExecutableDeployItem.newTransfer( - amount, - toAccount.publicKey, - null, - 1 - ); - let payment = DeployUtil.standardPayment(PAYMENT_AMOUNT); - return DeployUtil.makeDeploy(deployParams, transferParams, payment); -} - -async function fund(account) { - let deploy = transferDeploy(faucetAccount, account, FUND_AMOUNT); - await sendDeploy(deploy, [faucetAccount]); -} - module.exports = { 'randomMasterKey': randomMasterKey, - 'toAccountHashString': toAccountHashString, - 'fund': fund, + 'fundAccount': fundAccount, 'printAccount': printAccount, 'keys': { 'setAll': setAll, diff --git a/client/src/scenario-all.js b/client/src/scenario-all.js index 171d8df..bfd04ca 100644 --- a/client/src/scenario-all.js +++ b/client/src/scenario-all.js @@ -1,12 +1,25 @@ const keyManager = require('./key-manager'); -const amount = process.env.AMOUNT; +const TRANSFER_AMOUNT = process.env.TRANSFER_AMOUNT || 2500000000; (async function () { + + // In this example the 3 additional accounts will be added to + // the mainAccount but two out of four witll be needed to perform the deploy + // and three out of four to add new account. + + // To achive the task, we will: + // 1. Set Keys Management Threshold to 3. + // 2. Set Deploy Threshold to 2. + // 3. Set every account weight to 1. + // 4. Make a transfer from mainAccount to secondAccount using first & second accounts. + // + // 1, 2, 3 will be done in one step. + const masterKey = keyManager.randomMasterKey(); const mainAccount = masterKey.deriveIndex(1); const firstAccount = masterKey.deriveIndex(2); const secondAccount = masterKey.deriveIndex(3); - const thirdAccount = masterKey.deriveIndex(3); + const thirdAccount = masterKey.deriveIndex(4); console.log("Main account: " + mainAccount.publicKey.toHex()); console.log("First account: " + firstAccount.publicKey.toHex()); @@ -14,7 +27,7 @@ const amount = process.env.AMOUNT; console.log("Third account: " + thirdAccount.publicKey.toHex()); console.log("\n[x] Funding main account."); - await keyManager.fund(mainAccount); + await keyManager.fundAccount(mainAccount); await keyManager.printAccount(mainAccount); console.log("\n[x] Install Keys Manager contract"); @@ -22,8 +35,11 @@ const amount = process.env.AMOUNT; await keyManager.sendDeploy(deploy, [mainAccount]); await keyManager.printAccount(mainAccount); + // Deploy threshold is 2 out of 4 const deployThereshold = 2; + // Key Managment threshold is 3 out of 4 const keyManagementThreshold = 3; + // Every account weight is set to 1 const accounts = [ { publicKey: mainAccount.publicKey, weight: 1 }, { publicKey: firstAccount.publicKey, weight: 1 }, @@ -37,7 +53,7 @@ const amount = process.env.AMOUNT; await keyManager.printAccount(mainAccount); console.log("\n[x] Make transfer."); - deploy = keyManager.transferDeploy(mainAccount, secondAccount, amount); + deploy = keyManager.transferDeploy(mainAccount, secondAccount, TRANSFER_AMOUNT); await keyManager.sendDeploy(deploy, [firstAccount, secondAccount]); await keyManager.printAccount(mainAccount); })(); diff --git a/client/src/scenario-atomic.js b/client/src/scenario-atomic.js index 4e20c43..4a1ab76 100644 --- a/client/src/scenario-atomic.js +++ b/client/src/scenario-atomic.js @@ -1,5 +1,5 @@ const keyManager = require('./key-manager'); -const amount = process.env.AMOUNT; +const TRANSFER_AMOUNT = process.env.TRANSFER_AMOUNT || 2500000000; (async function () { @@ -29,7 +29,7 @@ const amount = process.env.AMOUNT; let secondAccount = masterKey.deriveIndex(3); console.log("\n0.1 Fund main account.\n"); - await keyManager.fund(mainAccount); + await keyManager.fundAccount(mainAccount); await keyManager.printAccount(mainAccount); console.log("\n[x]0.2 Install Keys Manager contract"); @@ -69,7 +69,7 @@ const amount = process.env.AMOUNT; // 6. Make a transfer from faucet using the new accounts. console.log("\n6. Make a transfer from faucet using the new accounts.\n"); - deploy = keyManager.transferDeploy(mainAccount, firstAccount, amount); + deploy = keyManager.transferDeploy(mainAccount, firstAccount, TRANSFER_AMOUNT); await keyManager.sendDeploy(deploy, [firstAccount, secondAccount]); await keyManager.printAccount(mainAccount); diff --git a/client/src/utils.js b/client/src/utils.js index e46ba21..eb058a8 100644 --- a/client/src/utils.js +++ b/client/src/utils.js @@ -4,10 +4,6 @@ function randomSeed() { return Array.from({length: 40}, () => Math.floor(Math.random() * 128)) } -function toAccountHashString(publicKey) { - return Buffer.from(publicKey.toAccountHash()).toString('hex'); -} - function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } @@ -19,9 +15,23 @@ const getAccountFromKeyPair = (baseKeyPath) => { return Keys.Ed25519.parseKeyFiles(publicKeyPath, privateKeyPath); } +const pauseAndWaitForKeyPress = async () => { + if (process.argv[2] === "interactive") { + process.stdin.setRawMode(true) + console.log("\n============================================\n"); + console.log("press any key to continue script execution"); + return new Promise(resolve => process.stdin.once('data', () => { + process.stdin.setRawMode(false) + resolve() + console.log("\n============================================\n"); + })) + } + return; +} + module.exports = { randomSeed: randomSeed, - toAccountHashString: toAccountHashString, sleep: sleep, - getAccountFromKeyPair: getAccountFromKeyPair + getAccountFromKeyPair: getAccountFromKeyPair, + pauseAndWaitForKeyPress }