Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down
33 changes: 33 additions & 0 deletions src/commands/offerAddVersion.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
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);
}
};
Comment on lines +15 to +33
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing propagation of failures

Same as in offerDisableVersion.ts: the error is logged but not re-thrown, so upstream code can’t tell the command failed.

🤖 Prompt for AI Agents
In src/commands/offerAddVersion.ts around lines 15 to 33, the catch block logs
the error but does not re-throw it, preventing upstream code from detecting the
failure. Modify the catch block to re-throw the caught error after logging it,
ensuring that the failure propagates properly to calling functions.

25 changes: 25 additions & 0 deletions src/commands/offerDisableVersion.ts
Original file line number Diff line number Diff line change
@@ -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;
};
Comment on lines +5 to +10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Input contract lacks basic sanity checks

id might be an empty string and ver could be <1; this will go straight to the chain and revert.
Add lightweight guards before the blockchain call.

🤖 Prompt for AI Agents
In src/commands/offerDisableVersion.ts between lines 5 and 10, the
OffersDisableVersionParams type lacks validation for the id and ver fields. Add
checks before the blockchain call to ensure id is not an empty string and ver is
at least 1. If these conditions are not met, throw an error or return early to
prevent sending invalid data to the blockchain and causing a revert.


export default async (params: OffersDisableVersionParams): Promise<void> => {
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);
}
Comment on lines +22 to +24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Swallowed error obstructs CLI exit status

The catch block only logs the error. The CLI that invokes this command will interpret a resolved promise as success, masking failures (e.g., CI/CD scripts will think the version was removed).
After logging, re-throw the error or process.exit(1) so the caller can react.

-  } catch (error: any) {
-    Printer.print('Remove offer version failed with error: ' + error?.message);
+  } catch (error: unknown) {
+    Printer.print(`Remove offer version failed: ${(error as Error).message}`);
+    throw error;        // let the upper layer decide how to handle/exit
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error: any) {
Printer.print('Remove offer version failed with error: ' + error?.message);
}
} catch (error: unknown) {
Printer.print(`Remove offer version failed: ${(error as Error).message}`);
throw error; // let the upper layer decide how to handle/exit
}
🤖 Prompt for AI Agents
In src/commands/offerDisableVersion.ts around lines 22 to 24, the catch block
logs the error but does not propagate it, causing the CLI to treat failures as
successes. To fix this, after logging the error message, either re-throw the
caught error or call process.exit(1) to ensure the CLI exits with a failure
status and the caller can handle the error properly.

};
73 changes: 73 additions & 0 deletions src/commands/workflowsReplenishDeposit.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> => {
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<void> => {
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');
}
Comment on lines +50 to +58
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Calculation loses precision for non-divisible minute values

tokenAmount = amountPerHour * minutes / 60 truncates towards zero (BigInt division), potentially under-funding the order.
Example: amountPerHour = 100, minutes = 59tokenAmount = 98 instead of 98.333….
Consider ceiling division or informing the user about the rounding strategy.

🤖 Prompt for AI Agents
In src/commands/workflowsReplenishDeposit.ts around lines 50 to 58, the
calculation of tokenAmount uses integer division with BigInt, which truncates
and loses precision for non-divisible minute values. To fix this, implement a
ceiling division approach to round up the result instead of truncating, ensuring
the tokenAmount does not under-fund the order. Adjust the formula to calculate
tokenAmount as (amountPerHour * minutes + 59) / 60 or an equivalent method to
achieve ceiling division with BigInt.


const confirmed =
params.yes ||
(await isActionConfirmed(
`Deposit will be replenished by ${weiToEther(tokenAmount)} SPPI. Order time is extended by ${minutes} minutes.`,
));
Comment on lines +60 to +64
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Dual-input ambiguity

If the user provides both --minutes and --sppi, the first if (params.minutes) branch silently wins. Make this mutually exclusive and throw if both are set.

🤖 Prompt for AI Agents
In src/commands/workflowsReplenishDeposit.ts around lines 60 to 64, the code
allows both --minutes and --sppi parameters to be set simultaneously, but only
processes the --minutes branch silently. To fix this, add a check before this
logic to detect if both params.minutes and params.sppi are set, and if so, throw
an error indicating that these options are mutually exclusive. This prevents
ambiguity by enforcing that only one of these inputs can be provided at a time.

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}`);
}
Comment on lines +70 to +72
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Error object typed as any and swallowed

Re-throw after logging (see comments in other files) so the CLI command reflects failure status and exit code.

🤖 Prompt for AI Agents
In src/commands/workflowsReplenishDeposit.ts around lines 70 to 72, the catch
block types the error as any and only logs it without re-throwing. Modify the
catch block to re-throw the caught error after logging the message so that the
CLI command properly reflects the failure status and exit code.

};
92 changes: 91 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -99,6 +110,8 @@ async function main(): Promise<void> {
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);
Expand Down Expand Up @@ -463,6 +476,41 @@ async function main(): Promise<void> {
await workflowsCreate(requestParams);
});

workflowsCommand
.command('replenish-deposit')
.argument('<orderId>', 'Order Id')
.option('--sppi <SPPI>', 'SPPI to add to the order')
.addOption(
new Option(
'--minutes <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',
Expand Down Expand Up @@ -1387,6 +1435,48 @@ async function main(): Promise<void> {
});
});

offerVersionCommand
.command('add')
.description('Add new version of offer')
.requiredOption('--offer <id>', 'Offer id')
.requiredOption('--path <path>', 'Path to offer version info file')
.option('--ver <version>', '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,
});
});
Comment on lines +1438 to +1458
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

ver is optional in CLI but required in service type

When --ver is omitted, options.ver is undefined, yet OfferAddVersionParams.ver is non-optional. TypeScript will compile due to any, but at runtime the service receives undefined and may fail on-chain.

Either:

  1. Make the CLI option .requiredOption, or
  2. Change the param to ver?: number and let the service auto-increment.
🤖 Prompt for AI Agents
In src/index.ts around lines 1438 to 1458, the CLI option --ver is currently
optional but the service expects a non-optional version number, causing runtime
issues if omitted. To fix this, either change the CLI option to
.requiredOption('--ver <version>', ...) to enforce providing a version, or
update the service parameter type to make ver optional (ver?: number) and
implement logic to auto-increment the version when it is not provided. Choose
one approach and update the code accordingly to ensure ver is always valid when
passed to the service.


offerVersionCommand
.command('disable')
.description('Disable offer version')
.requiredOption('--offer <id>', 'Offer id')
.requiredOption('--ver <version>', '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 <localPath> argument to the remote storage')
Expand Down
16 changes: 11 additions & 5 deletions src/services/createOrder.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -9,11 +9,17 @@ export type CreateOrderParams = {
};

export default async (params: CreateOrderParams): Promise<string> => {
const workflowCreationBLock = await BlockchainConnector.getInstance().getLastBlockInfo();

const orderLoaderFn = async (): Promise<string> => {
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;
Expand Down
23 changes: 23 additions & 0 deletions src/services/readOfferVersionInfo.ts
Original file line number Diff line number Diff line change
@@ -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<OfferVersionInfo> => {
const offerOption = await readJsonFile({
path: params.path,
validator: OfferVersionInfoSchema,
});

return offerOption;
};
Loading