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
4 changes: 3 additions & 1 deletion clijs/bin/dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
// eslint-disable-next-line n/shebang
import {execute} from '@oclif/core'

await execute({development: true, dir: import.meta.url})
await execute({development: true, dir: import.meta.url});

setImmediate(() => process.exit(0));
4 changes: 3 additions & 1 deletion clijs/bin/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@

import {execute} from '@oclif/core'

await execute({dir: import.meta.url})
await execute({dir: import.meta.url});

setImmediate(() => process.exit(0));
7 changes: 7 additions & 0 deletions clijs/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,15 @@ abstract class BaseCommand extends Command {
}

protected async finally(err: Error | undefined): Promise<unknown> {
this.closeConnection();
return super.finally(err);
}

protected closeConnection() {
this.rpcClient?.closeConnection();
this.cometaClient?.closeConnection();
this.faucetClient?.closeConnection();
}
}

export { BaseCommand };
17 changes: 14 additions & 3 deletions clijs/src/commands/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,28 @@ export default class BlockCommand extends BaseCommand {
throw new Error("RPC client is not initialized");
}

let block: Block<boolean>;

if (/^0x[0-9a-fA-F]+$/.test(args.blockId)) {
return await this.rpcClient.getBlockByHash(args.blockId as Hex);
block = await this.rpcClient.getBlockByHash(args.blockId as Hex);
// biome-ignore lint/style/noUselessElse: <explanation>
} else if (validBlockTags.includes(args.blockId as BlockTag)) {
return await this.rpcClient.getBlockByNumber(
block = await this.rpcClient.getBlockByNumber(
args.blockId as BlockTag,
undefined,
flags.shardId,
);
} else {
block = await this.rpcClient.getBlockByNumber(toHex(args.blockId), undefined, flags.shardId);
}

if (flags.quiet) {
this.log(block as unknown as string);
} else {
this.log("Block:", block);
}
return await this.rpcClient.getBlockByNumber(toHex(args.blockId), undefined, flags.shardId);

return block;
}
}

Expand Down
33 changes: 33 additions & 0 deletions clijs/src/commands/receipt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { ProcessedReceipt } from "@nilfoundation/niljs";
import { BaseCommand } from "../base.js";
import { hexArg } from "../types.js";

export default class ReceiptCommand extends BaseCommand {
static override description = "Retrieve a receipt from the cluster";

static override examples = ["<%= config.bin %> <%= command.id %>"];

static args = {
hash: hexArg({
name: "hash",
required: true,
description: "transaction hash",
}),
};

async run(): Promise<ProcessedReceipt | null> {
const { args, flags } = await this.parse(ReceiptCommand);

if (!this.rpcClient) {
throw new Error("RPC client is not initialized");
}

const res = await this.rpcClient.getTransactionReceiptByHash(args.hash);
if (flags.quiet) {
this.log(res as unknown as string);
} else {
this.log("Receipt data:", res as unknown as string);
}
return res;
Comment thread
Gezort marked this conversation as resolved.
}
}
35 changes: 18 additions & 17 deletions clijs/src/common/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ import ConfigManager, { ConfigKeys } from "./config.js";

