diff --git a/package-lock.json b/package-lock.json index 058091c..2f93b60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "@super-protocol/ctl", - "version": "0.11.0", + "version": "0.11.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@super-protocol/ctl", - "version": "0.11.0", + "version": "0.11.6", "license": "MIT", "dependencies": { "@amplitude/node": "^1.10.2", "@iarna/toml": "^2.2.5", "@super-protocol/distributed-secrets": "1.1.7", "@super-protocol/dto-js": "1.1.2", - "@super-protocol/sdk-js": "3.11.0", + "@super-protocol/sdk-js": "3.11.4", "@super-protocol/sp-files-addon": "^0.11.0", "@types/tar-stream": "^3.1.3", "axios": "1.6.2", @@ -5755,26 +5755,25 @@ } }, "node_modules/@super-protocol/pki-common": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/@super-protocol/pki-common/-/pki-common-1.6.4.tgz", - "integrity": "sha512-Hm8HRZBWnUCtC/LBPEdGPPGRgBzMaM0y3bP+W7WfxNBFdGFvChIoURaObg81aDOxgmj4hUk4yImr+911pRD2rQ==", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@super-protocol/pki-common/-/pki-common-1.6.5.tgz", + "integrity": "sha512-r4/EcmwM4pl3/CNuQKz9BMTO/TKaO6aNlsG/CDboA8xArXNwuvq6XY7tfriAJ/5z87ck/WGIEvdoz4lNeaGSgg==", "dependencies": { "hi-base32": "^0.5.1", "node-forge": "^1.3.1" } }, "node_modules/@super-protocol/sdk-js": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/@super-protocol/sdk-js/-/sdk-js-3.11.0.tgz", - "integrity": "sha512-S3vHN7eQ7dW3AuLex1yNraWL/3e4Vh0aeng4Be7ZhOYhYNJ6MuNXUr7Fg/4HRIfi4+PCGfKvP23ZR9WLpOU8Fg==", - "license": "MIT", + "version": "3.11.4", + "resolved": "https://registry.npmjs.org/@super-protocol/sdk-js/-/sdk-js-3.11.4.tgz", + "integrity": "sha512-woqmqVFjAT0iN7rECQIxkuMcdw4Bjoh+kO61Iu1qiNmXjXjDCFnFIw3dDSLi8qXSgWo89yEhYRhUSM2VYwPSoA==", "dependencies": { "@aws-sdk/client-s3": "^3.470.0", "@fidm/x509": "^1.2.1", "@msgpack/msgpack": "^2.8.0", "@sinclair/typebox": "0.33.17", "@super-protocol/dto-js": "1.1.10", - "@super-protocol/pki-common": "1.6.4", + "@super-protocol/pki-common": "1.6.5", "@super-protocol/uplink-nodejs": "^1.2.20", "asn1js": "^3.0.5", "axios": "^1.5.1", diff --git a/package.json b/package.json index 6d0b92d..75fab72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@super-protocol/ctl", - "version": "0.11.0", + "version": "0.11.6", "description": "A tool for publishing values in a secure and reliable way.", "main": "./build/index.js", "type": "commonjs", @@ -36,7 +36,7 @@ "@iarna/toml": "^2.2.5", "@super-protocol/distributed-secrets": "1.1.7", "@super-protocol/dto-js": "1.1.2", - "@super-protocol/sdk-js": "3.11.0", + "@super-protocol/sdk-js": "3.11.4", "@super-protocol/sp-files-addon": "^0.11.0", "@types/tar-stream": "^3.1.3", "axios": "1.6.2", diff --git a/src/commands/offerAddVersion.ts b/src/commands/offerAddVersion.ts new file mode 100644 index 0000000..88c2d18 --- /dev/null +++ b/src/commands/offerAddVersion.ts @@ -0,0 +1,33 @@ +import { Config as BlockchainConfig } from '@super-protocol/sdk-js'; +import addValueOfferVersion from '../services/addValueOfferVersion'; +import Printer from '../printer'; +import readOfferVersionInfo from '../services/readOfferVersionInfo'; +import initBlockchainConnector from '../services/initBlockchainConnector'; + +export type OfferAddVersionParams = { + actionAccountKey: string; + blockchainConfig: BlockchainConfig; + id: string; + ver: number; + path: string; +}; + +export default async (params: OfferAddVersionParams): Promise => { + try { + Printer.print('Connecting to the blockchain'); + await initBlockchainConnector({ + blockchainConfig: params.blockchainConfig, + actionAccountKey: params.actionAccountKey, + }); + const versionInfo = await readOfferVersionInfo({ path: params.path }); + const newVersion = await addValueOfferVersion({ + action: params.actionAccountKey, + version: params.ver, + versionInfo, + offerId: params.id, + }); + Printer.print(`Version ${newVersion} added successfully`); + } catch (error: any) { + Printer.print('Version add failed with error: ' + error?.message); + } +}; diff --git a/src/commands/offerDisableVersion.ts b/src/commands/offerDisableVersion.ts new file mode 100644 index 0000000..a8512a3 --- /dev/null +++ b/src/commands/offerDisableVersion.ts @@ -0,0 +1,25 @@ +import { Offer, Config as BlockchainConfig } from '@super-protocol/sdk-js'; +import Printer from '../printer'; +import initBlockchainConnector from '../services/initBlockchainConnector'; + +export type OffersDisableVersionParams = { + blockchainConfig: BlockchainConfig; + actionAccountKey: string; + id: string; + ver: number; +}; + +export default async (params: OffersDisableVersionParams): Promise => { + try { + Printer.print('Connecting to the blockchain'); + const actionAddress = await initBlockchainConnector({ + blockchainConfig: params.blockchainConfig, + actionAccountKey: params.actionAccountKey, + }); + const offer = new Offer(params.id); + await offer.deleteVersion(params.ver, { from: actionAddress }); + Printer.print(`Version ${params.ver} for offer ${params.id} successfully removed`); + } catch (error: any) { + Printer.print('Remove offer version failed with error: ' + error?.message); + } +}; diff --git a/src/commands/workflowsReplenishDeposit.ts b/src/commands/workflowsReplenishDeposit.ts new file mode 100644 index 0000000..3195cf5 --- /dev/null +++ b/src/commands/workflowsReplenishDeposit.ts @@ -0,0 +1,73 @@ +import { Config as BlockchainConfig, Orders, OrderStatus } from '@super-protocol/sdk-js'; +import inquirer, { QuestionCollection } from 'inquirer'; +import Printer from '../printer'; +import initBlockchainConnectorService from '../services/initBlockchainConnector'; +import checkOrderService from '../services/checkOrder'; +import { etherToWei, weiToEther } from '../utils'; + +export type WorkflowReplenishDepositParams = { + blockchainConfig: BlockchainConfig; + actionAccountKey: string; + minutes: number; + sppi: string; + orderId: string; + yes: boolean; +}; + +const isActionConfirmed = async (question: string): Promise => { + Printer.print(question); + + const questions: QuestionCollection = [ + { + type: 'confirm', + name: 'confirmation', + message: `Confirm?`, + default: true, + }, + ]; + const answers = await inquirer.prompt(questions); + + return answers.confirmation; +}; + +export default async (params: WorkflowReplenishDepositParams): Promise => { + Printer.print('Connecting to the blockchain'); + await initBlockchainConnectorService({ + blockchainConfig: params.blockchainConfig, + actionAccountKey: params.actionAccountKey, + }); + + try { + Printer.print(`Checking order ${params.orderId}`); + await checkOrderService({ + id: params.orderId, + statuses: [OrderStatus.Blocked, OrderStatus.Processing, OrderStatus.New], + }); + const amountPerHour = await Orders.calculateWorkflowPerHourPrice(params.orderId); + let tokenAmount: bigint; + let minutes: number; + + if (params.minutes) { + tokenAmount = (BigInt(amountPerHour) * BigInt(params.minutes)) / 60n; + minutes = params.minutes; + } else if (params.sppi) { + tokenAmount = etherToWei(params.sppi).toBigInt(); + minutes = Number((tokenAmount * 60n) / BigInt(amountPerHour)); + } else { + throw new Error('To complete command please define one of arguments --sppi or --minutes'); + } + + const confirmed = + params.yes || + (await isActionConfirmed( + `Deposit will be replenished by ${weiToEther(tokenAmount)} SPPI. Order time is extended by ${minutes} minutes.`, + )); + if (confirmed) { + await Orders.replenishWorkflowDeposit(params.orderId, tokenAmount.toString()); + + Printer.print('Deposit replenished successfully'); + } + } catch (error: any) { + Printer.print(`Order ${params.orderId} was not replenished: ${error?.message}`); + } +}; diff --git a/src/index.ts b/src/index.ts index ad9086e..66ee561 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,8 +22,17 @@ import ordersCancel from './commands/ordersCancel'; import ordersComplete, { OrderCompleteParams } from './commands/ordersComplete'; import ordersReplenishDeposit from './commands/ordersReplenishDeposit'; import workflowsCreate, { WorkflowCreateCommandParams } from './commands/workflowsCreate'; +import workflowsReplenishDeposit, { + WorkflowReplenishDepositParams, +} from './commands/workflowsReplenishDeposit'; import Printer from './printer'; -import { collectOptions, commaSeparatedList, processSubCommands, validateFields } from './utils'; +import { + collectOptions, + commaSeparatedList, + parseNumber, + processSubCommands, + validateFields, +} from './utils'; import generateSolutionKey from './commands/solutionsGenerateKey'; import prepareSolution from './commands/solutionsPrepare'; import ordersDownloadResult, { FilesDownloadParams } from './commands/ordersDownloadResult'; @@ -50,6 +59,8 @@ import offersUpdateOption from './commands/offersUpdateOption'; import offersDeleteOption from './commands/offersDeleteOption'; import offersGetSlot from './commands/offersGetSlot'; import offersGetOption from './commands/offersGetOption'; +import offerAddVersion from './commands/offerAddVersion'; +import offerDisableVersion from './commands/offerDisableVersion'; import { checkForUpdates } from './services/checkReleaseVersion'; import setup from './commands/setup'; import { workflowGenerateKey } from './commands/workflowsGenerateKey'; @@ -99,6 +110,8 @@ async function main(): Promise { const offersCommand = program.command('offers'); const offersListCommand = offersCommand.command('list'); const offersGetCommand = offersCommand.command('get'); + const offerCommand = program.command('offer'); + const offerVersionCommand = offerCommand.command('version'); const quotesCommand = program.command('quotes'); program.addCommand(secretsCommand); @@ -463,6 +476,41 @@ async function main(): Promise { await workflowsCreate(requestParams); }); + workflowsCommand + .command('replenish-deposit') + .argument('', 'Order Id') + .option('--sppi ', 'SPPI to add to the order') + .addOption( + new Option( + '--minutes ', + 'Time to extend order (automatically calculates tokens)', + ).argParser((val) => parseInt(val) || 0), + ) + .option('--yes', 'Silent question mode. All answers will be yes', false) + .action( + async ( + orderId: string, + options: { config: string; sppi: string; minutes: number; yes: boolean }, + ) => { + const configLoader = new ConfigLoader(options.config); + const actionAccountKey = configLoader.loadSection('blockchain').accountPrivateKey; + const blockchain = configLoader.loadSection('blockchain'); + const blockchainConfig = { + contractAddress: blockchain.smartContractAddress, + blockchainUrl: blockchain.rpcUrl, + }; + const requestParams: WorkflowReplenishDepositParams = { + blockchainConfig, + actionAccountKey, + orderId, + minutes: options.minutes, + sppi: options.sppi, + yes: options.yes, + }; + await workflowsReplenishDeposit(requestParams); + }, + ); + const ordersListFields = [ 'id', 'offer_name', @@ -1387,6 +1435,48 @@ async function main(): Promise { }); }); + offerVersionCommand + .command('add') + .description('Add new version of offer') + .requiredOption('--offer ', 'Offer id') + .requiredOption('--path ', 'Path to offer version info file') + .option('--ver ', 'Version number to add', parseNumber) + .action(async (options: { offer: string; path: string; ver: number; config: string }) => { + const configLoader = new ConfigLoader(options.config); + const blockchain = configLoader.loadSection('blockchain'); + const blockchainConfig = { + contractAddress: blockchain.smartContractAddress, + blockchainUrl: blockchain.rpcUrl, + }; + await offerAddVersion({ + actionAccountKey: blockchain.accountPrivateKey, + blockchainConfig, + id: options.offer, + ver: options.ver, + path: options.path, + }); + }); + + offerVersionCommand + .command('disable') + .description('Disable offer version') + .requiredOption('--offer ', 'Offer id') + .requiredOption('--ver ', 'Version number to delete', parseNumber) + .action(async (options: { offer: string; ver: number; config: string }) => { + const configLoader = new ConfigLoader(options.config); + const blockchain = configLoader.loadSection('blockchain'); + const blockchainConfig = { + contractAddress: blockchain.smartContractAddress, + blockchainUrl: blockchain.rpcUrl, + }; + await offerDisableVersion({ + blockchainConfig, + id: options.offer, + ver: options.ver, + actionAccountKey: blockchain.accountPrivateKey, + }); + }); + filesCommand .command('upload') .description('Upload a file specified by the argument to the remote storage') diff --git a/src/services/createOrder.ts b/src/services/createOrder.ts index befd67f..3d2ddc3 100644 --- a/src/services/createOrder.ts +++ b/src/services/createOrder.ts @@ -1,4 +1,4 @@ -import { OrderInfo, Orders, OrderSlots } from '@super-protocol/sdk-js'; +import { BlockchainConnector, OrderInfo, Orders, OrderSlots } from '@super-protocol/sdk-js'; import doWithRetries from './doWithRetries'; export type CreateOrderParams = { @@ -9,11 +9,17 @@ export type CreateOrderParams = { }; export default async (params: CreateOrderParams): Promise => { + const workflowCreationBLock = await BlockchainConnector.getInstance().getLastBlockInfo(); + const orderLoaderFn = async (): Promise => { - const event = await Orders.getByExternalId({ - externalId: params.orderInfo.externalId, - consumer: params.consumerAddress, - }); + const event = await Orders.getByExternalId( + { + externalId: params.orderInfo.externalId, + consumer: params.consumerAddress, + }, + workflowCreationBLock.index, + 'latest', + ); if (event && event?.orderId !== '-1') { return event.orderId; diff --git a/src/services/readOfferVersionInfo.ts b/src/services/readOfferVersionInfo.ts new file mode 100644 index 0000000..84248c2 --- /dev/null +++ b/src/services/readOfferVersionInfo.ts @@ -0,0 +1,23 @@ +import { OfferVersionInfo } from '@super-protocol/sdk-js'; +import readJsonFile from './readJsonFile'; +import { HashValidator } from '../services/readResourceFile'; +import { z } from 'zod'; + +export type ReadFileParams = { + path: string; +}; + +const OfferVersionInfoSchema = z.object({ + metadata: z.object({}).catchall(z.unknown()).optional(), + signatureKeyHash: HashValidator.optional(), + hash: HashValidator.optional(), +}); + +export default async (params: ReadFileParams): Promise => { + const offerOption = await readJsonFile({ + path: params.path, + validator: OfferVersionInfoSchema, + }); + + return offerOption; +}; diff --git a/src/services/readValueOfferInfo.ts b/src/services/readValueOfferInfo.ts index ff6f682..95a3df5 100644 --- a/src/services/readValueOfferInfo.ts +++ b/src/services/readValueOfferInfo.ts @@ -16,8 +16,12 @@ export type ReadValueOfferInfoFileParams = { }; const OfferInfoRestrictionsSchema = z.object({ - offers: z.array(z.string()), - versions: z.array(z.number()), + offers: z.array( + z.object({ + id: z.string(), + version: z.number(), + }), + ), types: z.array(z.nativeEnum(OfferType)), }); diff --git a/src/utils.ts b/src/utils.ts index b8f5649..e511da4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ import { promisify } from 'util'; import { exec as execCallback } from 'child_process'; -import { Command } from 'commander'; +import { Command, InvalidArgumentError } from 'commander'; import { DateTime } from 'luxon'; import { BigNumber, BigNumberish, ethers } from 'ethers'; import { ErrorMessageOptions, generateErrorMessage } from 'zod-error'; @@ -200,6 +200,12 @@ export const tryParse = (text: string) => { } }; +export const parseNumber = (val: string): number => { + const parsed = Number(val); + if (Number.isNaN(parsed)) throw new InvalidArgumentError('Not a number.'); + return parsed; +}; + export const isStorageConfigValid = (access: Config['storage']): boolean => Boolean(access.bucket && access.readAccessToken && access.writeAccessToken);