diff --git a/CMakeLists.txt b/CMakeLists.txt index 86e39e9..87552e4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,7 +79,7 @@ add_custom_target(installer COMMAND mkdir -p ./build/installer COMMAND bash -c "cp -r ./build/{sagent,sashi,hpfs,user-install.sh,user-uninstall.sh,contract_template} ./build/installer/" COMMAND bash -c "cp -r ./installer/{docker-install.sh,registry-install.sh,registry-uninstall.sh,prereq.sh,sashimono-install.sh,sashimono-uninstall.sh} ./build/installer/" - COMMAND bash -c "cp -r ./dependencies/{user-cgcreate.sh,libblake3.so} ./build/installer/" + COMMAND bash -c "cp -r ./dependencies/{user-cgcreate.sh,libblake3.so,licence.txt} ./build/installer/" COMMAND bash -c "cp -r ./mb-xrpl/dist ./build/installer/mb-xrpl" COMMAND tar cfz ./build/installer.tar.gz --directory=./build/ installer COMMAND rm -r ./build/installer diff --git a/dependencies/licence.txt b/dependencies/licence.txt new file mode 100644 index 0000000..0b16caf --- /dev/null +++ b/dependencies/licence.txt @@ -0,0 +1,14 @@ +Evernode beta Licence + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to use the software for, or in connection, +with the Evernode network for the duration of the beta test. + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS IN BETA AND PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE +AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/installer/sashimono-install.sh b/installer/sashimono-install.sh index a99634e..05ff825 100755 --- a/installer/sashimono-install.sh +++ b/installer/sashimono-install.sh @@ -95,9 +95,9 @@ if [ "$NO_MB" == "" ]; then echo "Registered host on Evernode." fi -# Copy contract template (delete existing) -rm -r "$SASHIMONO_DATA"/contract_template >/dev/null 2>&1 -cp -r "$script_dir"/contract_template $SASHIMONO_DATA +# Copy contract template and licence file (delete existing) +rm -r "$SASHIMONO_DATA"/{contract_template,licence.txt} >/dev/null 2>&1 +cp -r "$script_dir"/{contract_template,licence.txt} $SASHIMONO_DATA # Install Sashimono agent binaries into sashimono bin dir. cp "$script_dir"/{sagent,hpfs,user-cgcreate.sh,user-install.sh,user-uninstall.sh} $SASHIMONO_BIN diff --git a/mb-xrpl/lib/appenv.js b/mb-xrpl/lib/appenv.js index 37b1665..65a0eb8 100644 --- a/mb-xrpl/lib/appenv.js +++ b/mb-xrpl/lib/appenv.js @@ -12,7 +12,7 @@ appenv = { CONFIG_PATH: appenv.DATA_DIR + '/mb-xrpl.cfg', LOG_PATH: appenv.DATA_DIR + '/log/mb-xrpl.log', DB_PATH: appenv.DATA_DIR + '/mb-xrpl.sqlite', - DB_TABLE_NAME: 'instances_data', + DB_TABLE_NAME: 'instance_data', DB_UTIL_TABLE_NAME: 'util_data', LAST_WATCHED_LEDGER: 'last_watched_ledger', ACQUIRE_LEASE_TIMEOUT_THRESHOLD: 0.8, diff --git a/mb-xrpl/lib/message-board.js b/mb-xrpl/lib/message-board.js index 9c42583..43cde5f 100644 --- a/mb-xrpl/lib/message-board.js +++ b/mb-xrpl/lib/message-board.js @@ -58,7 +58,6 @@ class MessageBoard { this.hostClient = new evernode.HostClient(this.cfg.xrpl.address, this.cfg.xrpl.secret); await this.hostClient.connect(); - this.leaseAmount = this.cfg.xrpl.leaseAmount ? this.cfg.xrpl.leaseAmount : parseFloat(this.hostClient.config.purchaserTargetPrice); // in EVRs. // Get last heartbeat moment from the host info. const hostInfo = await this.hostClient.getRegistration(); @@ -135,46 +134,42 @@ class MessageBoard { async handleAcquireLease(r) { - if (r.host !== this.cfg.xrpl.address) { - console.log('Invalid host in the lease aquire.') - return; - } + const acquireRefId = r.acquireRefId; // Acquire tx hash. + const nfTokenId = r.nfTokenId; + const leaseAmount = parseFloat(r.leaseAmount); + const tenantAddress = r.tenant; + let requestValidated = false; + let leaseIndex = -1; // Lease index cannot be negative, So we keep initial non populated value as -1. this.db.open(); - // Update last watched ledger sequence number. - await this.updateLastIndexRecord(r.transaction.LastLedgerSequence); - - const acquireRefId = r.acquireRefId; // Acquire tx hash. - const tenantAddress = r.tenant; - const nfTokenId = r.nfTokenId; - const leaseAmount = parseFloat(r.leaseAmount); - - // Get the existing nft of the lease. - const nft = (await (new evernode.XrplAccount(tenantAddress)).getNfts())?.find(n => n.TokenID == nfTokenId); - if (!nft) { - console.log('Could not find the nft for lease acquire request.') - return; - } - // Get the lease index from the nft URI. - // - const prefixLen = evernode.EvernodeConstants.LEASE_NFT_PREFIX_HEX.length / 2; - const halfToSLen = appenv.TOS_HASH.length / 4; - const uriBuf = Buffer.from(nft.URI, 'hex'); - const leaseIndex = uriBuf.readUint16BE(prefixLen); - const uriLeaseAmount = evernode.XflHelpers.toString(uriBuf.readBigInt64BE(prefixLen + 2 + halfToSLen)); - - if (leaseAmount != parseFloat(uriLeaseAmount)) { - console.log('NFT embedded lease amount and acquire lease amount does not match.'); - return; - } - - // Since acquire is accepted for leaseAmount - const moments = 1; - try { + + if (r.host !== this.cfg.xrpl.address) + throw "Invalid host in the lease aquire."; + + // Update last watched ledger sequence number. + await this.updateLastIndexRecord(r.transaction.LastLedgerSequence); + + // Get the existing nft of the lease. + const nft = (await (new evernode.XrplAccount(tenantAddress)).getNfts())?.find(n => n.TokenID == nfTokenId); + if (!nft) + throw 'Could not find the nft for lease acquire request.'; + + const uriInfo = evernode.UtilHelpers.decodeLeaseNftUri(nft.URI); + + if (leaseAmount != uriInfo.leaseAmount) + throw 'NFT embedded lease amount and acquire lease amount does not match.'; + leaseIndex = uriInfo.leaseIndex; + + // Since acquire is accepted for leaseAmount + const moments = 1; + + // Use NFTokenId as the instance name. + const containerName = nfTokenId; console.log(`Received acquire lease from ${tenantAddress}`); - await this.createLeaseRecord(acquireRefId, tenantAddress, moments); + requestValidated = true; + await this.createLeaseRecord(acquireRefId, tenantAddress, containerName, moments); // The last validated ledger when we receive the acquire request. const startingValidatedLedger = this.lastValidatedLedgerIndex; @@ -193,7 +188,7 @@ class MessageBoard { } else { const instanceRequirements = r.payload; - const createRes = await this.sashiCli.createInstance(instanceRequirements); + const createRes = await this.sashiCli.createInstance(containerName, instanceRequirements); // Number of validated ledgers passed while the instance is created. diff = this.lastValidatedLedgerIndex - startingValidatedLedger; @@ -212,10 +207,10 @@ class MessageBoard { const currentLedgerIndex = this.lastValidatedLedgerIndex; // Add to in-memory expiry list, so the instance will get destroyed when the moments exceed, - this.addToExpiryList(acquireRefId, createRes.content.name, await this.getExpiryMoment(currentLedgerIndex, moments)); + this.addToExpiryList(acquireRefId, containerName, await this.getExpiryMoment(currentLedgerIndex, moments)); // Update the database for acquired record. - await this.updateAcquiredRecord(acquireRefId, createRes.content.name, currentLedgerIndex); + await this.updateAcquiredRecord(acquireRefId, currentLedgerIndex); // Send the acquire response with created instance info. await this.hostClient.acquireSuccess(acquireRefId, tenantAddress, createRes); @@ -225,17 +220,20 @@ class MessageBoard { catch (e) { console.error(e); - // Update the lease response for failures. - await this.updateLeaseStatus(acquireRefId, LeaseStatus.FAILED).catch(console.error); + // Update the lease response for failures (Only if the request validated and ACQUIRING record is added). + if (requestValidated) + await this.updateLeaseStatus(acquireRefId, LeaseStatus.FAILED).catch(console.error); - // Re-create the lease offer. - await this.recreateLeaseOffer(nfTokenId, leaseIndex, leaseAmount).catch(console.error); + // Re-create the lease offer (Only if the nft belongs to this request has a lease index). + if (leaseIndex >= 0) + await this.recreateLeaseOffer(nfTokenId, leaseIndex, leaseAmount).catch(console.error); // Send error transaction with received leaseAmount. - await this.hostClient.acquireError(acquireRefId, tenantAddress, leaseAmount, e.content).catch(console.error); + await this.hostClient.acquireError(acquireRefId, tenantAddress, leaseAmount, e.content || 'invalid_acquire_lease').catch(console.error); + } + finally { + this.db.close(); } - - this.db.close(); } async handleExtendLease(r) { @@ -243,39 +241,47 @@ class MessageBoard { this.db.open(); const extendRefId = r.extendRefId; + const nfTokenId = r.nfTokenId; + const tenantAddress = r.tenant; + const amount = r.payment; try { if (r.transaction.Destination !== this.cfg.xrpl.address) throw "Invalid destination"; - this.leaseAmount = this.cfg.xrpl.leaseAmount ? this.cfg.xrpl.leaseAmount : parseFloat(this.hostClient.config.purchaserTargetPrice); - if (this.leaseAmount <= 0) - throw "Invalid per moment lease amount"; - - const extendingMoments = Math.floor(r.payment / this.leaseAmount); - - if (extendingMoments < 1) - throw "The transaction does not satisfy the minimum extendable moments"; - - const tenantAcc = new evernode.XrplAccount(r.tenant); - const hostingNft = (await tenantAcc.getNfts()).find(n => n.TokenID === r.nfTokenId && n.URI.startsWith(evernode.EvernodeConstants.LEASE_NFT_PREFIX_HEX)); + const tenantAcc = new evernode.XrplAccount(tenantAddress); + const hostingNft = (await tenantAcc.getNfts()).find(n => n.TokenID === nfTokenId && n.URI.startsWith(evernode.EvernodeConstants.LEASE_NFT_PREFIX_HEX)); if (!hostingNft) throw "The NFT ownership verification was failed in the lease extension process"; - const instanceSearchCriteria = { tenant_xrp_address: r.tenant, container_name: hostingNft.TokenID }; + const uriInfo = evernode.UtilHelpers.decodeLeaseNftUri(hostingNft.URI); + const leaseAmount = uriInfo.leaseAmount; + if (leaseAmount <= 0) + throw "Invalid per moment lease amount"; + + const extendingMoments = Math.floor(amount / leaseAmount); + + if (extendingMoments < 1) + throw "The transaction does not satisfy the minimum extendable moments"; + + const instanceSearchCriteria = { tenant_xrp_address: tenantAddress, container_name: hostingNft.TokenID }; const instance = (await this.getLeaseRecords(instanceSearchCriteria)).find(i => (i.status === LeaseStatus.ACQUIRED || i.status === LeaseStatus.EXTENDED)); if (!instance) throw "No relevant instance was found to perform the lease extension"; + console.log(`Received extend lease from ${tenantAddress}`); + let expiryItemFound = false; + let expiryMoment; for (const item of this.expiryList) { if (item.containerName === instance.container_name) { item.expiryMoment += extendingMoments; + expiryMoment = item.expiryMoment; let obj = { status: LeaseStatus.EXTENDED, life_moments: (instance.life_moments + extendingMoments) @@ -290,13 +296,13 @@ class MessageBoard { throw "No matching expiration record was found for the instance"; // Send the extend success response - await this.hostClient.extendSuccess(extendRefId, r.tenant); + await this.hostClient.extendSuccess(extendRefId, tenantAddress, expiryMoment); } catch (e) { console.error(e); // Send the extend error response - await this.hostClient.extendError(extendRefId, r.tenant, e.content, `${r.payment}`); + await this.hostClient.extendError(extendRefId, tenantAddress, e.content || 'invalid_extend_lease', amount); } finally { this.db.close(); } @@ -351,12 +357,13 @@ class MessageBoard { return (await this.db.getValues(this.leaseTable)); } - async createLeaseRecord(txHash, txTenantAddress, moments) { + async createLeaseRecord(txHash, txTenantAddress, containerName, moments) { await this.db.insertValue(this.leaseTable, { timestamp: Date.now(), tx_hash: txHash, tenant_xrp_address: txTenantAddress, life_moments: moments, + container_name: containerName, status: LeaseStatus.ACQUIRING }); } @@ -367,9 +374,8 @@ class MessageBoard { }, { name: appenv.LAST_WATCHED_LEDGER }); } - async updateAcquiredRecord(txHash, containerName, ledgerIndex) { + async updateAcquiredRecord(txHash, ledgerIndex) { await this.db.updateValue(this.leaseTable, { - container_name: containerName, created_on_ledger: ledgerIndex, status: LeaseStatus.ACQUIRED }, { tx_hash: txHash }); diff --git a/mb-xrpl/lib/sashi-cli.js b/mb-xrpl/lib/sashi-cli.js index 8f1be0c..3e4ac57 100644 --- a/mb-xrpl/lib/sashi-cli.js +++ b/mb-xrpl/lib/sashi-cli.js @@ -9,9 +9,10 @@ class SashiCLI { this.cliPath = cliPath; } - async createInstance(requirements) { + async createInstance(containerName, requirements) { if (!requirements.type) requirements.type = 'create'; + requirements.container_name = containerName; const res = await this.execSashiCli(requirements); if (res.type === 'create_error') diff --git a/mb-xrpl/package-lock.json b/mb-xrpl/package-lock.json index 1349da7..d295641 100644 --- a/mb-xrpl/package-lock.json +++ b/mb-xrpl/package-lock.json @@ -772,9 +772,9 @@ "dev": true }, "evernode-js-client": { - "version": "0.4.22", - "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.4.22.tgz", - "integrity": "sha512-Ot8hpUvAnydli9mJejnVt6NmvhwQq3aNPE4QX3Y/PvMBSYWEhNk7SoDbVUzY9GLtwZ1l4nP7S8HzcM9ZrvV38Q==", + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.4.23.tgz", + "integrity": "sha512-elnJNvFF6VGmlgu8jkr3U2wzchMuLY1GE16JPPV3hZOlke++HUQujlI/fLkf/dBGmGCcfAsD2Y4QOxLRJ5PcqA==", "requires": { "elliptic": "6.5.4", "ripple-address-codec": "4.2.0", diff --git a/mb-xrpl/package.json b/mb-xrpl/package.json index 504a19f..c467caf 100644 --- a/mb-xrpl/package.json +++ b/mb-xrpl/package.json @@ -5,7 +5,7 @@ "build": "npm run lint && ncc build app.js --minify -o dist" }, "dependencies": { - "evernode-js-client": "0.4.22", + "evernode-js-client": "0.4.24", "sqlite3": "5.0.2" }, "devDependencies": { diff --git a/sashi-cli/cli-manager.cpp b/sashi-cli/cli-manager.cpp index 358baf9..dc2d003 100644 --- a/sashi-cli/cli-manager.cpp +++ b/sashi-cli/cli-manager.cpp @@ -8,10 +8,10 @@ namespace cli constexpr const char *DATA_DIR = "/etc/sashimono"; // Sashimono data directory. constexpr const char *BIN_DIR = "/usr/bin/sashimono"; // Sashimono bin directory. constexpr const int BUFFER_SIZE = 4096; // Max read buffer size. - constexpr const char *LIST_FORMATTER_STR = "%-38s%-27s%-10s%-10s%-10s%s\n"; + constexpr const char *LIST_FORMATTER_STR = "%-66s%-27s%-10s%-10s%-10s%s\n"; constexpr const char *MSG_LIST = "{\"type\": \"list\"}"; constexpr const char *MSG_BASIC = "{\"type\":\"%s\",\"container_name\":\"%s\"}"; - constexpr const char *MSG_CREATE = "{\"type\":\"create\",\"owner_pubkey\":\"%s\",\"contract_id\":\"%s\",\"image\":\"%s\",\"config\":{}}"; + constexpr const char *MSG_CREATE = "{\"type\":\"create\",\"container_name\":\"%s\",\"owner_pubkey\":\"%s\",\"contract_id\":\"%s\",\"image\":\"%s\",\"config\":{}}"; constexpr const char *DOCKER_ATTACH = "DOCKER_HOST=unix:///run/user/$(id -u %s)/docker.sock %s/dockerbin/docker attach --detach-keys=\"ctrl-c\" %s"; @@ -196,11 +196,11 @@ namespace cli return ret; } - int create(std::string_view owner, std::string_view contract_id, std::string_view image) + int create(std::string_view container_name, std::string_view owner, std::string_view contract_id, std::string_view image) { std::string msg, output; - msg.resize(75 + owner.size() + contract_id.size() + image.size()); - sprintf(msg.data(), MSG_CREATE, owner.data(), contract_id.data(), image.data()); + msg.resize(96 + owner.size() + contract_id.size() + image.size()); + sprintf(msg.data(), MSG_CREATE, container_name.data(), owner.data(), contract_id.data(), image.data()); const int ret = get_json_output(msg, output); if (ret == 0) diff --git a/sashi-cli/cli-manager.hpp b/sashi-cli/cli-manager.hpp index 701328e..33cb6d7 100644 --- a/sashi-cli/cli-manager.hpp +++ b/sashi-cli/cli-manager.hpp @@ -27,7 +27,7 @@ namespace cli int execute_basic(std::string_view type, std::string_view container_name); - int create(std::string_view owner, std::string_view contract_id, std::string_view image); + int create(std::string_view container_name, std::string_view owner, std::string_view contract_id, std::string_view image); int list(); diff --git a/sashi-cli/main.cpp b/sashi-cli/main.cpp index d6a0614..72f107a 100644 --- a/sashi-cli/main.cpp +++ b/sashi-cli/main.cpp @@ -96,6 +96,7 @@ int parse_cmd(int argc, char **argv) create->add_option("-i,--image", image, "Container image to use"); std::string container_name; + create->add_option("-n,--name", container_name, "Instance name"); start->add_option("-n,--name", container_name, "Instance name"); stop->add_option("-n,--name", container_name, "Instance name"); destroy->add_option("-n,--name", container_name, "Instance name"); @@ -163,7 +164,7 @@ int parse_cmd(int argc, char **argv) else if (create->parsed() && !contract_id.empty() && !image.empty()) { return execute_cli([&]() - { return cli::create(owner, contract_id, image); }); + { return cli::create(container_name, owner, contract_id, image); }); } else if (start->parsed() && !container_name.empty()) { diff --git a/src/comm/comm_handler.cpp b/src/comm/comm_handler.cpp index 32c63ba..c1fb47a 100644 --- a/src/comm/comm_handler.cpp +++ b/src/comm/comm_handler.cpp @@ -210,7 +210,7 @@ namespace comm hp::instance_info info; std::string error_msg; - if (hp::create_new_instance(error_msg, info, msg.pubkey, msg.contract_id, msg.image) == -1) + if (hp::create_new_instance(error_msg, info, msg.container_name, msg.pubkey, msg.contract_id, msg.image) == -1) __HANDLE_RESPONSE(msg::MSGTYPE_CREATE_ERROR, error_msg, -1); if (hp::initiate_instance(error_msg, info.container_name, init_msg) == -1) diff --git a/src/hp_manager.cpp b/src/hp_manager.cpp index e0a0c1c..07a90d8 100644 --- a/src/hp_manager.cpp +++ b/src/hp_manager.cpp @@ -109,7 +109,7 @@ namespace hp * @param image_key Docker image name to use (must exist in the config iamge list). * @return 0 on success and -1 on error. */ - int create_new_instance(std::string &error_msg, instance_info &info, std::string_view owner_pubkey, const std::string &contract_id, const std::string &image_key) + int create_new_instance(std::string &error_msg, instance_info &info, std::string_view container_name, std::string_view owner_pubkey, const std::string &contract_id, const std::string &image_key) { // If the max alloved instance count is already allocated. We won't allow more. const int allocated_count = sqlite::get_allocated_instance_count(db); @@ -145,7 +145,6 @@ namespace hp } const std::string image_name = img_itr->second; - std::string container_name = crypto::generate_uuid(); // This will be the docker container name as well as the contract folder name. int retries = 0; // If the generated uuid is already assigned to a container, we try generating a // unique uuid with max tries limited under a threshold. @@ -853,7 +852,7 @@ namespace hp * @param storage_kbytes Disk quota allowed for this user. * @param instance_ports Ports assigned to the instance. */ - int install_user(int &user_id, std::string &username, const size_t max_cpu_us, const size_t max_mem_kbytes, const size_t max_swap_kbytes, const size_t storage_kbytes, const std::string container_name, const ports instance_ports) + int install_user(int &user_id, std::string &username, const size_t max_cpu_us, const size_t max_mem_kbytes, const size_t max_swap_kbytes, const size_t storage_kbytes, std::string_view container_name, const ports instance_ports) { const std::vector input_params = { std::to_string(max_cpu_us), diff --git a/src/hp_manager.hpp b/src/hp_manager.hpp index d68ceff..664a813 100644 --- a/src/hp_manager.hpp +++ b/src/hp_manager.hpp @@ -58,7 +58,7 @@ namespace hp void deinit(); - int create_new_instance(std::string &error_msg, instance_info &info, std::string_view owner_pubkey, const std::string &contract_id, const std::string &image_key); + int create_new_instance(std::string &error_msg, instance_info &info, std::string_view container_name, std::string_view owner_pubkey, const std::string &contract_id, const std::string &image_key); int initiate_instance(std::string &error_msg, std::string_view container_name, const msg::initiate_msg &config_msg); @@ -85,7 +85,7 @@ namespace hp int write_json_values(jsoncons::ojson &d, const msg::config_struct &config); - int install_user(int &user_id, std::string &username, const size_t max_cpu_us, const size_t max_mem_kbytes, const size_t max_swap_kbytes, const size_t storage_kbytes, const std::string container_name, const ports instance_ports); + int install_user(int &user_id, std::string &username, const size_t max_cpu_us, const size_t max_mem_kbytes, const size_t max_swap_kbytes, const size_t storage_kbytes, std::string_view container_name, const ports instance_ports); int uninstall_user(std::string_view username, const ports assigned_ports, std::string_view instance_name); diff --git a/src/msg/json/msg_json.cpp b/src/msg/json/msg_json.cpp index adfe250..b130cc8 100644 --- a/src/msg/json/msg_json.cpp +++ b/src/msg/json/msg_json.cpp @@ -82,6 +82,18 @@ namespace msg::json if (extract_type(msg.type, d) == -1) return -1; + if (!d.contains(msg::FLD_CONTAINER_NAME)) + { + LOG_ERROR << "Field container_name is missing."; + return -1; + } + + if (!d[msg::FLD_CONTAINER_NAME].is()) + { + LOG_ERROR << "Invalid container_name value."; + return -1; + } + if (!d.contains(msg::FLD_PUBKEY)) { LOG_ERROR << "Field owner_pubkey is missing."; @@ -118,6 +130,7 @@ namespace msg::json return -1; } + msg.container_name = d[msg::FLD_CONTAINER_NAME].as(); msg.pubkey = d[msg::FLD_PUBKEY].as(); msg.contract_id = d[msg::FLD_CONTRACT_ID].as(); msg.image = d[msg::FLD_IMAGE].as(); diff --git a/src/msg/msg_common.hpp b/src/msg/msg_common.hpp index 7aa1083..7ad8908 100644 --- a/src/msg/msg_common.hpp +++ b/src/msg/msg_common.hpp @@ -9,6 +9,7 @@ namespace msg struct create_msg { std::string type; + std::string container_name; std::string pubkey; std::string contract_id; std::string image;