describe("ConfigManager", () => {
CliTest("should create a default config file if it does not exist", async ({ cfgPath }) => {
Comment thread
Gezort marked this conversation as resolved.
const configManager = new ConfigManager(cfgPath);
fs.rmSync(cfgPath, { recursive: true, force: true });
expect(fs.existsSync(cfgPath)).toBe(false);
new ConfigManager(cfgPath);
expect(fs.existsSync(cfgPath)).toBe(true);
});

CliTest("should load the default config", async ({ cfgPath }) => {
const configManager = new ConfigManager(cfgPath);
CliTest("should load the default config", async ({ configManager }) => {
const config = configManager.loadConfig();
expect(config).toHaveProperty("nil");
});

CliTest("should get a config value", async ({ cfgPath }) => {
const configManager = new ConfigManager(cfgPath);
CliTest("should get a config value", async ({ configManager }) => {
configManager.updateConfig("nil", ConfigKeys.RpcEndpoint, "http://127.0.0.1:8529");
const value = configManager.getConfigValue("nil", ConfigKeys.RpcEndpoint);
expect(value).toBe("http://127.0.0.1:8529");
Expand All @@ -25,26 +25,27 @@ describe("ConfigManager", () => {
expect(fallbackValue).toBe(value);
});

CliTest("should update a config value", async ({ cfgPath }) => {
const configManager = new ConfigManager(cfgPath);
CliTest("should update a config value", async ({ configManager }) => {
configManager.updateConfig("nil", ConfigKeys.RpcEndpoint, "http://127.0.0.1:1010");
const config = configManager.loadConfig();
expect(config.nil).toHaveProperty(ConfigKeys.RpcEndpoint, "http://127.0.0.1:1010");
});

CliTest("should preserve comments and structure when saving", async ({ cfgPath }) => {
const initialContent = `; Comment line
CliTest(
"should preserve comments and structure when saving",
async ({ cfgPath, configManager }) => {
const initialContent = `; Comment line
[nil]
rpc_endpoint = http://127.0.0.1:8529
`;
fs.writeFileSync(cfgPath, initialContent, "utf8");
fs.writeFileSync(cfgPath, initialContent, "utf8");

const configManager = new ConfigManager(cfgPath);
configManager.updateConfig("nil", ConfigKeys.CometaEndpoint, "http://127.0.0.1:1234");
const content = fs.readFileSync(cfgPath, "utf8");
configManager.updateConfig("nil", ConfigKeys.CometaEndpoint, "http://127.0.0.1:1234");
const content = fs.readFileSync(cfgPath, "utf8");

expect(content).toContain("; Comment line");
expect(content).toContain("rpc_endpoint = http://127.0.0.1:8529");
expect(content).toContain("cometa_endpoint = http://127.0.0.1:1234");
});
expect(content).toContain("; Comment line");
expect(content).toContain("rpc_endpoint = http://127.0.0.1:8529");
expect(content).toContain("cometa_endpoint = http://127.0.0.1:1234");
},
);
});
3 changes: 3 additions & 0 deletions clijs/src/sea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import AbiCommand from "./commands/abi";
import AbiDecode from "./commands/abi/decode";
import AbiEncode from "./commands/abi/encode";
import BlockCommand from "./commands/block";
import ReceiptCommand from "./commands/receipt";
import SmartAccountBalance from "./commands/smart-account/balance.js";
import SmartAccountCallReadOnly from "./commands/smart-account/call-readonly";
import SmartAccountDeploy from "./commands/smart-account/deploy.js";
Expand Down Expand Up @@ -40,6 +41,8 @@ export const COMMANDS: Record<string, Command.Class> = {
"keygen:new": KeygenNew,
"keygen:new-p2p": KeygenNewP2p,

receipt: ReceiptCommand,

"smart-account": SmartAccount,
"smart-account:balance": SmartAccountBalance,
"smart-account:call-readonly": SmartAccountCallReadOnly,
Expand Down
25 changes: 25 additions & 0 deletions clijs/test/commands/receipt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Hex, ProcessedReceipt } from "@nilfoundation/niljs";
import { describe, expect } from "vitest";
import { CliTest } from "../setup.js";

// To run this test you need to run the nild:
// nild run --http-port 8529
describe("receipt:get_receipt", () => {
CliTest("tests getting receipts", async ({ runCommand, smartAccount }) => {
const txHash = (
await runCommand([
"smart-account",
"send-tokens",
smartAccount.address,
"--amount",
"100",
"--feeCredit",
10_000_000_000_000 as unknown as string,
])
).result as Hex;
expect(txHash).toBeTruthy();

const r = (await runCommand(["receipt", txHash])).result as ProcessedReceipt;
expect(r.success).toBeTruthy();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's also check that output contains the string Receipt data:.
By the way, I guess in the case of --quiet we only need to print the json itself.

Copy link
Copy Markdown
Collaborator Author

@Gezort Gezort Apr 25, 2025

Choose a reason for hiding this comment

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

fixed. also fixed for the block command

regarding output: I haven't found an easy way to get it. inb4: r.stdout / r.stderr is empty

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Doesn't this approach work?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I did some research, and found out that without disableConsoleIntercept in vitest, intercepting the output in runCommand doesn't work. They are in conflict. We need to figure out how to make them work together correctly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm. The documentation for oclif/test explicitly says to use disableConsoleIntercept for vitest.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

});
});
31 changes: 26 additions & 5 deletions clijs/test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ async function createTempDir() {

interface CliTestFixture {
cfgPath: string;
configManager: ConfigManager;

runCommand: (args: string[]) => Promise<{
error?: Error & Partial<Errors.CLIError>;
Expand All @@ -47,7 +48,7 @@ interface CliTestFixture {
}

export const CliTest = test.extend<CliTestFixture>({
cfgPath: async ({ privateKey }, use) => {
cfgPath: async ({ privateKey, smartAccount }, use) => {
const { cfgDir, cfgPath } = await createTempDir();
const configManager = new ConfigManager(cfgPath);
configManager.updateConfig(ConfigKeys.NilSection, ConfigKeys.RpcEndpoint, testEnv.endpoint);
Expand All @@ -62,20 +63,24 @@ export const CliTest = test.extend<CliTestFixture>({
testEnv.faucetServiceEndpoint,
);
configManager.updateConfig(ConfigKeys.NilSection, ConfigKeys.PrivateKey, privateKey);
configManager.updateConfig(ConfigKeys.NilSection, ConfigKeys.Address, smartAccount.address);

await use(cfgPath);

fs.rmSync(cfgDir, { recursive: true, force: true });
},

configManager: async ({ cfgPath }, use) => {
const configManager = new ConfigManager(cfgPath);
await use(configManager);
},

runCommand: async ({ cfgPath }, use) => {
await use(async (cmdArgs: string[]) => {
const args = cmdArgs.concat(["-c", cfgPath]);
console.log("Running command:", args, "with root", path.join(__dirname, ".."));
const res = await runCommand(args, {
root: path.join(__dirname, ".."),
});
console.log("Command result:", res);
return res;
});
},
Expand All @@ -98,7 +103,11 @@ export const CliTest = test.extend<CliTestFixture>({
}),
}),

privateKey: generateRandomPrivateKey(),
// biome-ignore lint/correctness/noEmptyPattern:
privateKey: async ({}, use) => {
const key = generateRandomPrivateKey();
await use(key);
},

signer: async ({ privateKey }, use) => {
const signer = new LocalECDSAKeySigner({
Expand All @@ -107,14 +116,26 @@ export const CliTest = test.extend<CliTestFixture>({
await use(signer);
},

smartAccount: async ({ rpcClient, signer }, use) => {
smartAccount: async ({ rpcClient, faucetClient, signer }, use) => {
const smartAccount = new SmartAccountV1({
pubkey: signer.getPublicKey(),
salt: 100n,
shardId: 1,
client: rpcClient,
signer: signer,
});

const faucets = await faucetClient.getAllFaucets();
await faucetClient.topUpAndWaitUntilCompletion(
{
faucetAddress: faucets.NIL,
smartAccountAddress: smartAccount.address,
amount: 1_000_000_000_000_000_000n,
},
rpcClient,
);

smartAccount.selfDeploy(true);
await use(smartAccount);
},
});
7 changes: 7 additions & 0 deletions niljs/src/clients/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ class BaseClient {
this.shardId = config.shardId;
}

/**
* Closes the connection to the network.
*/
public closeConnection() {
this.transport.closeConnection();
}

/**
* Sends a request.
* @param requestObject The request object. It contains the request method and parameters.
Expand Down
3 changes: 3 additions & 0 deletions niljs/src/utils/faucet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,7 @@ export async function topUp({
},
client,
);

client.closeConnection();
faucetClient.closeConnection();
}