diff --git a/apps/README.md b/apps/README.md index abed47abfac0594c968b0d0e4857fc3d1815505b..20cc02072a153e156482705352901880a938f37d 100644 --- a/apps/README.md +++ b/apps/README.md @@ -6,6 +6,10 @@ This project has three command-line applications: - [sidenor-cli](./sidenor-cli) - [pubadmin-cli](./pubadmin-cli) +There is also a simulator to generate a bunch of transactions and affect stats. + +- [@hypercog/batch-sim](./batch-sim) + Finally, there is a common library that they use: - [@hypercog/utils](./utils) @@ -98,4 +102,8 @@ Bid from cement-company1-com:eDUwOTo6Q049dXNlcixPVT1jbGllbnQrT1U9b3JnMStPVT1kZXB $ cd ../pubadmin-cli $ node index.js stats + +# Or to generate/simulate a complete cycle: +$ cd ../batch-sim +$ node index.js simulate ``` diff --git a/apps/batch-sim/index.js b/apps/batch-sim/index.js new file mode 100644 index 0000000000000000000000000000000000000000..22491b112ffdfa12a3405759107e228a2108f9e6 --- /dev/null +++ b/apps/batch-sim/index.js @@ -0,0 +1,82 @@ +// COPYRIGHT: FUNDACIÓN TECNALIA RESEARCH & INNOVATION, 2022. + +const { Option } = require("commander") + +const { createCLIApp, HypercogApiClient } = require("@hypercog/utils") +const { + createAsset, + editComposition, + sendToCone, + sendToStock, + acceptBid +} = require("@hypercog/sidenor-cli") + +const { bidAsset } = require("@hypercog/cement-cli") + +const signInAs = async (apiClient, userMail, password) => { + const [username, organization] = userMail.split("@") + + const okLogin = await apiClient.signIn(username, password, organization) + if (!okLogin) { + console.error("Unauthorized login") + return + } +} + +const createAssetAndSendToCone = async (apiClient, name) => { + const assetId = await createAsset(apiClient, name) + await editComposition(apiClient, { assetId }) + return sendToCone(apiClient, { assetId }) +} + +const simulateCycle = async (arg1, cmd) => { + const { api, network, channel, chaincode, password, ...otherArgs } = + cmd.parent.opts() + + const apiClient = new HypercogApiClient(api, network, channel, chaincode) + + await signInAs(apiClient, otherArgs.sidenorUser, password) + + let batchId = await createAssetAndSendToCone(apiClient, "simulated asset 1") + batchId = await createAssetAndSendToCone(apiClient, "simulated asset 2") + batchId = await createAssetAndSendToCone(apiClient, "simulated asset 3") + + const stockId = await sendToStock(apiClient, { assetId: batchId }) + + console.log("Stock Id", stockId) + //await createAsset(apiClient, "simulated asset 2") + //await createAsset(apiClient, "simulated asset 3") + + await signInAs(apiClient, otherArgs.cement1User, password) + await bidAsset(apiClient, { assetId: stockId, quantity: 1000, price: 2 }) + + await signInAs(apiClient, otherArgs.cement2User, password) + await bidAsset(apiClient, { assetId: stockId, quantity: 500, price: 3 }) + + await signInAs(apiClient, otherArgs.sidenorUser, password) + await acceptBid(apiClient, { assetId: stockId }) +} + +const program = createCLIApp() + .addOption( + new Option( + "-su --sidenorUser <value>", + "User who will operate with blockchain as a steel manufacturer" + ).env("SIDENOR_USER_ID") + ) + .addOption( + new Option( + "-cu1 --cement1User <value>", + "User who will operate with blockchain as a cement company 1" + ).env("CEMENT1_USER_ID") + ) + .addOption( + new Option( + "-cu2 --cement2User <value>", + "User who will operate with blockchain as a cement company 2" + ).env("CEMENT2_USER_ID") + ) + +program.command("simulate").description("Simulate usage").action(simulateCycle) + +program.parse(process.argv) diff --git a/apps/batch-sim/package-lock.json b/apps/batch-sim/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..860683f2e1dfb02d38d8dfd7131fde7b7eef18e8 --- /dev/null +++ b/apps/batch-sim/package-lock.json @@ -0,0 +1,57 @@ +{ + "name": "@hypercog/batch-sim", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@hypercog/batch-sim", + "dependencies": { + "commander": "^9.0.0" + } + }, + "../cement-cli": { + "name": "@hypercog/cement-cli", + "version": "0.0.1", + "extraneous": true, + "dependencies": { + "@hypercog/utils": "^0.0.1" + } + }, + "../sidenor-cli": { + "name": "@hypercog/sidenor-cli", + "version": "0.0.1", + "extraneous": true, + "dependencies": { + "@faker-js/faker": "^5.5.3", + "@hypercog/utils": "^0.0.1" + } + }, + "../utils": { + "name": "@hypercog/utils", + "version": "0.0.1", + "extraneous": true, + "dependencies": { + "@faker-js/faker": "^5.5.3", + "@traceblock/api-client": "^0.2.5", + "commander": "^9.0.0", + "dotenv": "^16.0.0", + "inquirer": "^8.2.0" + } + }, + "node_modules/commander": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.0.0.tgz", + "integrity": "sha512-JJfP2saEKbQqvW+FI93OYUB4ByV5cizMpFMiiJI8xDbBvQvSkIk0VvQdn1CZ8mqAO8Loq2h0gYTYtDFUZUeERw==", + "engines": { + "node": "^12.20.0 || >=14" + } + } + }, + "dependencies": { + "commander": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.0.0.tgz", + "integrity": "sha512-JJfP2saEKbQqvW+FI93OYUB4ByV5cizMpFMiiJI8xDbBvQvSkIk0VvQdn1CZ8mqAO8Loq2h0gYTYtDFUZUeERw==" + } + } +} diff --git a/apps/batch-sim/package.json b/apps/batch-sim/package.json new file mode 100644 index 0000000000000000000000000000000000000000..d547ac0d7c26fab53bff897ce6473e839fa12bc2 --- /dev/null +++ b/apps/batch-sim/package.json @@ -0,0 +1,9 @@ +{ + "name": "@hypercog/batch-sim", + "dependencies": { + "@hypercog/utils": "^0.0.1", + "@hypercog/sidenor-cli": "^0.0.1", + "@hypercog/cement-cli": "^0.0.1", + "commander": "^9.0.0" + } +} diff --git a/apps/cement-cli/index.js b/apps/cement-cli/index.js index 78b906339d5f8d5cc1d67de4f42c2bb31a10d7df..6bf17a0aca0e51ca7aa45786f2cc00b1d8b6a8c4 100644 --- a/apps/cement-cli/index.js +++ b/apps/cement-cli/index.js @@ -2,47 +2,9 @@ const inquirer = require("inquirer") -const { - createCLIApp, - loginAndCallAfterwards, - promptAssetSelection -} = require("@hypercog/utils") +const { createCLIApp, loginAndCallAfterwards } = require("@hypercog/utils") -const bidAsset = async ( - apiClient, - { assetId: paramAssetId, quantity, price } -) => { - const biddableAssets = await apiClient.getBiddableAssets() - - const assetId = paramAssetId || (await promptAssetSelection(biddableAssets)) - - let chosenQuantity = quantity - if (!quantity) { - const { inputQuantity } = await inquirer.prompt([ - { - type: "number", - name: "inputQuantity", - message: "Choose an ammount of slag" - } - ]) - chosenQuantity = inputQuantity - } - - let chosenPrice = price - if (!quantity) { - const { inputPrice } = await inquirer.prompt([ - { - type: "number", - name: "inputPrice", - message: "Provide the offered price" - } - ]) - chosenPrice = inputPrice - } - - await apiClient.bidForAsset(assetId, chosenQuantity, chosenPrice) - console.log("Bid for slag stock", assetId) -} +const { bidAsset } = require("./lib") const program = createCLIApp() @@ -55,3 +17,6 @@ program .action(loginAndCallAfterwards(bidAsset)) program.parse(process.argv) + +// To be reused as a library in @hypercog/batch-sim +module.exports = { bidAsset } diff --git a/apps/cement-cli/lib.js b/apps/cement-cli/lib.js new file mode 100644 index 0000000000000000000000000000000000000000..d76da1924639e51385f591c48cacaa1531234335 --- /dev/null +++ b/apps/cement-cli/lib.js @@ -0,0 +1,44 @@ +// COPYRIGHT: FUNDACIÓN TECNALIA RESEARCH & INNOVATION, 2022. + +const inquirer = require("inquirer") + +const { promptAssetSelection } = require("@hypercog/utils") + +const bidAsset = async ( + apiClient, + { assetId: paramAssetId, quantity, price } +) => { + const biddableAssets = await apiClient.getBiddableAssets() + + const assetId = paramAssetId || (await promptAssetSelection(biddableAssets)) + + let chosenQuantity = quantity + if (!quantity) { + const { inputQuantity } = await inquirer.prompt([ + { + type: "number", + name: "inputQuantity", + message: "Choose an ammount of slag" + } + ]) + chosenQuantity = inputQuantity + } + + let chosenPrice = price + if (!quantity) { + const { inputPrice } = await inquirer.prompt([ + { + type: "number", + name: "inputPrice", + message: "Provide the offered price" + } + ]) + chosenPrice = inputPrice + } + + await apiClient.bidForAsset(assetId, chosenQuantity, chosenPrice) + console.log("Bid for slag stock", assetId) +} + +// To be reused as a library in @hypercog/batch-sim +module.exports = { bidAsset } diff --git a/apps/cement-cli/package.json b/apps/cement-cli/package.json index bc45a1c2d04829c4d39364fd44601ea063320885..b08528409da1f2a08288a2fb3b56435b35992f9a 100644 --- a/apps/cement-cli/package.json +++ b/apps/cement-cli/package.json @@ -1,7 +1,7 @@ { "name": "@hypercog/cement-cli", "version": "0.0.1", - "main": "index.js", + "main": "lib.js", "dependencies": { "@hypercog/utils": "^0.0.1" } diff --git a/apps/lerna.json b/apps/lerna.json index 5b6dae47fc33974a268dc6b4810d6061056b7930..f64339158c45c4752dfe6b92de81704bd10a7cde 100644 --- a/apps/lerna.json +++ b/apps/lerna.json @@ -1,4 +1,10 @@ { - "packages": ["utils", "sidenor-cli", "cement-cli", "pubadmin-cli"], + "packages": [ + "utils", + "sidenor-cli", + "cement-cli", + "pubadmin-cli", + "batch-sim" + ], "version": "0.0.1" } diff --git a/apps/sidenor-cli/index.js b/apps/sidenor-cli/index.js index 9f453de8ca039963f746bc7a1f9ca32b4ea4886b..ee4592d80aaf851fdc90465eca75c846d7faf761 100644 --- a/apps/sidenor-cli/index.js +++ b/apps/sidenor-cli/index.js @@ -1,211 +1,15 @@ // COPYRIGHT: FUNDACIÓN TECNALIA RESEARCH & INNOVATION, 2021. -const faker = require("@faker-js/faker") -const inquirer = require("inquirer") +const { createCLIApp, loginAndCallAfterwards } = require("@hypercog/utils") const { - createCLIApp, - loginAndCallAfterwards, - createRandomLocation, - promptAssetSelection, - decodeUserId, - TYPES, - STATUSES -} = require("@hypercog/utils") - -const createAsset = async (apiClient, { assetName }) => { - const newAsset = await apiClient.createAsset({ - id: `asst_${faker.datatype.uuid()}`, - type: TYPES.RESIDUO_PROCESOS_TERMICOS, - units: "kg", - quantity: faker.datatype.number({ max: 5000 }), - fields: { - description: "Escoria", - name: assetName, - status: STATUSES.SLAG, - castingId: 1 - }, - location: createRandomLocation() - }) - - console.log("Registered asset", newAsset.id) -} - -const editComposition = async (apiClient, { assetId: paramAssetId }) => { - const assetId = - paramAssetId || (await promptAssetSelection(await apiClient.richQuery())) - - const previousVersion = await apiClient.getAsset(assetId) - - await apiClient.modifyAsset({ - id: assetId, - fields: { - ...previousVersion.fields, - composition: "comp1" - } - }) - - console.log("New composition measure for", assetId) -} - -const sendToCone = async (apiClient, { assetId: paramAssetId }) => { - const assetId = - paramAssetId || - (await promptAssetSelection( - await apiClient.richQuery({ - fields: { - status: STATUSES.SLAG - } - }) - )) - - const assetToBeSent = await apiClient.getAsset(assetId) - const assetsInCone = await apiClient.richQuery({ - fields: { - status: STATUSES.SLAG_BATCH - } - }) - - const [firstAsset] = assetsInCone - const newConeQuantity = - assetToBeSent.quantity + (firstAsset ? firstAsset.quantity : 0) - - const coneAsset = await apiClient.joinAsset( - //`cone_${faker.datatype.uuid()}`, - firstAsset ? [firstAsset.id, assetId] : [assetId], - { parentsShouldExist: false, bidirectional: true }, - { - type: TYPES.RESIDUO_HIERRO_ACERO, - location: - assetsInCone.length === 0 - ? assetToBeSent.location - : assetsInCone[0].location, - units: "kg", - quantity: newConeQuantity, - fields: { - status: STATUSES.SLAG_BATCH - } - } - ) - - // Archive individual slag and previous cone - await apiClient.deleteAsset(assetId) - if (assetsInCone.length > 0) await apiClient.deleteAsset(assetsInCone[0].id) - - console.log("Moved to cone", coneAsset.id) -} - -const sendToStock = async (apiClient, { assetId: paramAssetId }) => { - const assetId = - paramAssetId || - (await promptAssetSelection( - await apiClient.richQuery({ - fields: { - status: STATUSES.SLAG_BATCH - } - }) - )) - - const assetToBeSent = await apiClient.getAsset(assetId) - if (assetToBeSent.fields.status !== STATUSES.SLAG_BATCH) { - console.error("You can only combine a cone with a stock") - return - } - - const selectedStockId = await promptAssetSelection( - [ - ...(await apiClient.richQuery({ - fields: { - status: STATUSES.STOCK - } - })), - { - id: "newStock", - type: TYPES.RESIDUO_HIERRO_ACERO, - fields: { name: "New stock", status: STATUSES.STOCK } - } - ], - "Select the stock where the cone will be merged" - ) - - if (!selectedStockId) { - console.error("You must select a stock") - return - } - - let previousStock = false - if (selectedStockId !== "newStock") { - previousStock = await apiClient.getAsset(selectedStockId) - } - const newStockQuantity = - assetToBeSent.quantity + (previousStock ? previousStock.quantity : 0) - - const newStockAsset = await apiClient.joinAsset( - selectedStockId === "newStock" ? [assetId] : [selectedStockId, assetId], - { parentsShouldExist: true, bidirectional: true }, - { - type: TYPES.RESIDUO_HIERRO_ACERO, - location: - selectedStockId === "newStock" - ? assetToBeSent.location - : ( - await apiClient.getAsset(selectedStockId) - ).location, - units: "kg", - quantity: newStockQuantity, - fields: { - status: STATUSES.STOCK, - name: "Stock" - } - } - ) - - // Archive individual slag and previous cone - await apiClient.deleteAsset(assetId) - if (selectedStockId !== "newStock") - await apiClient.deleteAsset(selectedStockId) - - console.log("Moved to stock", newStockAsset.id) -} - -const bidResponse = - action => - async (apiClient, { assetId: paramAssetId, bidder: paramBidder }) => { - const assetId = - paramAssetId || - (await promptAssetSelection( - await apiClient.richQuery({ - fields: { - status: STATUSES.STOCK - } - }) - )) - - const assetSelected = await apiClient.getAsset(assetId) - if (!assetSelected.fields.bids || assetSelected.fields.bids.length === 0) { - console.error(`There are no active bids to be ${action}ed`) - return - } - - let bidder = paramBidder - if (!bidder) { - const { inputBidder } = await inquirer.prompt([ - { - type: "list", - name: "inputBidder", - message: `Select the bidder whose bid will be ${action}ed`, - choices: assetSelected.fields.bids.map(b => ({ - name: `${b.bidder.id} (quantity: ${b.quantity}, price: ${b.price})`, - value: b.bidder - })) - } - ]) - bidder = inputBidder - } - - await apiClient[`${action}Bid`](assetId, bidder.id) - console.log(`Bid from ${bidder.id} ${action}ed for asset ${assetId}`) - } + createAsset, + editComposition, + sendToCone, + sendToStock, + acceptBid, + rejectBid +} = require("./lib") const program = createCLIApp() @@ -247,7 +51,7 @@ program "--bidder [value]", "Identifier of the bidder for the provided slag stock" ) - .action(loginAndCallAfterwards(bidResponse("accept"))) + .action(loginAndCallAfterwards(acceptBid)) program .command("bid-reject") @@ -257,7 +61,7 @@ program "--bidder [value]", "Identifier of the bidder for the provided slag stock" ) - .action(loginAndCallAfterwards(bidResponse("reject"))) + .action(loginAndCallAfterwards(rejectBid)) /*program .command("discard-stock") diff --git a/apps/sidenor-cli/lib.js b/apps/sidenor-cli/lib.js new file mode 100644 index 0000000000000000000000000000000000000000..d7b89286d947da6f7aca733ea0ddbb01f5c93e37 --- /dev/null +++ b/apps/sidenor-cli/lib.js @@ -0,0 +1,225 @@ +// COPYRIGHT: FUNDACIÓN TECNALIA RESEARCH & INNOVATION, 2022. + +const faker = require("@faker-js/faker") +const inquirer = require("inquirer") + +const { + createRandomLocation, + promptAssetSelection, + TYPES, + STATUSES +} = require("@hypercog/utils") + +const createAsset = async (apiClient, { assetName }) => { + const newAsset = await apiClient.createAsset({ + id: `asst_${faker.datatype.uuid()}`, + type: TYPES.RESIDUO_PROCESOS_TERMICOS, + units: "kg", + quantity: faker.datatype.number({ max: 5000 }), + fields: { + description: "Escoria", + name: assetName, + status: STATUSES.SLAG, + castingId: 1 + }, + location: createRandomLocation() + }) + + console.log("Registered asset", newAsset.id) + + return newAsset.id +} + +const editComposition = async (apiClient, { assetId: paramAssetId }) => { + const assetId = + paramAssetId || (await promptAssetSelection(await apiClient.richQuery())) + + const previousVersion = await apiClient.getAsset(assetId) + + await apiClient.modifyAsset({ + ...previousVersion, + id: assetId, + fields: { + ...previousVersion.fields, + composition: "comp1" + } + }) + + console.log("New composition measure for", assetId) +} + +const sendToCone = async (apiClient, { assetId: paramAssetId }) => { + const assetId = + paramAssetId || + (await promptAssetSelection( + await apiClient.richQuery({ + fields: { + status: STATUSES.SLAG + } + }) + )) + + const assetToBeSent = await apiClient.getAsset(assetId) + const assetsInCone = await apiClient.richQuery({ + fields: { + status: STATUSES.SLAG_BATCH + } + }) + + const [firstAsset] = assetsInCone + const newConeQuantity = + assetToBeSent.quantity + (firstAsset ? firstAsset.quantity : 0) + + const coneAsset = await apiClient.joinAsset( + //`cone_${faker.datatype.uuid()}`, + firstAsset ? [firstAsset.id, assetId] : [assetId], + { parentsShouldExist: false, bidirectional: true }, + { + type: TYPES.RESIDUO_HIERRO_ACERO, + location: + assetsInCone.length === 0 + ? assetToBeSent.location + : assetsInCone[0].location, + units: "kg", + quantity: newConeQuantity, + fields: { + status: STATUSES.SLAG_BATCH + } + } + ) + + // Archive individual slag and previous cone + await apiClient.deleteAsset(assetId) + if (assetsInCone.length > 0) await apiClient.deleteAsset(assetsInCone[0].id) + + console.log("Moved to cone", coneAsset.id) + + return coneAsset.id +} + +const sendToStock = async (apiClient, { assetId: paramAssetId }) => { + const assetId = + paramAssetId || + (await promptAssetSelection( + await apiClient.richQuery({ + fields: { + status: STATUSES.SLAG_BATCH + } + }) + )) + + const assetToBeSent = await apiClient.getAsset(assetId) + if (assetToBeSent.fields.status !== STATUSES.SLAG_BATCH) { + console.error("You can only combine a cone with a stock") + return + } + + const selectedStockId = await promptAssetSelection( + [ + ...(await apiClient.richQuery({ + fields: { + status: STATUSES.STOCK + } + })), + { + id: "newStock", + type: TYPES.RESIDUO_HIERRO_ACERO, + fields: { name: "New stock", status: STATUSES.STOCK } + } + ], + "Select the stock where the cone will be merged" + ) + + if (!selectedStockId) { + console.error("You must select a stock") + return + } + + let previousStock = false + if (selectedStockId !== "newStock") { + previousStock = await apiClient.getAsset(selectedStockId) + } + const newStockQuantity = + assetToBeSent.quantity + (previousStock ? previousStock.quantity : 0) + + const newStockAsset = await apiClient.joinAsset( + selectedStockId === "newStock" ? [assetId] : [selectedStockId, assetId], + { parentsShouldExist: true, bidirectional: true }, + { + type: TYPES.RESIDUO_HIERRO_ACERO, + location: + selectedStockId === "newStock" + ? assetToBeSent.location + : ( + await apiClient.getAsset(selectedStockId) + ).location, + units: "kg", + quantity: newStockQuantity, + fields: { + status: STATUSES.STOCK, + name: "Stock" + } + } + ) + + // Archive individual slag and previous cone + await apiClient.deleteAsset(assetId) + if (selectedStockId !== "newStock") + await apiClient.deleteAsset(selectedStockId) + + console.log("Moved to stock", newStockAsset.id) + + return newStockAsset.id +} + +const bidResponse = + action => + async (apiClient, { assetId: paramAssetId, bidder: paramBidder }) => { + const assetId = + paramAssetId || + (await promptAssetSelection( + await apiClient.richQuery({ + fields: { + status: STATUSES.STOCK + } + }) + )) + + const assetSelected = await apiClient.getAsset(assetId) + if (!assetSelected.fields.bids || assetSelected.fields.bids.length === 0) { + console.error(`There are no active bids to be ${action}ed`) + return + } + + let bidder = paramBidder + if (!bidder) { + const { inputBidder } = await inquirer.prompt([ + { + type: "list", + name: "inputBidder", + message: `Select the bidder whose bid will be ${action}ed`, + choices: assetSelected.fields.bids.map(b => ({ + name: `${b.bidder.id} (quantity: ${b.quantity}, price: ${b.price})`, + value: b.bidder + })) + } + ]) + bidder = inputBidder + } + + await apiClient[`${action}Bid`](assetId, bidder.id) + console.log(`Bid from ${bidder.id} ${action}ed for asset ${assetId}`) + } + +const acceptBid = bidResponse("accept") +const rejectBid = bidResponse("reject") + +// To be reused as a library in @hypercog/batch-sim +module.exports = { + createAsset, + editComposition, + sendToCone, + sendToStock, + acceptBid, + rejectBid +} diff --git a/apps/sidenor-cli/package.json b/apps/sidenor-cli/package.json index 9b0366e6a9cbb19e3a82734cb72662defeb0a638..3a3ea1f8db8a3cb61601bdd620420e7eb672a1aa 100644 --- a/apps/sidenor-cli/package.json +++ b/apps/sidenor-cli/package.json @@ -1,7 +1,7 @@ { "name": "@hypercog/sidenor-cli", "version": "0.0.1", - "main": "index.js", + "main": "lib.js", "scripts": { "sidenor": "func() { node index.js $@; }; func", "sidenor2": "node index.js --" diff --git a/apps/utils/utils.js b/apps/utils/utils.js index 2ccb083407de1ac2611f60de9abc5455e70fe6a1..8293535c98320635e210d22cf31fe787fececdb6 100644 --- a/apps/utils/utils.js +++ b/apps/utils/utils.js @@ -150,6 +150,7 @@ const decodeUserId = userId => { } module.exports = { + HypercogApiClient, createCLIApp, loginAndCallAfterwards, createRandomLocation,