Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(kadena-cli): add kadena-cli devnet commands #1140

Closed
wants to merge 13 commits into from
Closed
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
5 changes: 5 additions & 0 deletions .changeset/large-snails-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@kadena/kadena-cli': patch
---

add devnet commands to kadena cli
27 changes: 26 additions & 1 deletion packages/libs/client-utils/etc/client-utils-coin.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,36 @@ event: "listen";
data: ICommandResult;
}], [], Promise<string> | Promise<undefined> | Promise<number> | Promise<false> | Promise<true> | Promise<IPactInt> | Promise<IPactDecimal> | Promise<Date> | Promise<PactValue[]>>;

// Warning: (ae-forgotten-export) The symbol "TCreatePrincipalAccountCommandInput" needs to be exported by the entry point index.d.ts
//
// @alpha (undocumented)
export const createPrincipalAccount: (inputs: TCreatePrincipalAccountCommandInput, config: IClientConfig) => Promise<IEmitterWrapper<[{
event: "sign";
data: ICommand;
}, {
event: "preflight";
data: ILocalCommandResult;
}, {
event: "submit";
data: ITransactionDescriptor;
}, {
event: "listen";
data: ICommandResult;
}], [], Promise<string> | Promise<undefined> | Promise<number> | Promise<false> | Promise<true> | Promise<IPactInt> | Promise<IPactDecimal> | Promise<Date> | Promise<PactValue[]>>>;

// Warning: (ae-forgotten-export) The symbol "ICreatePrincipalCommandInput" needs to be exported by the entry point index.d.ts
//
// @alpha (undocumented)
export const createPrincipalCommand: (inputs: ICreatePrincipalCommandInput, config: Omit<IClientConfig, 'sign'>) => Promise<() => IEmitterWrapper<[{
event: "dirtyRead";
data: ICommandResult;
}], [], Promise<string> | Promise<undefined> | Promise<number> | Promise<false> | Promise<true> | Promise<IPactInt> | Promise<IPactDecimal> | Promise<Date> | Promise<PactValue[]>>>;

// @alpha (undocumented)
export const details: (account: string, networkId: string, chainId: ChainId, host?: IClientConfig['host']) => Promise<string> | Promise<undefined> | Promise<number> | Promise<false> | Promise<true> | Promise<IPactInt> | Promise<IPactDecimal> | Promise<Date> | Promise<PactValue[]>;

// @alpha (undocumented)
export const getBalance: (account: string, networkId: string, chainId: ChainId, host?: IClientConfig['host']) => Promise<string> | Promise<undefined> | Promise<number> | Promise<false> | Promise<true> | Promise<IPactInt> | Promise<IPactDecimal> | Promise<Date> | Promise<PactValue[]>;
export const getBalance: (account: string, networkId: string, chainId: ChainId, host?: IClientConfig['host']) => Promise<void | undefined> | Promise<string | void> | Promise<number | void> | Promise<false | void> | Promise<true | void> | Promise<void | IPactInt> | Promise<void | IPactDecimal> | Promise<void | Date> | Promise<void | PactValue[]>;

// Warning: (ae-forgotten-export) The symbol "IRotateCommandInput" needs to be exported by the entry point index.d.ts
//
Expand Down
29 changes: 27 additions & 2 deletions packages/libs/client-utils/src/coin/create-account.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import type { ChainId } from '@kadena/client';
import { Pact, readKeyset } from '@kadena/client';
import {
addData,
addKeyset,
addSigner,
composePactCommand,
execution,
setMeta,
} from '@kadena/client/fp';

import { submitClient } from '../core/client-helpers';
import { pipe } from 'ramda';
import { dirtyReadClient, submitClient } from '../core/client-helpers';
import type { IClientConfig } from '../core/utils/helpers';

interface ICreateAccountCommandInput {
Expand All @@ -21,6 +22,8 @@ interface ICreateAccountCommandInput {
chainId: ChainId;
}

type TCreatePrincipalAccountCommandInput = Omit<ICreateAccountCommandInput, 'account'>;

const createAccountCommand = ({
account,
keyset,
Expand All @@ -35,10 +38,32 @@ const createAccountCommand = ({
addSigner(gasPayer.publicKeys, (signFor) => [signFor('coin.GAS')]),
setMeta({ senderAccount: gasPayer.account, chainId }),
);

/**
* @alpha
*/
export const createAccount = (
inputs: ICreateAccountCommandInput,
config: IClientConfig,
) => submitClient(config)(createAccountCommand(inputs));

/**
* @alpha
*/
export const createPrincipalAccount = async (
inputs: TCreatePrincipalAccountCommandInput,
config: IClientConfig,
) => {
const getPrincipal = pipe(
() => '(create-principal (read-keyset "ks"))',
execution,
addData('ks', inputs.keyset),
dirtyReadClient(config),
);

const account = await getPrincipal().execute();
return submitClient(config)(createAccountCommand({
account: account as string,
...inputs
}));
}
30 changes: 30 additions & 0 deletions packages/libs/client-utils/src/coin/create-principle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { ChainId } from '@kadena/client';
import {
addData,
execution,
} from '@kadena/client/fp';
import { pipe } from 'ramda';
import { dirtyReadClient } from '../core/client-helpers';
import type { IClientConfig } from '../core/utils/helpers';

interface ICreatePrincipalCommandInput {
keyset: {
keys: string[];
pred: 'keys-all' | 'keys-two' | 'keys-one';
};
gasPayer: { account: string; publicKeys: string[] };
chainId: ChainId;
}
/**
* @alpha
*/
export const createPrincipalCommand = async (
inputs: ICreatePrincipalCommandInput,
config: Omit<IClientConfig, 'sign'>,
) =>
pipe(
() => '(create-principal (read-keyset "ks"))',
execution,
addData('ks', inputs.keyset),
dirtyReadClient(config),
);
2 changes: 1 addition & 1 deletion packages/libs/client-utils/src/coin/get-balance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ export const getBalance = (
},
}),
);
return balance(account).execute();
return balance(account).execute().catch(e => console.log(e.message));
};
1 change: 1 addition & 0 deletions packages/libs/client-utils/src/coin/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './create-account';
export * from './create-principle';
export * from './details';
export * from './get-balance';
export * from './rotate';
Expand Down
1 change: 0 additions & 1 deletion packages/tools/kadena-cli/.kadena/keys/andy.plain.key

This file was deleted.

1 change: 0 additions & 1 deletion packages/tools/kadena-cli/.kadena/keys/b.hd.phrase

This file was deleted.

1 change: 0 additions & 1 deletion packages/tools/kadena-cli/.kadena/keys/n.hd.phrase

This file was deleted.

2 changes: 1 addition & 1 deletion packages/tools/kadena-cli/.kadena/networks/testnet.yaml
Copy link
Member

Choose a reason for hiding this comment

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

The .kadena shouldn't be checked in, right?

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
network: testnet
networkId: testnet01
networkId: testnet04
networkHost: https://api.testnet.chainweb.com
networkExplorerUrl: https://explorer.chainweb.com/testnet/tx/
1 change: 1 addition & 0 deletions packages/tools/kadena-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"dependencies": {
"@inquirer/prompts": "^3.0.4",
"@kadena/client": "workspace:^",
"@kadena/client-utils": "workspace:^",
jessevanmuijden marked this conversation as resolved.
Show resolved Hide resolved
"@kadena/cryptography-utils": "workspace:*",
"@kadena/pactjs": "workspace:*",
"@kadena/pactjs-cli": "workspace:^",
Expand Down
115 changes: 115 additions & 0 deletions packages/tools/kadena-cli/src/account/accountHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { getExistingAccounts, sanitizeFilename } from "../utils/helpers.js";
import { defaultAccountsPath, accountDefaults } from "../constants/accounts.js";
import { removeFile, writeFile } from "../utils/filesystem.js";
import { existsSync, readFileSync, type WriteFileOptions } from 'fs';
import yaml from 'js-yaml';
import path from 'path';
import chalk from 'chalk';
import { ChainId } from "@kadena/types";

export interface ICustomAccountsChoice {
value: string;
name?: string;
description?: string;
disabled?: boolean | string;
}

export interface IAccountCreateOptions {
name: string;
account: string;
keyset: string;
network: string;
chainId: ChainId;
}

/**
* Removes the given account from the accounts folder
*
* @param {Pick<IAccountCreateOptions, 'name'>} options - The account configuration.
* @param {string} options.name - The name of the account.
*/
export function removeAccount(options: Pick<IAccountCreateOptions, 'name'>): void {
const { name } = options;
jessevanmuijden marked this conversation as resolved.
Show resolved Hide resolved
const sanitizedName = sanitizeFilename(name).toLowerCase();
const accountFilePath = path.join(
defaultAccountsPath,
`${sanitizedName}.yaml`,
);

removeFile(accountFilePath);
}

export function writeAccount(options: IAccountCreateOptions): void {
const { name } = options;
const sanitizedName = sanitizeFilename(name).toLowerCase();
const accountFilePath = path.join(
defaultAccountsPath,
`${sanitizedName}.yaml`,
);

writeFile(
accountFilePath,
yaml.dump(options),
'utf8' as WriteFileOptions,
);
jessevanmuijden marked this conversation as resolved.
Show resolved Hide resolved
}

export async function displayAccountsConfig(): Promise<void> {
const log = console.log;
const formatLength = 80; // Maximum width for the display

const displaySeparator = (): void => {
log(chalk.green('-'.padEnd(formatLength, '-')));
};

const formatConfig = (
key: string,
value?: string,
isDefault?: boolean,
): string => {
const valueDisplay =
(value?.trim() ?? '') !== '' ? chalk.green(value!) : chalk.red('Not Set');

const defaultIndicator =
isDefault === true ? chalk.yellow(' (Using defaults)') : '';
const keyValue = `${key}: ${valueDisplay}${defaultIndicator}`;
const remainingWidth =
formatLength - keyValue.length > 0 ? formatLength - keyValue.length : 0;
return ` ${keyValue}${' '.repeat(remainingWidth)} `;
};


const existingAccounts: ICustomAccountsChoice[] = await getExistingAccounts();

existingAccounts.forEach(({ value }) => {
const filePath = path.join(defaultAccountsPath, `${value}.yaml`);
if (! existsSync) {
return;
}

const accountConfig = (yaml.load(
readFileSync(filePath, 'utf8'),
) as IAccountCreateOptions);

displaySeparator();
log(formatConfig('Name', value));
log(formatConfig('Account', accountConfig.account));
log(formatConfig('Keyset', accountConfig.keyset));
log(formatConfig('Network', accountConfig.network));
log(formatConfig('Chain ID', accountConfig.chainId.toString()));
});

displaySeparator();
};

export function loadAccountConfig(name: string): IAccountCreateOptions | never {
const filePath = path.join(defaultAccountsPath, `${name}.yaml`);

if (! existsSync(filePath)) {
throw new Error('Account file not found.')
}

return (yaml.load(
readFileSync(filePath, 'utf8'),
) as IAccountCreateOptions);
};
97 changes: 97 additions & 0 deletions packages/tools/kadena-cli/src/account/createAccountCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { createAccount, createPrincipalAccount, createPrincipalCommand } from '@kadena/client-utils/coin';
import { createCommand } from '../utils/createCommand.js';
import { globalOptions } from '../utils/globalOptions.js';
import chalk from 'chalk';
import { ChainId, createSignWithKeypair } from '@kadena/client';
import { writeAccount } from './accountHelpers.js';
import { loadKeysetConfig } from '../keyset/keysetHelpers.js';
import { loadKeypairConfig } from '../keypair/keypairHelpers.js';

// eslint-disable-next-line @rushstack/typedef-var
export const createAccountCommand = createCommand(
'create',
'Create account',
[globalOptions.accountName(), globalOptions.keyset(), globalOptions.network(), globalOptions.chainId(), globalOptions.gasPayer()],
async (config) => {
try {
const publicKeys = config.keysetConfig.publicKeys.split(',').map(value => value.trim()).filter(value => value.length);
for (let keypair of config.keysetConfig.publicKeysFromKeypairs) {
const keypairConfig = await loadKeypairConfig(keypair)
publicKeys.push(keypairConfig.publicKey || '');
}

const createPrincipal = await createPrincipalCommand({
keyset: {
pred: config.keysetConfig.predicate as 'keys-all' | 'keys-two' | 'keys-one',
keys: publicKeys,
},
gasPayer: { account: 'dummy', publicKeys: [] },
chainId: config.chainId as ChainId,
},
{
host: config.networkConfig.networkHost,
defaults: {
networkId: config.networkConfig.networkId,
meta: {
chainId: config.chainId as ChainId,
}
},
});

const account = await createPrincipal().execute();

writeAccount({
...config,
name: config.account,
account: account as string,
});

console.log(chalk.green(`\nSaved the account configuration "${config.account}".\n`));

// @todo: make gas payer configuration more flexible.
const gasPayerKeyset = await loadKeysetConfig(config.gasPayerConfig.keyset);
// @todo: now it is simply assumed that the gas payer account is governed with a keypair
const gasPayerKeypair = await loadKeypairConfig(gasPayerKeyset.publicKeysFromKeypairs.pop() || '');

// @todo: also allow other signing methods
const signWithKeyPair = createSignWithKeypair({
publicKey: gasPayerKeypair.publicKey,
secretKey: gasPayerKeypair.secretKey,
});

const c = await createAccount(
{
account: account as string,
keyset: {
pred: config.keysetConfig.predicate as 'keys-all' | 'keys-two' | 'keys-one',
keys: publicKeys,
},
gasPayer: { account: config.gasPayerConfig.account, publicKeys: [gasPayerKeypair.publicKey] },
chainId: config.chainId as ChainId,
},
{
host: config.networkConfig.networkHost,
defaults: {
networkId: config.networkConfig.networkId,
meta: {
chainId: config.chainId as ChainId,
}
},
sign: signWithKeyPair,
},
)

const result = await c.execute();

console.log(result);

if (result === 'Write succeeded') {
console.log(chalk.green(`\nCreated account "${account}" guarded by keyset "${config.keyset}" on chain ${config.chainId} of "${config.network}".\n`));
} else {
console.log(chalk.red(`\nFailed to created the account on "${config.network}".\n`));
}
} catch (e) {
console.log(chalk.red(e.message));
}
},
);
Loading
Loading